// Tool operation backends that execute inside the sandbox (bwrap / sandbox-exec). // Only the write-capable tools are routed here — reads (read/ls/find/grep) run as // pi's normal host tools, since a read can't breach a *write* boundary. import type { EditOperations, WriteOperations } from "@earendil-works/pi-coding-agent"; import type { BashOperations } from "@earendil-works/pi-coding-agent"; import { resolveInputPath } from "./paths.ts"; import type { Sandbox } from "./sandbox.ts"; async function run( c: Sandbox, argv: string[], opts: { input?: Buffer | string; signal?: AbortSignal } = {}, ): Promise { const res = await c.exec(argv, opts); if (res.code !== 0) { throw new Error(res.stderr.trim() || `command failed (exit ${res.code}): ${argv.join(" ")}`); } return res.stdout; } /** Resolve a model path to an absolute host path (identity — same inside the sandbox). */ function apath(c: Sandbox, p: string): string { return resolveInputPath(c.spec.cwd, p); } export function writeOps(c: Sandbox): WriteOperations { return { writeFile: async (p, content) => { await run(c, ["sh", "-c", 'cat > "$1"', "sh", apath(c, p)], { input: content }); }, mkdir: async (dir) => { await run(c, ["mkdir", "-p", "--", apath(c, dir)]); }, }; } export function editOps(c: Sandbox): EditOperations { return { readFile: async (p) => run(c, ["cat", "--", apath(c, p)]), writeFile: async (p, content) => { await run(c, ["sh", "-c", 'cat > "$1"', "sh", apath(c, p)], { input: content }); }, access: async (p) => { // Route through the sandbox so readonly mode reports "not writable" cleanly // (the file sits on a read-only bind / write-denied path). await run(c, ["sh", "-c", 'test -r "$1" && test -w "$1"', "sh", apath(c, p)]); }, }; } /** BashOperations: stream a command running inside the sandbox. */ export function bashOps(c: Sandbox): BashOperations { return { exec: async (command, execCwd, { onData, signal, timeout, env }) => { return c.execShellStream(command, apath(c, execCwd), { onData, signal, timeout, env }); }, }; }