claw-web-v2 / server /runtime /sandbox.ts
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;
}
});
}