SPB Git
2.9 KB · 53 lines typescript
1import "server-only";2import { spawn, type ChildProcessByStdio } from "node:child_process";3import type { Readable } from "node:stream";4import fs from "node:fs/promises";5import os from "node:os";6import path from "node:path";7import { env } from "../env";89/**10 * Runs Python in a throw-away directory with no network, under macOS sandbox-exec when available.11 * Limits: 20 s wall clock, 64 KB of captured output, no access outside the temp dir (seatbelt profile).12 */13export async function runPython(code: string, signal?: AbortSignal): Promise<{ stdout: string; stderr: string; exitCode: number | null; timedOut: boolean; durationMs: number; files?: string[] }> {14  const dir = await fs.mkdtemp(path.join(os.tmpdir(), "csa-py-"));15  const script = path.join(dir, "main.py");16  await fs.writeFile(script, code, "utf8");17  const profile = `(version 1)18(deny default)19(allow process-exec process-fork)20(allow sysctl-read)21(allow mach-lookup)22(allow file-read*)23(deny file-read* (subpath "${os.homedir()}"))24(allow file-read* (subpath "${dir}"))25(allow file-write* (subpath "${dir}"))26(allow file-write* (subpath "/private/tmp") (subpath "/private/var/folders"))27(deny network*)28`;29  const profilePath = path.join(dir, "profile.sb");30  await fs.writeFile(profilePath, profile);31  const useSeatbelt = process.platform === "darwin";32  const cmd = useSeatbelt ? "/usr/bin/sandbox-exec" : env.PYTHON_BIN;33  const args = useSeatbelt ? ["-f", profilePath, env.PYTHON_BIN, "-I", "-B", script] : ["-I", "-B", script];34  const t0 = Date.now();35  return await new Promise(resolve => {36    const child: ChildProcessByStdio<null, Readable, Readable> = 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 });37    let out = "", err = "", timedOut = false;38    const cap = (s: string, d: string) => (s.length < 65_536 ? s + d : s);39    child.stdout.on("data", (d: Buffer) => { out = cap(out, d.toString()); });40    child.stderr.on("data", (d: Buffer) => { err = cap(err, d.toString()); });41    const timer = setTimeout(() => { timedOut = true; child.kill("SIGKILL"); }, 20_000);42    signal?.addEventListener("abort", () => child.kill("SIGKILL"));43    child.on("close", async (code: number | null) => {44      clearTimeout(timer);45      let files: string[] = [];46      try { files = (await fs.readdir(dir)).filter(f => !["main.py", "profile.sb"].includes(f) && !f.endsWith(".pyc")); } catch { /* ignore */ }47      resolve({ stdout: out, stderr: err, exitCode: code, timedOut, durationMs: Date.now() - t0, files });48      setTimeout(() => fs.rm(dir, { recursive: true, force: true }).catch(() => {}), 60_000);49    });50    child.on("error", (e: Error) => { clearTimeout(timer); resolve({ stdout: out, stderr: String(e.message), exitCode: null, timedOut: false, durationMs: Date.now() - t0 }); });51  });52}53
Files · laptop-build-20261101
Menu

Theme