/** * agent-sandbox — lightweight Docker sandbox for pi coding agent. * * Default: complete isolation (no network, no host mounts). * The container has an internal /workspace directory that is always * writable, so bash/read/write/edit work even with no mounts. * * Opt in to networking, CWD mount, skills mount, SSH agent forwarding. * * Flags: * --no-sandbox disable sandbox entirely (sandbox is on by default) * --sandbox-network allow outbound network (needed for browser tool) * --sandbox-mount-cwd bind-mount the project at /workspace (rw) * --sandbox-mount-skills mount agent skill directories (ro) * --sandbox-mount-ssh forward $SSH_AUTH_SOCK into the container * --sandbox-name reusable container name (default: pi-agent-) * --sandbox-memory memory limit (default: 4g) * --sandbox-cpus CPU limit (default: 2) * * Commands: * /sandbox show container status and resource usage * /sandbox doctor verify tools inside the container * /sandbox stop|restart|rebuild|prune * /sandbox network|ssh|cwd|skills on|off */ import { randomUUID } from "node:crypto"; import { readFileSync, writeFileSync, existsSync, statSync, readdirSync } from "node:fs"; import { join as pathJoin, resolve as resolvePath, dirname } from "node:path"; import { homedir } from "node:os"; import { type ExtensionAPI, type ExtensionUIContext, createBashTool, createEditTool, createReadTool, createWriteTool, } from "@earendil-works/pi-coding-agent"; import { createRealDockerClient, createRealProcessRunner, q } from "./docker"; import { SandboxManager } from "./sandbox"; import { ToggleStore } from "./toggles"; import { createReadOps, createWriteOps, createEditOps, createBashOps } from "./tools"; import { handleSandboxCommand } from "./commands"; import type { FileStore, SkillResolver, SandboxFlags, UIContext } from "./types"; import { createUIContext } from "./types"; // ── Constants ──────────────────────────────────────────────────────────── const LOG_FILE = "/tmp/agent-sandbox.log"; function log(msg: string) { try { writeFileSync(LOG_FILE, `${new Date().toISOString()} ${msg}\n`, { flag: "a" }); } catch {} } // ── File I/O (real implementation of FileStore) ────────────────────────── const fileStore: FileStore = { read(path: string): string | null { try { return existsSync(path) ? readFileSync(path, "utf-8") : null; } catch { return null; } }, write(path: string, data: string): void { try { writeFileSync(path, data); } catch {} }, exists(path: string): boolean { return existsSync(path); }, }; // ── Session ID ─────────────────────────────────────────────────────────── function getSessionId(): string { const file = `/tmp/agent-sandbox-session-${process.pid}.json`; try { if (existsSync(file)) return JSON.parse(readFileSync(file, "utf-8")).id; } catch {} const id = randomUUID().slice(0, 8); try { writeFileSync(file, JSON.stringify({ id, pid: process.pid })); } catch {} return id; } // ── Skill discovery (real implementation of SkillResolver) ─────────────── const skillResolver: SkillResolver = { discover(): string[] { const roots = [ pathJoin(homedir(), ".agents", "skills"), pathJoin(homedir(), ".pi", "agent", "skills"), ]; const dirs: string[] = []; for (const root of roots) { if (!existsSync(root)) continue; try { for (const entry of readdirSync(root)) { const full = pathJoin(root, entry); try { if (statSync(full).isDirectory() && !dirs.includes(full)) dirs.push(full); } catch { /* skip */ } } } catch { /* skip */ } } return dirs; }, }; // ── Extension directory (for rebuild command) ──────────────────────────── function getExtensionDir(): string { const candidates = [ resolvePath(homedir(), "workspace", "agent-sandbox"), resolvePath(homedir(), ".pi", "agent", "extensions", "sandbox"), ]; for (const dir of candidates) { if (existsSync(pathJoin(dir, "Dockerfile"))) return dir; } throw new Error("Cannot find agent-sandbox Dockerfile."); } // ── Pi docs discovery ───────────────────────────────────────────────────── /** Resolve the pi agent docs directory, or null if not found. */ function discoverDocsPath(): string | null { try { const piRoot = dirname(require.resolve("@earendil-works/pi-coding-agent/package.json")); const docsDir = pathJoin(piRoot, "docs"); if (existsSync(docsDir) && statSync(docsDir).isDirectory()) { return docsDir; } return null; } catch { return null; } } // ── Browser tool ───────────────────────────────────────────────────────── function createBrowserTool(manager: SandboxManager) { return { name: "browser", label: "Browser (sandboxed)", description: "Navigate the web using a headless Chromium browser (Playwright). Use when you need to view a webpage, click elements, fill forms, or extract content.", promptSnippet: "Navigate and interact with web pages using Playwright", promptGuidelines: [ "Use the browser tool to view web pages, interact with elements, and extract page content.", "The browser runs headless inside the sandbox container. All navigation is isolated.", "Write the full Playwright script — the tool executes it as `node -e '