// Configuration resolution: defaults <- global json <- project json (if trusted) // <- env overrides. Much smaller than the container model — no image, runtime, // packages, GC, or env mirroring (the sandbox inherits the host env directly). import { existsSync, readFileSync } from "node:fs"; import { join } from "node:path"; import { CONFIG_DIR_NAME, getAgentDir } from "@earendil-works/pi-coding-agent"; import type { Backend } from "./sandbox.ts"; export type Mode = "off" | "readonly" | "restricted"; export interface GuardConfig { backend: "auto" | Backend; // auto => bwrap on Linux, sandbox-exec on macOS mode: Mode; allow: string[]; // extra rw dirs (restricted mode) } const DEFAULTS: GuardConfig = { backend: "auto", mode: "readonly", allow: [], }; function readJson(path: string): Partial { if (!existsSync(path)) return {}; try { return JSON.parse(readFileSync(path, "utf8")); } catch (e) { console.error(`pi-guard: could not parse ${path}: ${e}`); return {}; } } function merge(base: GuardConfig, o: Partial): GuardConfig { return { backend: o.backend ?? base.backend, mode: o.mode ?? base.mode, allow: o.allow ?? base.allow, }; } /** Load config: defaults <- global json <- project json (if trusted) <- env overrides. */ export function loadConfig(cwd: string, trusted = false): GuardConfig { const globalPath = join(getAgentDir(), "extensions", "pi-guard.json"); let cfg = merge(DEFAULTS, readJson(globalPath)); if (trusted) { const projectPath = join(cwd, CONFIG_DIR_NAME, "pi-guard.json"); cfg = merge(cfg, readJson(projectPath)); } const envBackend = process.env.PI_GUARD_BACKEND; if (envBackend === "auto" || envBackend === "bwrap" || envBackend === "sandbox-exec") { cfg = { ...cfg, backend: envBackend }; } const envMode = process.env.PI_GUARD_MODE; if (envMode === "off" || envMode === "readonly" || envMode === "restricted") { cfg = { ...cfg, mode: envMode }; } return cfg; }