import { afterEach, describe, expect, it } from "bun:test"; import express from "express"; import { type Server } from "node:http"; import { mkdtemp, readFile, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { config } from "../../src/config.js"; import { createConversationWorkspace } from "../../src/runtime/conversationWorkspace.js"; import { removeRuntimeSessionContext, setRuntimeSessionContext } from "../../src/runtime/sessionContext.js"; import { registerSandboxToolBridge } from "../../src/routes/sandboxToolBridge.js"; let server: Server | undefined; let root: string | undefined; const previousRoot = config.CONVERSATION_WORKSPACE_ROOT; const session = "sandbox-bridge-test"; afterEach(async () => { if (server) await new Promise((resolve) => server!.close(() => resolve())); if (root) await rm(root, { recursive: true, force: true }); config.CONVERSATION_WORKSPACE_ROOT = previousRoot; removeRuntimeSessionContext(session); }); describe("sandbox execution bridge", () => { it("executes only after approval and cannot override the owning workspace", async () => { root = await mkdtemp(join(tmpdir(), "sandbox-bridge-test-")); config.CONVERSATION_WORKSPACE_ROOT = root; const directory = await createConversationWorkspace(root); setRuntimeSessionContext({ sessionId: session, clientSessionId: session, actorKey: "owner", projectKey: "project", traceId: "test" }); let approve!: () => void; let approved = false; let ready!: () => void; const requested = new Promise((resolve) => { ready = resolve; }); const app = express(); app.use(express.json()); registerSandboxToolBridge(app, { internalToken: "TEST_ONLY", runtime: { ensureSessionWorkspace: async (id) => { expect(id).toBe(session); return directory; }, approveSandboxShell: async (input) => { if (approved) return; expect(input.command).toContain("approved.txt"); ready(); await new Promise((resolve) => { approve = resolve; }); approved = true; }, } }); server = app.listen(0, "127.0.0.1"); await new Promise((resolve) => server!.once("listening", resolve)); const url = `http://127.0.0.1:${(server.address() as { port: number }).port}/internal/tools/sandbox-shell`; const body = { session_id: session, call_id: "call-test", message_id: "message-test", command: "printf approved > approved.txt; cat approved.txt" }; const post = (input: unknown, headers: Record = {}) => fetch(url, { method: "POST", headers: { "Content-Type": "application/json", "x-agent-internal-token": "TEST_ONLY", ...headers }, body: JSON.stringify(input) }); expect((await post(body, { Origin: "http://browser" })).status).toBe(403); expect((await post({ ...body, workspace_directory: "/tmp/other" })).status).toBe(400); expect((await post({ ...body, session_id: "missing" })).status).toBe(404); expect((await post({ ...body, command: "cat /app/.env" })).status).toBe(403); const response = post(body); await requested; expect(await Bun.file(join(directory, "files", "approved.txt")).exists()).toBeFalse(); approve(); expect(await (await response).json()).toMatchObject({ stdout: "approved", exitCode: 0, cwd: join(directory, "files") }); expect(await readFile(join(directory, "files", "approved.txt"), "utf8")).toBe("approved"); const large = await (await post({ ...body, command: "python3 -c 'print(\"x\" * 10000)'" })).json() as { stdout: string; stdout_file: string }; expect(Buffer.byteLength(large.stdout)).toBe(3000); expect(large.stdout_file.startsWith(join(directory, "files"))).toBeTrue(); expect(await readFile(large.stdout_file, "utf8")).toBe(`${"x".repeat(10000)}\n`); }); });