mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-09 18:32:55 +00:00
884 lines
31 KiB
TypeScript
884 lines
31 KiB
TypeScript
import fs from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import type { EmbeddedRunAttemptParams } from "openclaw/plugin-sdk/agent-harness-runtime";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import {
|
|
addSandboxShellDynamicToolsIfAvailable,
|
|
buildDynamicTools,
|
|
filterCodexDynamicToolsForAllowlist,
|
|
hasWildcardCodexToolsAllow,
|
|
includeForcedCodexDynamicToolAllow,
|
|
resetOpenClawCodingToolsFactoryForTests,
|
|
resolveOpenClawCodingToolsSessionKeys,
|
|
setOpenClawCodingToolsFactoryForTests,
|
|
shouldEnableCodexAppServerNativeToolSurface,
|
|
shouldForceMessageTool,
|
|
} from "./dynamic-tool-build.js";
|
|
import {
|
|
filterCodexDynamicTools,
|
|
resolveCodexDynamicToolsLoading,
|
|
} from "./dynamic-tool-profile.js";
|
|
import { createCodexDynamicToolBridge } from "./dynamic-tools.js";
|
|
import { createCodexTestModel } from "./test-support.js";
|
|
|
|
let tempDir: string;
|
|
|
|
type RuntimeDynamicToolForTest = Parameters<
|
|
typeof createCodexDynamicToolBridge
|
|
>[0]["tools"][number];
|
|
|
|
function createParams(sessionFile: string, workspaceDir: string): EmbeddedRunAttemptParams {
|
|
return {
|
|
prompt: "hello",
|
|
sessionId: "session-1",
|
|
sessionKey: "agent:main:session-1",
|
|
sessionFile,
|
|
workspaceDir,
|
|
runId: "run-1",
|
|
provider: "codex",
|
|
modelId: "gpt-5.4-codex",
|
|
model: createCodexTestModel("codex"),
|
|
contextTokenBudget: 150_000,
|
|
contextWindowInfo: {
|
|
tokens: 150_000,
|
|
referenceTokens: 200_000,
|
|
source: "agentContextTokens",
|
|
},
|
|
thinkLevel: "medium",
|
|
disableTools: true,
|
|
timeoutMs: 5_000,
|
|
authStorage: {} as never,
|
|
authProfileStore: { version: 1, profiles: {} },
|
|
modelRegistry: {} as never,
|
|
} as EmbeddedRunAttemptParams;
|
|
}
|
|
|
|
function createCodexRuntimePlanFixture(): NonNullable<EmbeddedRunAttemptParams["runtimePlan"]> {
|
|
return {
|
|
auth: {},
|
|
observability: {
|
|
resolvedRef: "codex/gpt-5.4-codex",
|
|
provider: "codex",
|
|
modelId: "gpt-5.4-codex",
|
|
harnessId: "codex",
|
|
},
|
|
prompt: {
|
|
resolveSystemPromptContribution: () => undefined,
|
|
},
|
|
tools: {
|
|
normalize: (tools: unknown[]) => tools,
|
|
logDiagnostics: () => undefined,
|
|
},
|
|
} as unknown as NonNullable<EmbeddedRunAttemptParams["runtimePlan"]>;
|
|
}
|
|
|
|
function createRuntimeDynamicTool(name: string): RuntimeDynamicToolForTest {
|
|
return {
|
|
name,
|
|
label: name,
|
|
description: `${name} test tool`,
|
|
parameters: {
|
|
type: "object",
|
|
properties: {},
|
|
additionalProperties: false,
|
|
},
|
|
execute: vi.fn(async () => ({
|
|
content: [{ type: "text" as const, text: `${name} done` }],
|
|
details: {},
|
|
})),
|
|
};
|
|
}
|
|
|
|
async function buildDynamicToolsForTest(
|
|
params: EmbeddedRunAttemptParams,
|
|
workspaceDir: string,
|
|
options: Partial<Parameters<typeof buildDynamicTools>[0]> = {},
|
|
) {
|
|
const sandboxSessionKey = params.sessionKey;
|
|
if (!sandboxSessionKey) {
|
|
throw new Error("createParams must provide a sessionKey for Codex dynamic tool tests.");
|
|
}
|
|
return buildDynamicTools({
|
|
params,
|
|
resolvedWorkspace: workspaceDir,
|
|
effectiveWorkspace: workspaceDir,
|
|
sandboxSessionKey,
|
|
sandbox: { enabled: false, backendId: "docker" } as never,
|
|
nativeToolSurfaceEnabled: true,
|
|
runAbortController: new AbortController(),
|
|
sessionAgentId: "main",
|
|
pluginConfig: {},
|
|
onYieldDetected: () => undefined,
|
|
...options,
|
|
});
|
|
}
|
|
|
|
describe("Codex app-server dynamic tool build", () => {
|
|
beforeEach(async () => {
|
|
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-tools-"));
|
|
});
|
|
|
|
afterEach(async () => {
|
|
resetOpenClawCodingToolsFactoryForTests();
|
|
vi.restoreAllMocks();
|
|
vi.unstubAllEnvs();
|
|
await fs.rm(tempDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it("filters Codex-native dynamic tools from app-server tool exposure", () => {
|
|
const tools = [
|
|
"read",
|
|
"write",
|
|
"edit",
|
|
"apply_patch",
|
|
"exec",
|
|
"process",
|
|
"update_plan",
|
|
"tool_call",
|
|
"tool_describe",
|
|
"tool_search",
|
|
"tool_search_code",
|
|
"web_search",
|
|
"message",
|
|
"heartbeat_respond",
|
|
"sessions_spawn",
|
|
].map((name) => ({ name }));
|
|
|
|
expect(filterCodexDynamicTools(tools, {}).map((tool) => tool.name)).toEqual([
|
|
"web_search",
|
|
"message",
|
|
"heartbeat_respond",
|
|
"sessions_spawn",
|
|
]);
|
|
});
|
|
|
|
it("applies additional Codex dynamic tool excludes without exposing Codex-native tools", () => {
|
|
const tools = ["read", "exec", "message", "custom_tool"].map((name) => ({ name }));
|
|
|
|
expect(
|
|
filterCodexDynamicTools(tools, {
|
|
codexDynamicToolsExclude: ["custom_tool"],
|
|
}).map((tool) => tool.name),
|
|
).toEqual(["message"]);
|
|
});
|
|
|
|
it("exposes app-server-owned tools directly for forced private QA Codex runtime", () => {
|
|
const tools = ["read", "write", "image_generate", "message"].map((name) => ({ name }));
|
|
const privateQaCodexEnv = {
|
|
OPENCLAW_BUILD_PRIVATE_QA: "1",
|
|
OPENCLAW_QA_FORCE_RUNTIME: "codex",
|
|
};
|
|
|
|
expect(filterCodexDynamicTools(tools, {}, privateQaCodexEnv).map((tool) => tool.name)).toEqual([
|
|
"read",
|
|
"write",
|
|
"image_generate",
|
|
"message",
|
|
]);
|
|
expect(resolveCodexDynamicToolsLoading({}, privateQaCodexEnv)).toBe("direct");
|
|
});
|
|
|
|
it("quarantines unreadable tool entries before Codex-specific filtering", async () => {
|
|
const messageTool = createRuntimeDynamicTool("message");
|
|
const sourceTools = new Proxy([messageTool] as RuntimeDynamicToolForTest[], {
|
|
get(target, property, receiver) {
|
|
if (property === "0") {
|
|
throw new Error("fuzzplugin tool entry getter exploded");
|
|
}
|
|
if (property === "1") {
|
|
return messageTool;
|
|
}
|
|
if (property === "length") {
|
|
return 2;
|
|
}
|
|
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);
|
|
params.disableTools = false;
|
|
params.runtimePlan = createCodexRuntimePlanFixture();
|
|
|
|
await expect(buildDynamicToolsForTest(params, workspaceDir)).resolves.toEqual([messageTool]);
|
|
});
|
|
|
|
it("limits Codex memory flush runs to managed read and write tools", async () => {
|
|
const factoryOptions: unknown[] = [];
|
|
setOpenClawCodingToolsFactoryForTests((options) => {
|
|
factoryOptions.push(options);
|
|
return [
|
|
createRuntimeDynamicTool("read"),
|
|
createRuntimeDynamicTool("write"),
|
|
createRuntimeDynamicTool("exec"),
|
|
createRuntimeDynamicTool("process"),
|
|
createRuntimeDynamicTool("apply_patch"),
|
|
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.trigger = "memory";
|
|
params.memoryFlushWritePath = "memory/2026-05-22.md";
|
|
const sandbox = { enabled: true, backendId: "docker" } as never;
|
|
|
|
const nativeToolSurfaceEnabled = shouldEnableCodexAppServerNativeToolSurface(params, sandbox);
|
|
const tools = await buildDynamicToolsForTest(params, workspaceDir, {
|
|
sandbox,
|
|
nativeToolSurfaceEnabled,
|
|
});
|
|
|
|
expect(nativeToolSurfaceEnabled).toBe(false);
|
|
expect(factoryOptions).toHaveLength(1);
|
|
expect(factoryOptions[0]).toMatchObject({
|
|
trigger: "memory",
|
|
memoryFlushWritePath: "memory/2026-05-22.md",
|
|
});
|
|
expect(tools.map((tool) => tool.name)).toEqual(["read", "write"]);
|
|
});
|
|
|
|
it("exposes OpenClaw sandbox shell tools under distinct names for non-Docker sandbox backends", async () => {
|
|
setOpenClawCodingToolsFactoryForTests(() => [
|
|
createRuntimeDynamicTool("read"),
|
|
createRuntimeDynamicTool("write"),
|
|
createRuntimeDynamicTool("edit"),
|
|
createRuntimeDynamicTool("apply_patch"),
|
|
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 tools = await buildDynamicToolsForTest(params, workspaceDir, {
|
|
sandbox: { enabled: true, backendId: "ssh" } as never,
|
|
nativeToolSurfaceEnabled: false,
|
|
});
|
|
|
|
expect(tools.map((tool) => tool.name)).toEqual(["message", "sandbox_exec", "sandbox_process"]);
|
|
expect(tools.find((tool) => tool.name === "sandbox_exec")?.description).toContain(
|
|
"configured sandbox backend",
|
|
);
|
|
expect(tools.find((tool) => tool.name === "sandbox_process")?.description).toContain(
|
|
"sandbox_exec sessions",
|
|
);
|
|
});
|
|
|
|
it("exposes Docker sandbox shell tools when OpenClaw sandboxing disables native Code Mode", async () => {
|
|
setOpenClawCodingToolsFactoryForTests(() => [
|
|
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;
|
|
const nativeToolSurfaceEnabled = shouldEnableCodexAppServerNativeToolSurface(params, sandbox);
|
|
|
|
const dockerTools = await buildDynamicToolsForTest(params, workspaceDir, {
|
|
sandbox,
|
|
nativeToolSurfaceEnabled,
|
|
});
|
|
|
|
expect(nativeToolSurfaceEnabled).toBe(false);
|
|
expect(dockerTools.map((tool) => tool.name)).toEqual([
|
|
"message",
|
|
"sandbox_exec",
|
|
"sandbox_process",
|
|
]);
|
|
});
|
|
|
|
it("keeps OpenClaw shell tools for node-targeted Codex app-server runs", async () => {
|
|
setOpenClawCodingToolsFactoryForTests(() => [
|
|
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.execOverrides = {
|
|
host: "node",
|
|
node: "mac-mini",
|
|
security: "full",
|
|
ask: "off",
|
|
};
|
|
|
|
const tools = await buildDynamicToolsForTest(params, workspaceDir, {
|
|
nativeToolSurfaceEnabled: false,
|
|
});
|
|
|
|
expect(tools.map((tool) => tool.name)).toEqual(["message", "exec", "process"]);
|
|
|
|
const runtimePolicySessionFile = path.join(tempDir, "runtime-policy-session.jsonl");
|
|
const runtimePolicyParams = createParams(runtimePolicySessionFile, workspaceDir);
|
|
runtimePolicyParams.disableTools = false;
|
|
runtimePolicyParams.runtimePlan = createCodexRuntimePlanFixture();
|
|
runtimePolicyParams.sessionKey = "agent:main:session-1";
|
|
runtimePolicyParams.sandboxSessionKey = "agent:policy:session-1";
|
|
runtimePolicyParams.config = {
|
|
agents: {
|
|
list: [
|
|
{ id: "main", tools: { exec: { host: "gateway" } } },
|
|
{ id: "policy", tools: { exec: { host: "node", node: "worker-1" } } },
|
|
],
|
|
},
|
|
} as never;
|
|
const runtimePolicyTools = await buildDynamicToolsForTest(runtimePolicyParams, workspaceDir, {
|
|
sandboxSessionKey: "agent:policy:session-1",
|
|
nativeToolSurfaceEnabled: false,
|
|
sessionAgentId: "policy",
|
|
});
|
|
|
|
expect(runtimePolicyTools.map((tool) => tool.name)).toEqual(["message", "exec", "process"]);
|
|
});
|
|
|
|
it("exposes Docker sandbox shell tools when native Code Mode cannot honor sandbox paths", async () => {
|
|
setOpenClawCodingToolsFactoryForTests(() => [
|
|
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 tools = await buildDynamicToolsForTest(params, workspaceDir, {
|
|
sandbox: {
|
|
enabled: true,
|
|
backendId: "docker",
|
|
docker: { binds: ["/tmp/openclaw-data:/data:rw"] },
|
|
} as never,
|
|
nativeToolSurfaceEnabled: false,
|
|
});
|
|
|
|
expect(tools.map((tool) => tool.name)).toEqual(["message", "sandbox_exec", "sandbox_process"]);
|
|
expect(tools.find((tool) => tool.name === "sandbox_exec")?.description).toContain(
|
|
"Docker container-path bind layout",
|
|
);
|
|
});
|
|
|
|
it("does not expose sandbox shell tools when sandbox routing is disabled", async () => {
|
|
setOpenClawCodingToolsFactoryForTests(() => [
|
|
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 disabledSandboxTools = await buildDynamicToolsForTest(params, workspaceDir, {
|
|
sandbox: { enabled: false, backendId: "ssh" } as never,
|
|
});
|
|
|
|
expect(disabledSandboxTools.map((tool) => tool.name)).toEqual(["message"]);
|
|
});
|
|
|
|
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);
|
|
params.disableTools = false;
|
|
params.runtimePlan = createCodexRuntimePlanFixture();
|
|
|
|
const tools = await buildDynamicToolsForTest(params, workspaceDir, {
|
|
sandbox: { enabled: true, backendId: "ssh" } as never,
|
|
});
|
|
|
|
expect(tools.map((tool) => tool.name)).toEqual(["message"]);
|
|
});
|
|
|
|
it("honors Codex dynamic tool excludes for sandbox shell exposure", async () => {
|
|
setOpenClawCodingToolsFactoryForTests(() => [
|
|
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();
|
|
|
|
for (const excludedToolName of ["sandbox_exec", "process"]) {
|
|
const tools = await buildDynamicToolsForTest(params, workspaceDir, {
|
|
sandbox: { enabled: true, backendId: "ssh" } as never,
|
|
pluginConfig: { codexDynamicToolsExclude: [excludedToolName] },
|
|
});
|
|
|
|
expect(tools.map((tool) => tool.name)).toEqual(["message"]);
|
|
}
|
|
});
|
|
|
|
it("points yielded sandbox_exec follow-up guidance at sandbox_process", async () => {
|
|
const execTool = createRuntimeDynamicTool("exec");
|
|
vi.mocked(execTool.execute).mockResolvedValueOnce({
|
|
content: [
|
|
{
|
|
type: "text",
|
|
text: "Command still running (session exec-1, pid 123). Use process (list/poll/log/write/send-keys/submit/paste/kill/clear/remove) for follow-up.",
|
|
},
|
|
],
|
|
details: { status: "running" },
|
|
});
|
|
const processTool = createRuntimeDynamicTool("process");
|
|
const workspaceDir = path.join(tempDir, "workspace");
|
|
const tools = addSandboxShellDynamicToolsIfAvailable([], [execTool, processTool], {
|
|
params: createParams(path.join(tempDir, "session.jsonl"), workspaceDir),
|
|
sandbox: { enabled: true, backendId: "ssh" },
|
|
nativeToolSurfaceEnabled: false,
|
|
sessionAgentId: "main",
|
|
pluginConfig: {},
|
|
} as never);
|
|
|
|
const sandboxExec = tools.find((tool) => tool.name === "sandbox_exec");
|
|
const result = await sandboxExec?.execute("call-1", {}, undefined);
|
|
|
|
expect(result?.content).toEqual([
|
|
{
|
|
type: "text",
|
|
text: "Command still running (session exec-1, pid 123). Use sandbox_process (list/poll/log/write/send-keys/submit/paste/kill/clear/remove) for follow-up.",
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("passes auth profiles into Codex dynamic tool construction", async () => {
|
|
const sessionFile = path.join(tempDir, "session.jsonl");
|
|
const workspaceDir = path.join(tempDir, "workspace");
|
|
const params = createParams(sessionFile, workspaceDir);
|
|
const authProfileStore = {
|
|
version: 1,
|
|
profiles: {
|
|
"openai:api-key-backup": {
|
|
provider: "openai",
|
|
type: "api_key",
|
|
key: "not-a-real-key",
|
|
},
|
|
},
|
|
} satisfies EmbeddedRunAttemptParams["authProfileStore"];
|
|
params.disableTools = false;
|
|
params.authProfileStore = authProfileStore;
|
|
params.runtimePlan = createCodexRuntimePlanFixture();
|
|
const factoryOptions: unknown[] = [];
|
|
setOpenClawCodingToolsFactoryForTests((options) => {
|
|
factoryOptions.push(options);
|
|
return [];
|
|
});
|
|
|
|
await buildDynamicToolsForTest(params, workspaceDir, { sandbox: null as never });
|
|
|
|
expect(factoryOptions).toHaveLength(1);
|
|
expect((factoryOptions[0] as { authProfileStore?: unknown }).authProfileStore).toBe(
|
|
authProfileStore,
|
|
);
|
|
});
|
|
|
|
it("passes runtime config into Codex exec dynamic tool construction", async () => {
|
|
const sessionFile = path.join(tempDir, "session.jsonl");
|
|
const workspaceDir = path.join(tempDir, "workspace");
|
|
const params = createParams(sessionFile, workspaceDir);
|
|
const runtimeConfig = {
|
|
tools: {
|
|
exec: {
|
|
mode: "auto",
|
|
reviewer: {
|
|
timeoutMs: 1234,
|
|
},
|
|
},
|
|
},
|
|
} as EmbeddedRunAttemptParams["config"];
|
|
params.disableTools = false;
|
|
params.config = runtimeConfig;
|
|
params.runtimePlan = createCodexRuntimePlanFixture();
|
|
const factoryOptions: unknown[] = [];
|
|
setOpenClawCodingToolsFactoryForTests((options) => {
|
|
factoryOptions.push(options);
|
|
return [];
|
|
});
|
|
|
|
await buildDynamicToolsForTest(params, workspaceDir, { sandbox: null as never });
|
|
|
|
const toolOptions = factoryOptions[0] as {
|
|
config?: unknown;
|
|
exec?: { config?: unknown; mode?: unknown };
|
|
};
|
|
expect(factoryOptions).toHaveLength(1);
|
|
expect(toolOptions.config).toBe(runtimeConfig);
|
|
expect(toolOptions.exec?.config).toBe(runtimeConfig);
|
|
expect(toolOptions.exec?.mode).toBeUndefined();
|
|
});
|
|
|
|
it("uses the tool auth profile store for Codex dynamic tool construction", async () => {
|
|
const sessionFile = path.join(tempDir, "session.jsonl");
|
|
const workspaceDir = path.join(tempDir, "workspace");
|
|
const params = createParams(sessionFile, workspaceDir);
|
|
const transportAuthProfileStore = {
|
|
version: 1,
|
|
profiles: {
|
|
"openai:work": {
|
|
provider: "openai",
|
|
type: "oauth",
|
|
access: "transport-token",
|
|
refresh: "transport-refresh",
|
|
expires: Date.now() + 60_000,
|
|
},
|
|
},
|
|
} satisfies EmbeddedRunAttemptParams["authProfileStore"];
|
|
const toolAuthProfileStore = {
|
|
version: 1,
|
|
profiles: {
|
|
"openai:work": {
|
|
provider: "openai",
|
|
type: "oauth",
|
|
access: "transport-token",
|
|
refresh: "transport-refresh",
|
|
expires: Date.now() + 60_000,
|
|
},
|
|
"xai:work": {
|
|
provider: "xai",
|
|
type: "oauth",
|
|
access: "xai-token",
|
|
refresh: "xai-refresh",
|
|
expires: Date.now() + 60_000,
|
|
},
|
|
},
|
|
} satisfies EmbeddedRunAttemptParams["authProfileStore"];
|
|
params.disableTools = false;
|
|
params.authProfileStore = transportAuthProfileStore;
|
|
params.toolAuthProfileStore = toolAuthProfileStore;
|
|
params.runtimePlan = createCodexRuntimePlanFixture();
|
|
const factoryOptions: unknown[] = [];
|
|
setOpenClawCodingToolsFactoryForTests((options) => {
|
|
factoryOptions.push(options);
|
|
return [];
|
|
});
|
|
|
|
await buildDynamicToolsForTest(params, workspaceDir, { sandbox: null as never });
|
|
|
|
expect(factoryOptions).toHaveLength(1);
|
|
expect((factoryOptions[0] as { authProfileStore?: unknown }).authProfileStore).toBe(
|
|
toolAuthProfileStore,
|
|
);
|
|
});
|
|
|
|
it("keeps canonical OpenAI Codex runs on OpenAI dynamic tool policy", async () => {
|
|
const sessionFile = path.join(tempDir, "session.jsonl");
|
|
const workspaceDir = path.join(tempDir, "workspace");
|
|
const params = createParams(sessionFile, workspaceDir);
|
|
params.disableTools = false;
|
|
params.provider = "openai";
|
|
params.modelId = "gpt-5.5";
|
|
params.model = {
|
|
...createCodexTestModel("openai"),
|
|
id: "gpt-5.5",
|
|
name: "gpt-5.5",
|
|
api: "openai-responses",
|
|
} as EmbeddedRunAttemptParams["model"];
|
|
params.runtimePlan = {
|
|
...createCodexRuntimePlanFixture(),
|
|
observability: {
|
|
resolvedRef: "openai/gpt-5.5",
|
|
provider: "openai",
|
|
modelId: "gpt-5.5",
|
|
harnessId: "codex",
|
|
},
|
|
};
|
|
const factoryOptions: unknown[] = [];
|
|
setOpenClawCodingToolsFactoryForTests((options) => {
|
|
factoryOptions.push(options);
|
|
return [];
|
|
});
|
|
|
|
await buildDynamicToolsForTest(params, workspaceDir, { sandbox: null as never });
|
|
|
|
expect(factoryOptions).toHaveLength(1);
|
|
expect((factoryOptions[0] as { modelProvider?: unknown }).modelProvider).toBe("openai");
|
|
expect((factoryOptions[0] as { modelApi?: unknown }).modelApi).toBe("openai-responses");
|
|
});
|
|
|
|
it("enables gateway subagent binding for forced private QA Codex runs", async () => {
|
|
vi.stubEnv("OPENCLAW_BUILD_PRIVATE_QA", "1");
|
|
vi.stubEnv("OPENCLAW_QA_FORCE_RUNTIME", "codex");
|
|
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 factoryOptions: unknown[] = [];
|
|
setOpenClawCodingToolsFactoryForTests((options) => {
|
|
factoryOptions.push(options);
|
|
return [createRuntimeDynamicTool("sessions_spawn")];
|
|
});
|
|
|
|
const tools = await buildDynamicToolsForTest(params, workspaceDir, { sandbox: null as never });
|
|
|
|
expect(factoryOptions).toHaveLength(1);
|
|
const factoryOption = factoryOptions[0] as { allowGatewaySubagentBinding?: unknown };
|
|
expect(factoryOption.allowGatewaySubagentBinding).toBe(true);
|
|
expect(tools.map((tool) => tool.name)).toEqual(["sessions_spawn"]);
|
|
});
|
|
|
|
it("normalizes Codex dynamic toolsAllow entries before filtering", () => {
|
|
const tools = ["exec", "sandbox_exec", "sandbox_process", "apply_patch", "read", "message"].map(
|
|
(name) => ({ name }),
|
|
);
|
|
|
|
expect(
|
|
filterCodexDynamicToolsForAllowlist(tools, [" BASH ", "apply-patch", "READ"]).map(
|
|
(tool) => tool.name,
|
|
),
|
|
).toEqual(["exec", "sandbox_exec", "sandbox_process", "apply_patch", "read"]);
|
|
});
|
|
|
|
it("treats an explicit empty Codex dynamic toolsAllow as no tools", () => {
|
|
const tools = ["message", "web_search"].map((name) => ({ name }));
|
|
|
|
expect(filterCodexDynamicToolsForAllowlist(tools, [])).toEqual([]);
|
|
});
|
|
|
|
it("treats wildcard Codex dynamic toolsAllow as unrestricted", () => {
|
|
const tools = ["message", "web_search"].map((name) => ({ name }));
|
|
|
|
expect(filterCodexDynamicToolsForAllowlist(tools, [" * "])).toEqual(tools);
|
|
expect(hasWildcardCodexToolsAllow([" * "])).toBe(true);
|
|
});
|
|
|
|
it("disables Codex native tool surfaces for restricted runtime allowlists", () => {
|
|
const workspaceDir = path.join(tempDir, "workspace");
|
|
const params = createParams(path.join(tempDir, "session.jsonl"), workspaceDir);
|
|
params.disableTools = false;
|
|
|
|
expect(shouldEnableCodexAppServerNativeToolSurface(params)).toBe(true);
|
|
|
|
params.toolsAllow = ["*"];
|
|
expect(shouldEnableCodexAppServerNativeToolSurface(params)).toBe(true);
|
|
|
|
params.toolsAllow = [];
|
|
expect(shouldEnableCodexAppServerNativeToolSurface(params)).toBe(false);
|
|
|
|
params.toolsAllow = ["message"];
|
|
expect(shouldEnableCodexAppServerNativeToolSurface(params)).toBe(false);
|
|
});
|
|
|
|
it("disables Codex native tool surfaces when the effective exec target is node", () => {
|
|
const workspaceDir = path.join(tempDir, "workspace");
|
|
const sessionParams = createParams(path.join(tempDir, "session.jsonl"), workspaceDir);
|
|
sessionParams.disableTools = false;
|
|
sessionParams.execOverrides = {
|
|
host: "node",
|
|
node: "mac-mini",
|
|
security: "full",
|
|
ask: "off",
|
|
};
|
|
|
|
expect(shouldEnableCodexAppServerNativeToolSurface(sessionParams)).toBe(false);
|
|
|
|
sessionParams.toolsAllow = ["*"];
|
|
expect(shouldEnableCodexAppServerNativeToolSurface(sessionParams)).toBe(false);
|
|
|
|
const globalParams = createParams(path.join(tempDir, "global-session.jsonl"), workspaceDir);
|
|
globalParams.disableTools = false;
|
|
globalParams.config = { tools: { exec: { host: "node" } } } as never;
|
|
|
|
expect(shouldEnableCodexAppServerNativeToolSurface(globalParams)).toBe(false);
|
|
|
|
const autoOverrideParams = createParams(
|
|
path.join(tempDir, "auto-override-session.jsonl"),
|
|
workspaceDir,
|
|
);
|
|
autoOverrideParams.disableTools = false;
|
|
autoOverrideParams.config = { tools: { exec: { host: "node" } } } as never;
|
|
autoOverrideParams.execOverrides = { host: "auto" };
|
|
|
|
expect(shouldEnableCodexAppServerNativeToolSurface(autoOverrideParams)).toBe(true);
|
|
|
|
const agentParams = createParams(path.join(tempDir, "agent-session.jsonl"), workspaceDir);
|
|
agentParams.disableTools = false;
|
|
agentParams.config = {
|
|
agents: {
|
|
list: [{ id: "main", tools: { exec: { host: "node" } } }],
|
|
},
|
|
} as never;
|
|
|
|
expect(
|
|
shouldEnableCodexAppServerNativeToolSurface(agentParams, undefined, {
|
|
agentId: "main",
|
|
}),
|
|
).toBe(false);
|
|
|
|
const runtimePolicyParams = createParams(
|
|
path.join(tempDir, "runtime-policy-session.jsonl"),
|
|
workspaceDir,
|
|
);
|
|
runtimePolicyParams.disableTools = false;
|
|
runtimePolicyParams.sessionKey = "agent:main:session-1";
|
|
runtimePolicyParams.sandboxSessionKey = "agent:policy:session-1";
|
|
runtimePolicyParams.config = {
|
|
agents: {
|
|
list: [
|
|
{ id: "main", tools: { exec: { host: "gateway" } } },
|
|
{ id: "policy", tools: { exec: { host: "node", node: "worker-1" } } },
|
|
],
|
|
},
|
|
} as never;
|
|
|
|
expect(shouldEnableCodexAppServerNativeToolSurface(runtimePolicyParams)).toBe(false);
|
|
});
|
|
|
|
it("disables Codex native tool surfaces whenever an OpenClaw sandbox is active", () => {
|
|
const workspaceDir = path.join(tempDir, "workspace");
|
|
const params = createParams(path.join(tempDir, "session.jsonl"), workspaceDir);
|
|
params.disableTools = false;
|
|
|
|
expect(
|
|
shouldEnableCodexAppServerNativeToolSurface(params, {
|
|
enabled: true,
|
|
backendId: "docker",
|
|
docker: { binds: [] },
|
|
} as never),
|
|
).toBe(false);
|
|
|
|
expect(
|
|
shouldEnableCodexAppServerNativeToolSurface(params, {
|
|
enabled: true,
|
|
backendId: "docker",
|
|
docker: { binds: ["/tmp/openclaw-data:/data:rw"] },
|
|
} as never),
|
|
).toBe(false);
|
|
|
|
expect(
|
|
shouldEnableCodexAppServerNativeToolSurface(params, {
|
|
enabled: true,
|
|
backendId: "docker",
|
|
docker: { binds: ["/tmp/openclaw-data:/tmp/openclaw-data:rw"] },
|
|
} as never),
|
|
).toBe(false);
|
|
|
|
expect(
|
|
shouldEnableCodexAppServerNativeToolSurface(params, {
|
|
enabled: true,
|
|
backendId: "docker",
|
|
docker: {
|
|
binds: [
|
|
"/tmp/openclaw-data:/tmp/openclaw-data:rw",
|
|
"/tmp/openclaw-data/secrets:/tmp/openclaw-data/secrets:ro",
|
|
],
|
|
},
|
|
} as never),
|
|
).toBe(false);
|
|
|
|
expect(
|
|
shouldEnableCodexAppServerNativeToolSurface(params, {
|
|
enabled: true,
|
|
backendId: "ssh",
|
|
} as never),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("keeps sandbox exec-server native surfaces behind sandbox tool policy", () => {
|
|
const workspaceDir = path.join(tempDir, "workspace");
|
|
const params = createParams(path.join(tempDir, "session.jsonl"), workspaceDir);
|
|
params.disableTools = false;
|
|
const sandbox = {
|
|
enabled: true,
|
|
backendId: "docker",
|
|
backend: {},
|
|
tools: {
|
|
allow: ["exec", "process", "read", "write", "edit", "apply_patch"],
|
|
deny: [],
|
|
},
|
|
};
|
|
|
|
expect(
|
|
shouldEnableCodexAppServerNativeToolSurface(params, sandbox as never, {
|
|
sandboxExecServerEnabled: true,
|
|
}),
|
|
).toBe(true);
|
|
|
|
expect(
|
|
shouldEnableCodexAppServerNativeToolSurface(
|
|
params,
|
|
{
|
|
...sandbox,
|
|
tools: { allow: ["exec"], deny: [] },
|
|
} as never,
|
|
{ sandboxExecServerEnabled: true },
|
|
),
|
|
).toBe(false);
|
|
|
|
expect(
|
|
shouldEnableCodexAppServerNativeToolSurface(
|
|
params,
|
|
{
|
|
...sandbox,
|
|
tools: { allow: [], deny: ["write"] },
|
|
} as never,
|
|
{ sandboxExecServerEnabled: true },
|
|
),
|
|
).toBe(false);
|
|
|
|
params.toolsAllow = ["message"];
|
|
expect(
|
|
shouldEnableCodexAppServerNativeToolSurface(params, sandbox as never, {
|
|
sandboxExecServerEnabled: true,
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("forces the message dynamic tool for message-tool-only source replies", () => {
|
|
const workspaceDir = path.join(tempDir, "workspace");
|
|
const params = createParams(path.join(tempDir, "session.jsonl"), workspaceDir);
|
|
params.sourceReplyDeliveryMode = "message_tool_only";
|
|
|
|
expect(shouldForceMessageTool(params)).toBe(true);
|
|
expect(includeForcedCodexDynamicToolAllow([], params)).toEqual(["message"]);
|
|
|
|
params.disableMessageTool = true;
|
|
expect(shouldForceMessageTool(params)).toBe(false);
|
|
|
|
params.disableMessageTool = false;
|
|
params.sourceReplyDeliveryMode = "automatic";
|
|
expect(shouldForceMessageTool(params)).toBe(false);
|
|
});
|
|
|
|
it("passes the live run session key to Codex dynamic tools when sandbox policy uses another key", () => {
|
|
const workspaceDir = path.join(tempDir, "workspace");
|
|
const params = createParams(path.join(tempDir, "session.jsonl"), workspaceDir);
|
|
params.sessionKey = "agent:main:main";
|
|
|
|
expect(
|
|
resolveOpenClawCodingToolsSessionKeys(params, "agent:main:telegram:default:direct:1234"),
|
|
).toEqual({
|
|
sessionKey: "agent:main:telegram:default:direct:1234",
|
|
runSessionKey: "agent:main:main",
|
|
});
|
|
|
|
expect(resolveOpenClawCodingToolsSessionKeys(params, "agent:main:main")).toEqual({
|
|
sessionKey: "agent:main:main",
|
|
runSessionKey: undefined,
|
|
});
|
|
});
|
|
});
|