import { beforeEach, describe, expect, it, vi } from "vitest"; const sharedClientMocks = vi.hoisted(() => ({ createIsolatedCodexAppServerClient: vi.fn(), getSharedCodexAppServerClient: vi.fn(), })); vi.mock("./shared-client.js", () => ({ ...sharedClientMocks, getLeasedSharedCodexAppServerClient: sharedClientMocks.getSharedCodexAppServerClient, releaseLeasedSharedCodexAppServerClient: vi.fn(), })); const { requestCodexAppServerJson } = await import("./request.js"); describe("requestCodexAppServerJson sandbox guard", () => { beforeEach(() => { sharedClientMocks.createIsolatedCodexAppServerClient.mockReset(); sharedClientMocks.getSharedCodexAppServerClient.mockReset(); }); it("fails closed before raw app-server bypass methods in sandboxed sessions", async () => { await expect( requestCodexAppServerJson({ method: "command/exec", requestParams: { command: ["sh", "-lc", "id"] }, config: { agents: { defaults: { sandbox: { mode: "all" } } } }, sessionKey: "sandboxed-session", }), ).rejects.toThrow( "Codex-native app-server method `command/exec` is unavailable because OpenClaw sandboxing is active for this session.", ); expect(sharedClientMocks.getSharedCodexAppServerClient).not.toHaveBeenCalled(); }); it("fails closed before raw app-server bypass methods when exec host=node is active", async () => { for (const method of ["command/exec", "process/spawn"]) { await expect( requestCodexAppServerJson({ method, requestParams: { command: ["sh", "-lc", "id"] }, config: { tools: { exec: { host: "node", node: "worker-1" } } }, sessionKey: "node-session", }), ).rejects.toThrow( `Codex-native app-server method \`${method}\` is unavailable because OpenClaw exec host=node is active for this session.`, ); } expect(sharedClientMocks.getSharedCodexAppServerClient).not.toHaveBeenCalled(); }); it("allows metadata methods in sandboxed sessions", async () => { const request = vi.fn(async () => ({ ok: true })); sharedClientMocks.getSharedCodexAppServerClient.mockResolvedValue({ request }); await expect( requestCodexAppServerJson({ method: "thread/list", requestParams: { limit: 10 }, config: { agents: { defaults: { sandbox: { mode: "all" } } } }, sessionKey: "sandboxed-session", }), ).resolves.toEqual({ ok: true }); expect(request).toHaveBeenCalledWith("thread/list", { limit: 10 }, { timeoutMs: 60_000 }); }); it("fails closed for config-level exec host=node even without a session key", async () => { await expect( requestCodexAppServerJson({ method: "command/exec", requestParams: { command: ["sh", "-lc", "id"] }, config: { tools: { exec: { host: "node", node: "worker-1" } } }, }), ).rejects.toThrow( "Codex-native app-server method `command/exec` is unavailable because OpenClaw exec host=node is active for this session.", ); expect(sharedClientMocks.getSharedCodexAppServerClient).not.toHaveBeenCalled(); }); it("fails closed for MCP reload when config-level exec host=node is active", async () => { await expect( requestCodexAppServerJson({ method: "config/mcpServer/reload", requestParams: {}, config: { tools: { exec: { host: "node", node: "worker-1" } } }, }), ).rejects.toThrow( "Codex-native app-server method `config/mcpServer/reload` is unavailable because OpenClaw exec host=node is active for this session.", ); expect(sharedClientMocks.getSharedCodexAppServerClient).not.toHaveBeenCalled(); }); it("allows metadata methods when exec host=node is active", async () => { const request = vi.fn(async () => ({ ok: true })); sharedClientMocks.getSharedCodexAppServerClient.mockResolvedValue({ request }); await expect( requestCodexAppServerJson({ method: "thread/list", requestParams: { limit: 10 }, config: { tools: { exec: { host: "node", node: "worker-1" } } }, sessionKey: "node-session", }), ).resolves.toEqual({ ok: true }); expect(request).toHaveBeenCalledWith("thread/list", { limit: 10 }, { timeoutMs: 60_000 }); }); it("allows sandbox-pinned thread starts in sandboxed sessions", async () => { const request = vi.fn(async () => ({ thread: { id: "thread-1" }, model: "gpt-5.5" })); sharedClientMocks.getSharedCodexAppServerClient.mockResolvedValue({ request }); const params = { cwd: "/workspace", environments: [{ environmentId: "openclaw-sandbox-abc123", cwd: "/workspace" }], }; await expect( requestCodexAppServerJson({ method: "thread/start", requestParams: params, config: { agents: { defaults: { sandbox: { mode: "all" } } } }, sessionKey: "sandboxed-session", }), ).resolves.toEqual({ thread: { id: "thread-1" }, model: "gpt-5.5" }); expect(request).toHaveBeenCalledWith("thread/start", params, { timeoutMs: 60_000 }); }); it("blocks thread starts with sandbox environments when exec host=node is active", async () => { const params = { cwd: "/workspace", environments: [{ environmentId: "openclaw-sandbox-abc123", cwd: "/workspace" }], }; await expect( requestCodexAppServerJson({ method: "thread/start", requestParams: params, config: { agents: { defaults: { sandbox: { mode: "all" } } }, tools: { exec: { host: "node", node: "worker-1" } }, }, sessionKey: "node-session", }), ).rejects.toThrow( "Codex-native app-server method `thread/start` is unavailable because OpenClaw exec host=node is active for this session.", ); expect(sharedClientMocks.getSharedCodexAppServerClient).not.toHaveBeenCalled(); }); });