Spaces:
Sleeping
Sleeping
Claw Web
Full parity with original Rust: session validation, sandbox detection, remote proxy, hooks payload, LSP shutdown
49cbb33 | // βββ 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; | |
| } | |
| }); | |
| } | |