import "server-only"; import { spawn, type ChildProcessByStdio } from "node:child_process"; import type { Readable } from "node:stream"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { env } from "../env"; /** * Runs Python in a throw-away directory with no network, under macOS sandbox-exec when available. * Limits: 20 s wall clock, 64 KB of captured output, no access outside the temp dir (seatbelt profile). */ export async function runPython(code: string, signal?: AbortSignal): Promise<{ stdout: string; stderr: string; exitCode: number | null; timedOut: boolean; durationMs: number; files?: string[] }> { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "csa-py-")); const script = path.join(dir, "main.py"); await fs.writeFile(script, code, "utf8"); const profile = `(version 1) (deny default) (allow process-exec process-fork) (allow sysctl-read) (allow mach-lookup) (allow file-read*) (deny file-read* (subpath "${os.homedir()}")) (allow file-read* (subpath "${dir}")) (allow file-write* (subpath "${dir}")) (allow file-write* (subpath "/private/tmp") (subpath "/private/var/folders")) (deny network*) `; const profilePath = path.join(dir, "profile.sb"); await fs.writeFile(profilePath, profile); const useSeatbelt = process.platform === "darwin"; const cmd = useSeatbelt ? "/usr/bin/sandbox-exec" : env.PYTHON_BIN; const args = useSeatbelt ? ["-f", profilePath, env.PYTHON_BIN, "-I", "-B", script] : ["-I", "-B", script]; const t0 = Date.now(); return await new Promise(resolve => { const child: ChildProcessByStdio = spawn(cmd, args, { cwd: dir, env: { ...process.env, PATH: "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin", HOME: dir, TMPDIR: dir, PYTHONDONTWRITEBYTECODE: "1", MPLBACKEND: "Agg" }, stdio: ["ignore", "pipe", "pipe"] as const }); let out = "", err = "", timedOut = false; const cap = (s: string, d: string) => (s.length < 65_536 ? s + d : s); child.stdout.on("data", (d: Buffer) => { out = cap(out, d.toString()); }); child.stderr.on("data", (d: Buffer) => { err = cap(err, d.toString()); }); const timer = setTimeout(() => { timedOut = true; child.kill("SIGKILL"); }, 20_000); signal?.addEventListener("abort", () => child.kill("SIGKILL")); child.on("close", async (code: number | null) => { clearTimeout(timer); let files: string[] = []; try { files = (await fs.readdir(dir)).filter(f => !["main.py", "profile.sb"].includes(f) && !f.endsWith(".pyc")); } catch { /* ignore */ } resolve({ stdout: out, stderr: err, exitCode: code, timedOut, durationMs: Date.now() - t0, files }); setTimeout(() => fs.rm(dir, { recursive: true, force: true }).catch(() => {}), 60_000); }); child.on("error", (e: Error) => { clearTimeout(timer); resolve({ stdout: out, stderr: String(e.message), exitCode: null, timedOut: false, durationMs: Date.now() - t0 }); }); }); }