added pi-gondolin.ts for quemu vm

quietlight
May 5, 2026, 10:05 PM
5O2QH5NZ37ZUMVA7N6CVHNUM4BRL7ZB6N5TQ6ZZWJUSERXWCBOGQC

Dependencies

Change contents

  • file addition: pi-gondolin.ts (----------)
    [5.1]
    /**
    * Pi + Gondolin Sandbox Example (pi extension)
    *
    * This extension overrides pi's built-in `read`/`write`/`edit`/`bash` tools so
    * they execute inside a Gondolin micro-VM instead of on the host.
    *
    * The directory you start `pi` in is mounted read-write at `/workspace` inside
    * the VM.
    *
    * How to run:
    * 1. Install dependencies for this repo (so imports resolve):
    * pnpm install
    * 2. Ensure QEMU is installed (see the gondolin README "Quick Start")
    * 3. Start pi in the project you want to sandbox:
    * cd /path/to/your/project
    * pi -e /absolute/path/to/gondolin/host/examples/pi-gondolin.ts
    *
    * Notes:
    * - The VM is started on `session_start` (and lazily if a tool is used before that)
    * - User `!` commands are also executed inside the VM
    * - Module resolution happens relative to this file, so keeping it inside the
    * gondolin repo (or installing `@earendil-works/gondolin` next to it) is easiest
    */
    import path from "node:path";
    import type {
    ExtensionAPI,
    ExtensionContext,
    } from "@mariozechner/pi-coding-agent";
    import {
    type BashOperations,
    createBashTool,
    createEditTool,
    createReadTool,
    createWriteTool,
    type EditOperations,
    type ReadOperations,
    type WriteOperations,
    } from "@mariozechner/pi-coding-agent";
    import { RealFSProvider, VM } from "@earendil-works/gondolin";
    const GUEST_WORKSPACE = "/workspace";
    function shQuote(value: string): string {
    // POSIX shell quoting: wraps in single quotes and escapes internal quotes
    return "'" + value.replace(/'/g, "'\\''") + "'";
    }
    function toGuestPath(localCwd: string, localPath: string): string {
    // pi tools pass absolute local paths; map them into /workspace.
    const rel = path.relative(localCwd, localPath);
    if (rel === "") return GUEST_WORKSPACE;
    if (rel.startsWith("..") || path.isAbsolute(rel)) {
    throw new Error(`path escapes workspace: ${localPath}`);
    }
    // Convert platform separators to POSIX for the Linux guest
    const posixRel = rel.split(path.sep).join(path.posix.sep);
    return path.posix.join(GUEST_WORKSPACE, posixRel);
    }
    function createGondolinReadOps(vm: VM, localCwd: string): ReadOperations {
    return {
    readFile: async (p) => {
    const guestPath = toGuestPath(localCwd, p);
    const r = await vm.exec(["/bin/cat", guestPath]);
    if (!r.ok) {
    throw new Error(`cat failed (${r.exitCode}): ${r.stderr}`);
    }
    return r.stdoutBuffer;
    },
    access: async (p) => {
    const guestPath = toGuestPath(localCwd, p);
    const r = await vm.exec([
    "/bin/sh",
    "-lc",
    `test -r ${shQuote(guestPath)}`,
    ]);
    if (!r.ok) {
    throw new Error(`not readable: ${p}`);
    }
    },
    detectImageMimeType: async (p) => {
    const guestPath = toGuestPath(localCwd, p);
    try {
    // Run through the shell because `file` might live in `/usr/bin` depending on the image
    const r = await vm.exec([
    "/bin/sh",
    "-lc",
    `file --mime-type -b ${shQuote(guestPath)}`,
    ]);
    if (!r.ok) return null;
    const m = r.stdout.trim();
    return ["image/jpeg", "image/png", "image/gif", "image/webp"].includes(
    m,
    )
    ? m
    : null;
    } catch {
    return null;
    }
    },
    };
    }
    function createGondolinWriteOps(vm: VM, localCwd: string): WriteOperations {
    return {
    writeFile: async (p, content) => {
    const guestPath = toGuestPath(localCwd, p);
    const dir = path.posix.dirname(guestPath);
    // Base64 roundtrip to avoid quoting issues
    const b64 = Buffer.from(content, "utf8").toString("base64");
    const script = [
    `set -eu`,
    `mkdir -p ${shQuote(dir)}`,
    `echo ${shQuote(b64)} | base64 -d > ${shQuote(guestPath)}`,
    ].join("\n");
    const r = await vm.exec(["/bin/sh", "-lc", script]);
    if (!r.ok) {
    throw new Error(`write failed (${r.exitCode}): ${r.stderr}`);
    }
    },
    mkdir: async (dir) => {
    const guestDir = toGuestPath(localCwd, dir);
    const r = await vm.exec(["/bin/mkdir", "-p", guestDir]);
    if (!r.ok) {
    throw new Error(`mkdir failed (${r.exitCode}): ${r.stderr}`);
    }
    },
    };
    }
    function createGondolinEditOps(vm: VM, localCwd: string): EditOperations {
    const r = createGondolinReadOps(vm, localCwd);
    const w = createGondolinWriteOps(vm, localCwd);
    return { readFile: r.readFile, access: r.access, writeFile: w.writeFile };
    }
    function sanitizeEnv(
    env?: NodeJS.ProcessEnv,
    ): Record<string, string> | undefined {
    if (!env) return undefined;
    const out: Record<string, string> = {};
    for (const [k, v] of Object.entries(env)) {
    if (typeof v === "string") out[k] = v;
    }
    return out;
    }
    function createGondolinBashOps(vm: VM, localCwd: string): BashOperations {
    return {
    exec: async (command, cwd, { onData, signal, timeout, env }) => {
    const guestCwd = toGuestPath(localCwd, cwd);
    const ac = new AbortController();
    const onAbort = () => ac.abort();
    signal?.addEventListener("abort", onAbort, { once: true });
    let timedOut = false;
    const timer =
    timeout && timeout > 0
    ? setTimeout(() => {
    timedOut = true;
    ac.abort();
    }, timeout * 1000)
    : undefined;
    try {
    // `/bin/bash -lc` for a familiar environment (pipelines, expansions, etc.)
    const proc = vm.exec(["/bin/bash", "-lc", command], {
    cwd: guestCwd,
    signal: ac.signal,
    env: sanitizeEnv(env),
    stdout: "pipe",
    stderr: "pipe",
    });
    for await (const chunk of proc.output()) {
    onData(chunk.data);
    }
    const r = await proc;
    return { exitCode: r.exitCode };
    } catch (err) {
    if (signal?.aborted) throw new Error("aborted");
    if (timedOut) throw new Error(`timeout:${timeout}`);
    throw err;
    } finally {
    if (timer) clearTimeout(timer);
    signal?.removeEventListener("abort", onAbort);
    }
    },
    };
    }
    export default function (pi: ExtensionAPI) {
    const localCwd = process.cwd();
    const localRead = createReadTool(localCwd);
    const localWrite = createWriteTool(localCwd);
    const localEdit = createEditTool(localCwd);
    const localBash = createBashTool(localCwd);
    let vm: VM | null = null;
    let vmStarting: Promise<VM> | null = null;
    async function ensureVm(ctx?: ExtensionContext) {
    if (vm) return vm;
    if (vmStarting) return vmStarting;
    vmStarting = (async () => {
    ctx?.ui.setStatus(
    "gondolin",
    ctx.ui.theme.fg(
    "accent",
    `Gondolin: starting (mount ${GUEST_WORKSPACE})`,
    ),
    );
    const created = await VM.create({
    vfs: {
    mounts: {
    [GUEST_WORKSPACE]: new RealFSProvider(localCwd),
    },
    },
    });
    vm = created;
    ctx?.ui.setStatus(
    "gondolin",
    ctx.ui.theme.fg(
    "accent",
    `Gondolin: running (${localCwd} -> ${GUEST_WORKSPACE})`,
    ),
    );
    ctx?.ui.notify(
    `Gondolin VM ready. Host ${localCwd} mounted at ${GUEST_WORKSPACE}`,
    "info",
    );
    return created;
    })();
    return vmStarting;
    }
    pi.on("session_start", async (_event, ctx) => {
    // Start eagerly so the user sees errors early (missing qemu, etc.)
    await ensureVm(ctx);
    });
    pi.on("session_shutdown", async (_event, ctx) => {
    if (!vm) return;
    ctx.ui.setStatus(
    "gondolin",
    ctx.ui.theme.fg("muted", "Gondolin: stopping"),
    );
    try {
    await vm.close();
    } finally {
    vm = null;
    vmStarting = null;
    }
    });
    pi.registerTool({
    ...localRead,
    async execute(id, params, signal, onUpdate, ctx) {
    const activeVm = await ensureVm(ctx);
    const tool = createReadTool(localCwd, {
    operations: createGondolinReadOps(activeVm, localCwd),
    });
    return tool.execute(id, params, signal, onUpdate);
    },
    });
    pi.registerTool({
    ...localWrite,
    async execute(id, params, signal, onUpdate, ctx) {
    const activeVm = await ensureVm(ctx);
    const tool = createWriteTool(localCwd, {
    operations: createGondolinWriteOps(activeVm, localCwd),
    });
    return tool.execute(id, params, signal, onUpdate);
    },
    });
    pi.registerTool({
    ...localEdit,
    async execute(id, params, signal, onUpdate, ctx) {
    const activeVm = await ensureVm(ctx);
    const tool = createEditTool(localCwd, {
    operations: createGondolinEditOps(activeVm, localCwd),
    });
    return tool.execute(id, params, signal, onUpdate);
    },
    });
    pi.registerTool({
    ...localBash,
    async execute(id, params, signal, onUpdate, ctx) {
    const activeVm = await ensureVm(ctx);
    const tool = createBashTool(localCwd, {
    operations: createGondolinBashOps(activeVm, localCwd),
    });
    return tool.execute(id, params, signal, onUpdate);
    },
    });
    // Run user `!` commands inside the VM too
    pi.on("user_bash", (_event, ctx) => {
    if (!vm) return;
    return { operations: createGondolinBashOps(vm, localCwd) };
    });
    // Replace the CWD line in the system prompt so the model sees /workspace
    pi.on("before_agent_start", async (event, ctx) => {
    await ensureVm(ctx);
    const modified = event.systemPrompt.replace(
    `Current working directory: ${localCwd}`,
    `Current working directory: ${GUEST_WORKSPACE} (Gondolin VM, mounted from host: ${localCwd})`,
    );
    return { systemPrompt: modified };
    });
    }
  • replacement in me.txt at line 595
    [2.3280][2.3280:3450]()
    └─────────┴───────┴─────────────────────┴──────────────┘
    [2.3280]
    └─────────┴───────┴─────────────────────┴──────────────┘
    XNCKPSQERHNFZ6R4HHLDYJJUZEAPCKI4NXZITXVOMJOQXSUD6QQAC