


























@@ -39,7 +39,10 @@ import * as authBridge from "./auth-bridge.js";
3939import { resolveCodexAppServerEnvApiKeyCacheKey } from "./auth-bridge.js";
4040import type { CodexAppServerClientFactory } from "./client-factory.js";
4141import { readCodexPluginConfig, resolveCodexAppServerRuntimeOptions } from "./config.js";
42-import { CODEX_OPENCLAW_DYNAMIC_TOOL_NAMESPACE } from "./dynamic-tools.js";
42+import {
43+CODEX_OPENCLAW_DYNAMIC_TOOL_NAMESPACE,
44+createCodexDynamicToolBridge,
45+} from "./dynamic-tools.js";
4346import * as elicitationBridge from "./elicitation-bridge.js";
4447import {
4548buildCodexPluginAppCacheKey,
@@ -463,9 +466,14 @@ function createNamedDynamicTool(
463466};
464467}
465468466-function createRuntimeDynamicTool(name: string) {
469+type RuntimeDynamicToolForTest = Parameters<
470+typeof createCodexDynamicToolBridge
471+>[0]["tools"][number];
472+473+function createRuntimeDynamicTool(name: string): RuntimeDynamicToolForTest {
467474return {
468475 name,
476+label: name,
469477description: `${name} test tool`,
470478parameters: {
471479type: "object",
@@ -476,7 +484,7 @@ function createRuntimeDynamicTool(name: string) {
476484content: [{ type: "text" as const, text: `${name} done` }],
477485details: {},
478486})),
479-} as never;
487+};
480488}
481489482490function createPluginAppConfigPatch() {
@@ -696,6 +704,177 @@ describe("runCodexAppServerAttempt", () => {
696704expect(testing.resolveCodexDynamicToolsLoading({}, privateQaCodexEnv)).toBe("direct");
697705});
698706707+it("exposes OpenClaw sandbox shell tools under distinct names for non-Docker sandbox backends", async () => {
708+testing.setOpenClawCodingToolsFactoryForTests(() => [
709+createRuntimeDynamicTool("read"),
710+createRuntimeDynamicTool("write"),
711+createRuntimeDynamicTool("edit"),
712+createRuntimeDynamicTool("apply_patch"),
713+createRuntimeDynamicTool("exec"),
714+createRuntimeDynamicTool("process"),
715+createRuntimeDynamicTool("message"),
716+]);
717+const sessionFile = path.join(tempDir, "session.jsonl");
718+const workspaceDir = path.join(tempDir, "workspace");
719+const params = createParams(sessionFile, workspaceDir);
720+params.disableTools = false;
721+const sandboxSessionKey = params.sessionKey;
722+if (!sandboxSessionKey) {
723+throw new Error("createParams must provide a sessionKey for Codex dynamic tool tests.");
724+}
725+726+const tools = await testing.buildDynamicTools({
727+ params,
728+resolvedWorkspace: workspaceDir,
729+effectiveWorkspace: workspaceDir,
730+ sandboxSessionKey,
731+sandbox: { enabled: true, backendId: "ssh" } as never,
732+runAbortController: new AbortController(),
733+sessionAgentId: "main",
734+pluginConfig: {},
735+onYieldDetected: () => undefined,
736+});
737+738+expect(tools.map((tool) => tool.name)).toEqual(["message", "sandbox_exec", "sandbox_process"]);
739+expect(tools.find((tool) => tool.name === "sandbox_exec")?.description).toContain(
740+"configured sandbox backend",
741+);
742+expect(tools.find((tool) => tool.name === "sandbox_process")?.description).toContain(
743+"sandbox_exec sessions",
744+);
745+});
746+747+it("does not expose sandbox shell tools unless non-Docker sandbox routing is available", async () => {
748+testing.setOpenClawCodingToolsFactoryForTests(() => [
749+createRuntimeDynamicTool("exec"),
750+createRuntimeDynamicTool("process"),
751+createRuntimeDynamicTool("message"),
752+]);
753+const sessionFile = path.join(tempDir, "session.jsonl");
754+const workspaceDir = path.join(tempDir, "workspace");
755+const params = createParams(sessionFile, workspaceDir);
756+params.disableTools = false;
757+const sandboxSessionKey = params.sessionKey;
758+if (!sandboxSessionKey) {
759+throw new Error("createParams must provide a sessionKey for Codex dynamic tool tests.");
760+}
761+762+const dockerTools = await testing.buildDynamicTools({
763+ params,
764+resolvedWorkspace: workspaceDir,
765+effectiveWorkspace: workspaceDir,
766+ sandboxSessionKey,
767+sandbox: { enabled: true, backendId: "docker" } as never,
768+runAbortController: new AbortController(),
769+sessionAgentId: "main",
770+pluginConfig: {},
771+onYieldDetected: () => undefined,
772+});
773+const disabledSandboxTools = await testing.buildDynamicTools({
774+ params,
775+resolvedWorkspace: workspaceDir,
776+effectiveWorkspace: workspaceDir,
777+ sandboxSessionKey,
778+sandbox: { enabled: false, backendId: "ssh" } as never,
779+runAbortController: new AbortController(),
780+sessionAgentId: "main",
781+pluginConfig: {},
782+onYieldDetected: () => undefined,
783+});
784+785+expect(dockerTools.map((tool) => tool.name)).toEqual(["message"]);
786+expect(disabledSandboxTools.map((tool) => tool.name)).toEqual(["message"]);
787+});
788+789+it("does not expose sandbox_exec without a matching process follow-up tool", async () => {
790+testing.setOpenClawCodingToolsFactoryForTests(() => [
791+createRuntimeDynamicTool("exec"),
792+createRuntimeDynamicTool("message"),
793+]);
794+const sessionFile = path.join(tempDir, "session.jsonl");
795+const workspaceDir = path.join(tempDir, "workspace");
796+const params = createParams(sessionFile, workspaceDir);
797+params.disableTools = false;
798+const sandboxSessionKey = params.sessionKey;
799+if (!sandboxSessionKey) {
800+throw new Error("createParams must provide a sessionKey for Codex dynamic tool tests.");
801+}
802+803+const tools = await testing.buildDynamicTools({
804+ params,
805+resolvedWorkspace: workspaceDir,
806+effectiveWorkspace: workspaceDir,
807+ sandboxSessionKey,
808+sandbox: { enabled: true, backendId: "ssh" } as never,
809+runAbortController: new AbortController(),
810+sessionAgentId: "main",
811+pluginConfig: {},
812+onYieldDetected: () => undefined,
813+});
814+815+expect(tools.map((tool) => tool.name)).toEqual(["message"]);
816+});
817+818+it("honors Codex dynamic tool excludes for sandbox shell exposure", async () => {
819+testing.setOpenClawCodingToolsFactoryForTests(() => [
820+createRuntimeDynamicTool("exec"),
821+createRuntimeDynamicTool("process"),
822+createRuntimeDynamicTool("message"),
823+]);
824+const sessionFile = path.join(tempDir, "session.jsonl");
825+const workspaceDir = path.join(tempDir, "workspace");
826+const params = createParams(sessionFile, workspaceDir);
827+params.disableTools = false;
828+const sandboxSessionKey = params.sessionKey;
829+if (!sandboxSessionKey) {
830+throw new Error("createParams must provide a sessionKey for Codex dynamic tool tests.");
831+}
832+833+for (const excludedToolName of ["sandbox_exec", "process"]) {
834+const tools = await testing.buildDynamicTools({
835+ params,
836+resolvedWorkspace: workspaceDir,
837+effectiveWorkspace: workspaceDir,
838+ sandboxSessionKey,
839+sandbox: { enabled: true, backendId: "ssh" } as never,
840+runAbortController: new AbortController(),
841+sessionAgentId: "main",
842+pluginConfig: { codexDynamicToolsExclude: [excludedToolName] },
843+onYieldDetected: () => undefined,
844+});
845+846+expect(tools.map((tool) => tool.name)).toEqual(["message"]);
847+}
848+});
849+850+it("points yielded sandbox_exec follow-up guidance at sandbox_process", async () => {
851+const execTool = createRuntimeDynamicTool("exec");
852+vi.mocked(execTool.execute).mockResolvedValueOnce({
853+content: [
854+{
855+type: "text",
856+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.",
857+},
858+],
859+details: { status: "running" },
860+});
861+const processTool = createRuntimeDynamicTool("process");
862+const tools = testing.addSandboxShellDynamicToolsIfAvailable([], [execTool, processTool], {
863+sandbox: { enabled: true, backendId: "ssh" },
864+pluginConfig: {},
865+} as never);
866+867+const sandboxExec = tools.find((tool) => tool.name === "sandbox_exec");
868+const result = await sandboxExec?.execute("call-1", {}, undefined);
869+870+expect(result?.content).toEqual([
871+{
872+type: "text",
873+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.",
874+},
875+]);
876+});
877+699878it("starts Codex threads without duplicate OpenClaw workspace tools by default", async () => {
700879const sessionFile = path.join(tempDir, "session.jsonl");
701880const workspaceDir = path.join(tempDir, "workspace");
@@ -980,13 +1159,15 @@ describe("runCodexAppServerAttempt", () => {
9801159});
98111609821161it("normalizes Codex dynamic toolsAllow entries before filtering", () => {
983-const tools = ["exec", "apply_patch", "read", "message"].map((name) => ({ name }));
1162+const tools = ["exec", "sandbox_exec", "sandbox_process", "apply_patch", "read", "message"].map(
1163+(name) => ({ name }),
1164+);
98411659851166expect(
9861167testing
9871168.filterCodexDynamicToolsForAllowlist(tools, [" BASH ", "apply-patch", "READ"])
9881169.map((tool) => tool.name),
989-).toEqual(["exec", "apply_patch", "read"]);
1170+).toEqual(["exec", "sandbox_exec", "sandbox_process", "apply_patch", "read"]);
9901171});
99111729921173it("treats an explicit empty Codex dynamic toolsAllow as no tools", () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。