/** * Sandbox manager: decides when to use Docker isolation vs local execution. * * Modes: * - "off" — all tools run locally (default) * - "available" — use Docker when available, fall back to local * - "required" — require Docker for destructive tools (enterprise mode) */ import type { ToolResult } from "../protocol" import { runInSandbox } from "./docker" export type SandboxMode = "off" | "available" | "required" let sandboxMode: SandboxMode = "off" let dockerAvailable: boolean | null = null export function getSandboxMode(): SandboxMode { return sandboxMode } export function setSandboxMode(mode: SandboxMode): void { sandboxMode = mode dockerAvailable = null // re-check on next use } export function isDockerAvailable(): boolean { if (dockerAvailable !== null) return dockerAvailable // Quick check: can we run `docker info`? try { const { execSync } = require("child_process") as typeof import("child_process") execSync("docker info", { timeout: 3000, stdio: "pipe" }) dockerAvailable = true } catch { dockerAvailable = false } return dockerAvailable } /** * Execute a bash command, optionally in a Docker sandbox. */ export async function sandboxedExec( command: string, cwd: string, timeout: number, ): Promise { if (sandboxMode === "off") { return localExec(command, cwd, timeout) } if (sandboxMode === "available" && !isDockerAvailable()) { return localExec(command, cwd, timeout) } if (sandboxMode === "required" && !isDockerAvailable()) { return { name: "bash", output: { error: "Enterprise mode requires Docker but Docker is not available. Install Docker or disable enterprise mode.", }, isError: true, } } return runInSandbox(command, cwd) } function localExec(command: string, cwd: string, timeout: number): ToolResult { const { execSync } = require("child_process") as typeof import("child_process") try { const stdout = execSync(command, { cwd, timeout, maxBuffer: 1024 * 1024 * 10, encoding: "utf-8", shell: process.platform === "win32" ? "cmd.exe" : "/bin/bash", }) return { name: "bash", output: { stdout: stdout || "(no output)", exitCode: 0 }, isError: false } } catch (err: unknown) { const e = err as { stdout?: string; stderr?: string; status?: number; killed?: boolean } let output = "" if (e.stdout) output += String(e.stdout).slice(0, 50_000) if (e.stderr) output += "\n--- stderr ---\n" + String(e.stderr).slice(0, 50_000) if (e.killed) output += `\n--- Process timed out after ${timeout}ms ---` return { name: "bash", output: { stdout: output.trim(), exitCode: e.status ?? 1 }, isError: true, } } }