// ─── sandbox.ts — Matches original rust/crates/runtime/src/sandbox.rs EXACTLY ── // Container detection, Linux namespace sandboxing, filesystem isolation import { execSync } from "child_process"; import * as fs from "fs"; import * as path from "path"; import * as os from "os"; // ─── Types (match original Rust structs exactly) ──────────────────────────── export enum FilesystemIsolationMode { Off = "off", // matches original Off WorkspaceOnly = "workspace-only", // matches original #[default] WorkspaceOnly AllowList = "allow-list", // matches original AllowList } // Backward compat alias export const FilesystemIsolationModeNone = FilesystemIsolationMode.Off; export interface SandboxConfig { enabled?: boolean; namespaceRestrictions?: boolean; // namespace_restrictions networkIsolation?: boolean; // network_isolation filesystemMode?: FilesystemIsolationMode; // filesystem_mode allowedMounts: string[]; // allowed_mounts } export interface SandboxRequest { enabled: boolean; namespaceRestrictions: boolean; networkIsolation: boolean; filesystemMode: FilesystemIsolationMode; allowedMounts: string[]; } // Matches original SandboxStatus struct EXACTLY — all fields present export interface SandboxStatus { enabled: boolean; requested: SandboxRequest; // NEW: matches original supported: boolean; // NEW: matches original active: boolean; // NEW: matches original namespaceSupported: boolean; // NEW: matches original namespace_supported namespaceActive: boolean; networkSupported: boolean; // NEW: matches original network_supported networkActive: boolean; filesystemMode: FilesystemIsolationMode; filesystemActive: boolean; allowedMounts: string[]; inContainer: boolean; containerMarkers: string[]; fallbackReason: string | undefined; } export interface ContainerEnvironment { inContainer: boolean; markers: string[]; } export interface SandboxDetectionInputs { envPairs: [string, string][]; dockerenvExists: boolean; containerenvExists: boolean; proc1Cgroup: string | undefined; } export interface LinuxSandboxCommand { program: string; args: string[]; env: [string, string][]; } // ─── Default config ────────────────────────────────────────────────────────── export function defaultSandboxConfig(): SandboxConfig { return { enabled: undefined, namespaceRestrictions: undefined, networkIsolation: undefined, filesystemMode: undefined, allowedMounts: [], }; } // ─── SandboxConfig.resolve_request (matches original exactly) ─────────────── export function resolveRequest( config: SandboxConfig, overrideEnabled?: boolean, overrideNamespace?: boolean, overrideNetwork?: boolean, overrideFilesystem?: FilesystemIsolationMode, overrideMounts?: string[] ): SandboxRequest { return { enabled: overrideEnabled ?? config.enabled ?? true, namespaceRestrictions: overrideNamespace ?? config.namespaceRestrictions ?? true, networkIsolation: overrideNetwork ?? config.networkIsolation ?? false, filesystemMode: overrideFilesystem ?? config.filesystemMode ?? FilesystemIsolationMode.WorkspaceOnly, allowedMounts: overrideMounts ?? [...config.allowedMounts], }; } // ─── Container detection (matches original detect_container_environment_from EXACTLY) ── export function detectContainerEnvironmentFromInputs( inputs: SandboxDetectionInputs ): ContainerEnvironment { const markers: string[] = []; // Check /.dockerenv (matches original) if (inputs.dockerenvExists) { markers.push("/.dockerenv"); } // Check /run/.containerenv (matches original) if (inputs.containerenvExists) { markers.push("/run/.containerenv"); } // Check environment variables — matches original EXACTLY: // "container" | "docker" | "podman" | "kubernetes_service_host" for (const [key, value] of inputs.envPairs) { const normalized = key.toLowerCase(); if ( (normalized === "container" || normalized === "docker" || normalized === "podman" || normalized === "kubernetes_service_host") && value.length > 0 ) { markers.push(`env:${key}=${value}`); } } // Check /proc/1/cgroup for container markers — matches original EXACTLY: // ["docker", "containerd", "kubepods", "podman", "libpod"] if (inputs.proc1Cgroup) { const cgroupContent = inputs.proc1Cgroup; for (const needle of ["docker", "containerd", "kubepods", "podman", "libpod"]) { if (cgroupContent.includes(needle)) { markers.push(`/proc/1/cgroup:${needle}`); } } } // Sort and dedup (matches original) markers.sort(); const deduped = [...new Set(markers)]; return { inContainer: deduped.length > 0, markers: deduped, }; } export function detectContainerEnvironment(): ContainerEnvironment { let dockerenvExists = false; let containerenvExists = false; let proc1Cgroup: string | undefined; try { dockerenvExists = fs.existsSync("/.dockerenv"); } catch {} try { containerenvExists = fs.existsSync("/run/.containerenv"); } catch {} try { proc1Cgroup = fs.readFileSync("/proc/1/cgroup", "utf-8"); } catch {} const envPairs: [string, string][] = Object.entries(process.env) .filter(([_, v]) => v !== undefined) .map(([k, v]) => [k, v!]); return detectContainerEnvironmentFromInputs({ envPairs, dockerenvExists, containerenvExists, proc1Cgroup, }); } // ─── Sandbox status resolution (matches original resolve_sandbox_status_for_request EXACTLY) ── export function resolveSandboxStatusForRequest( request: SandboxRequest, cwd: string ): SandboxStatus { const container = detectContainerEnvironment(); const isLinux = os.platform() === "linux"; // namespace_supported = cfg!(target_os = "linux") && command_exists("unshare") const namespaceSupported = isLinux && commandExists("unshare"); const networkSupported = namespaceSupported; const filesystemActive = request.enabled && request.filesystemMode !== FilesystemIsolationMode.Off; const fallbackReasons: string[] = []; // Matches original fallback reason logic exactly if (request.enabled && request.namespaceRestrictions && !namespaceSupported) { fallbackReasons.push( "namespace isolation unavailable (requires Linux with `unshare`)" ); } if (request.enabled && request.networkIsolation && !networkSupported) { fallbackReasons.push( "network isolation unavailable (requires Linux with `unshare`)" ); } if ( request.enabled && request.filesystemMode === FilesystemIsolationMode.AllowList && request.allowedMounts.length === 0 ) { fallbackReasons.push( "filesystem allow-list requested without configured mounts" ); } // active = request.enabled && (!namespace || supported) && (!network || supported) const active = request.enabled && (!request.namespaceRestrictions || namespaceSupported) && (!request.networkIsolation || networkSupported); const allowedMounts = normalizeMounts(request.allowedMounts, cwd); return { enabled: request.enabled, requested: { ...request }, supported: namespaceSupported, active, namespaceSupported, namespaceActive: request.enabled && request.namespaceRestrictions && namespaceSupported, networkSupported, networkActive: request.enabled && request.networkIsolation && networkSupported, filesystemMode: request.filesystemMode, filesystemActive, allowedMounts, inContainer: container.inContainer, containerMarkers: container.markers, fallbackReason: fallbackReasons.length > 0 ? fallbackReasons.join("; ") : undefined, }; } // Convenience wrapper matching original resolve_sandbox_status() export function resolveSandboxStatus( config: SandboxConfig, cwd: string ): SandboxStatus { const request = resolveRequest(config); return resolveSandboxStatusForRequest(request, cwd); } // ─── Build sandbox command (matches original build_linux_sandbox_command EXACTLY) ── export function buildLinuxSandboxCommand( command: string, cwd: string, status: SandboxStatus ): LinuxSandboxCommand | undefined { if ( os.platform() !== "linux" || !status.enabled || (!status.namespaceActive && !status.networkActive) ) { return undefined; } const args: string[] = [ "--user", "--map-root-user", "--mount", "--ipc", "--pid", "--uts", "--fork", ]; if (status.networkActive) { args.push("--net"); } args.push("sh"); args.push("-lc"); args.push(command); const sandboxHome = path.join(cwd, ".sandbox-home"); const sandboxTmp = path.join(cwd, ".sandbox-tmp"); const env: [string, string][] = [ ["HOME", sandboxHome], ["TMPDIR", sandboxTmp], [ "CLAW_SANDBOX_FILESYSTEM_MODE", status.filesystemMode, ], [ "CLAW_SANDBOX_ALLOWED_MOUNTS", status.allowedMounts.join(":"), ], ]; const pathEnv = process.env.PATH; if (pathEnv) { env.push(["PATH", pathEnv]); } return { program: "unshare", args, env, }; } // ─── Environment info ──────────────────────────────────────────────────────── export interface EnvironmentInfo { os: string; arch: string; platform: string; hostname: string; cpus: number; totalMemoryMb: number; freeMemoryMb: number; shell: string; homeDir: string; tempDir: string; nodeVersion: string; availableTools: string[]; } export function environmentInfo(): EnvironmentInfo { const availableTools: string[] = []; const toolsToCheck = [ "git", "node", "python3", "python", "pip", "npm", "pnpm", "yarn", "cargo", "rustc", "go", "java", "gcc", "g++", "make", "cmake", "docker", "kubectl", "curl", "wget", "jq", "rg", "fd", "bat", "exa", "tmux", "vim", "nano", ]; for (const tool of toolsToCheck) { if (commandExists(tool)) { availableTools.push(tool); } } return { os: os.type(), arch: os.arch(), platform: os.platform(), hostname: os.hostname(), cpus: os.cpus().length, totalMemoryMb: Math.round(os.totalmem() / 1024 / 1024), freeMemoryMb: Math.round(os.freemem() / 1024 / 1024), shell: process.env.SHELL || "/bin/bash", homeDir: os.homedir(), tempDir: os.tmpdir(), nodeVersion: process.version, availableTools, }; } // ─── Helpers ───────────────────────────────────────────────────────────────── function normalizeMounts(mounts: string[], cwd: string): string[] { return mounts.map((mount) => { if (path.isAbsolute(mount)) { return mount; } return path.join(cwd, mount); }); } function commandExists(command: string): boolean { const pathEnv = process.env.PATH; if (!pathEnv) return false; const dirs = pathEnv.split(path.delimiter); return dirs.some((dir) => { try { return fs.existsSync(path.join(dir, command)); } catch { return false; } }); }