@the-agency/vmpi

Run pi sandboxed in a QEMU microVM via Gondolin

Packages

Package details

package

Install @the-agency/vmpi from npm and Pi will load the resources declared by the package manifest.

$ pi install npm:@the-agency/vmpi
Package
@the-agency/vmpi
Version
0.4.4
Published
Sep 18, 2026
Downloads
411/mo · 348/wk
Author
joshmock
License
MIT
Types
package
Size
79 KB
Dependencies
4 dependencies · 0 peers

Security note

Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.

README

vmpi

NPM Downloads

Run pi sandboxed in a QEMU microVM via Gondolin.

vmpi feels just like pi, but the agent runs in a hardware-isolated sandbox with access to only:

  • the current directory (mounted read-write at /workspace via VFS)
  • ~/.pi config (mounted read-only at /root/.pi via VFS)
  • LLM provider APIs (configurable network allowlist via HTTP hooks)

...and nothing else.

Resources: 1024 MiB RAM, 1 vCPU, network restricted to configured LLM provider domains

No root required: Gondolin's QEMU backend runs entirely as the current user.

Why vmpi?

Running pi directly gives the agent access to your entire filesystem and unrestricted network access. That is fine for trusted, well-understood tasks, but risky for exploratory or agentic workloads where a hallucination or a bad tool call could touch files outside your project.

vmpi wraps Gondolin, a QEMU microVM library, to give you hardware-level isolation without the setup cost. You could configure Gondolin directly, but you would need to handle all of the following yourself:

  • Build and cache a base VM checkpoint so each run starts in ~1 s instead of minutes
  • Mount your current directory and ~/.pi config into the VM at the right paths
  • Install pi inside the VM and keep it updated
  • Enforce a network allowlist so the agent can reach your LLM provider but nothing else
  • Translate ~/.pi/agent/sessions/ directories so prior conversation history is visible inside the VM and new sessions are written back to the host

vmpi handles all of that. From the outside it feels like typing pi.

Requirements

QEMU

For both macOS and Linux, QEMU is required. Look for installation instructions appropriate to your environment.

e2fsprogs (recommended)

vmpi setup uses dumpe2fs, e2fsck, and resize2fs to check and, when necessary, grow the Gondolin rootfs image. On Linux these come with your distribution's e2fsprogs package. On macOS they are not part of the base system; install them with:

brew install e2fsprogs

The formula is keg-only, but vmpi probes the Homebrew keg paths (/opt/homebrew/opt/e2fsprogs/sbin, /usr/local/opt/e2fsprogs/sbin) automatically. Without e2fsprogs, setup skips the resize check with a warning.

Install

npm install -g @the-agency/vmpi

# or clone and link locally:
cd packages/vmpi
npm install
npm run build
npm link

Usage

# first run: builds a base VM checkpoint (downloads pi, ~5 min)
vmpi setup

# subsequent runs resume from the checkpoint in ~1 s
vmpi "refactor the auth module to use JWTs"

# rebuild the base checkpoint (e.g. to upgrade pi)
vmpi setup

# show checkpoint status
vmpi status

# enable Gondolin debug logging
vmpi setup --debug
vmpi --debug

Every vmpi invocation:

  1. Resumes an ephemeral VM from the base checkpoint (network: configured policy, VFS mounts)
  2. Mounts the current directory at /workspace
  3. Mounts ~/.pi at /root/.pi
  4. Runs pi update to install any pi packages listed in the config
  5. Prepares Pi session history: symlinks ~/.pi/agent/sessions/ subdirectory to host CWD session dir
  6. Runs pi [args] interactively inside the VM with a full PTY
  7. Collects sessions written during the run back to the host
  8. Closes the VM when pi exits

vmpi setup:

  1. Downloads the pi tarball on the host (cached in ~/.vmpi/cache/)
  2. Boots a fresh VM and writes the tarball into it
  3. Runs npm install -g inside the VM
  4. Creates a disk checkpoint at ~/.vmpi/base-checkpoint.qcow2

Configuration

vmpi reads configuration from two separate sources with different trust levels.

Trusted config (host-owned) holds all security-sensitive settings and is read only from:

  • ~/.config/vmpi/config.json
  • ~/.config/vmpi/config.yaml
  • ~/.config/vmpi/config.yml

(~/.config follows $XDG_CONFIG_HOME when set.)

Project config holds non-security preferences only. It is searched from your current directory up to the root directory with one of the following names:

  • .vmpirc.json
  • .vmpirc.yaml
  • .vmpirc.yml

Security-sensitive fields (network, mounts, secrets, piConfigDir, stateDir) are read only from the trusted config. If a project .vmpirc.* declares any of them, they are ignored and a warning is printed. This ensures an untrusted repository cannot expand the guest's host capabilities. The remaining fields (memory, cpus, rootfsExtraMb, guestPackages, postSetupHooks) may be set per project; when set in both, the project value wins for these preferences.

Example trusted ~/.config/vmpi/config.json

{
  "network": {
    "providers": ["github-copilot", "anthropic"],
    "allowedDomains": ["my-custom-llm.example.com"],
    "localServices": [{ "hostname": "ollama.local", "port": 11434 }]
  }
}

To use the gh CLI inside the sandbox, add the github network preset and forward your token in the trusted config:

{
  "network": {
    "providers": ["anthropic", "github"]
  },
  "secrets": {
    "GITHUB_TOKEN": { "hosts": ["api.github.com", "github.com"] }
  }
}

Example project .vmpirc.json

{
  "memory": 1024,
  "cpus": 2,
  "guestPackages": ["github-cli"],
  "postSetupHooks": ["npm install -g typescript"]
}

Options

Source is trusted (~/.config/vmpi/config.*) or project (.vmpirc.*). trusted keys are security-sensitive and ignored if declared in a project config.

Key Source Default Description
memory project 1024 RAM in MiB
cpus project 1 vCPU count
rootfsExtraMb project 128 MiB to add to the Gondolin rootfs image during vmpi setup when free space is below this threshold. Increase this if setup fails with a disk-full error.
guestPackages project [] Extra Alpine packages to install in the guest during vmpi setup, in addition to the defaults: git, fd, ripgrep, curl, jq, bash, python3, py3-pip, nodejs, npm, make, patch, file, sqlite.
postSetupHooks project [] Shell commands to run inside the VM after packages are installed, before the checkpoint is saved. Each command runs via /bin/sh -c. A non-zero exit aborts setup. Use this to install tools not available as Alpine packages, e.g. npm install -g typescript or gem install rails.
piConfigDir trusted ~/.pi Path to the pi config directory on the host
stateDir trusted ~/.vmpi Where vmpi stores the base checkpoint and tarball cache
network.policy trusted inferred "allow-all", "deny-all", or "custom". Auto-set to "custom" when providers/domains are configured
network.providers trusted [] LLM provider names to allow (see below)
network.allowedDomains trusted [] Additional external domain patterns to allow
network.localServices trusted [] Host services to expose inside the VM. Each entry is { hostname, port }. The VM can reach hostname at the given host port via a raw TCP tunnel.
mounts trusted [] Host directories to mount into the VM at runtime. Each entry is { "host": "...", "guest": "..." }. The host path supports a leading ~. Example: [{ "host": "~/.config/some-tool", "guest": "/root/.config/some-tool" }].
secrets trusted {} Secrets to inject into the VM, each scoped to specific hosts using Gondolin's secret handling. Each key is the guest env var name. Value: { "hosts": ["api.github.com"] }. Override the host-side var name with "env": { "hosts": [...], "env": "MY_PAT" }. Values are passed via a tmpfs env file and never written to persistent storage.
Environment variables (VMPI_MEMORY, VMPI_CPUS, PI_CONFIG_DIR, VMPI_STATE_DIR, VMPI_ROOTFS_EXTRA_MB) override their config file equivalents.

Built-in providers

Provider Allowed domains
github-copilot *.githubcopilot.com, api.github.com, copilot-proxy.githubusercontent.com
gemini generativelanguage.googleapis.com, oauth2.googleapis.com, www.googleapis.com
openai api.openai.com
anthropic api.anthropic.com
github github.com, *.github.com, *.githubusercontent.com
openrouter api.openrouter.ai
ollama, llama.cpp localhost, 127.0.0.1

Multiple providers can be combined. Their domains are merged with any allowedDomains.

How it works

Gondolin manages QEMU microVMs with a JavaScript-implemented network stack and VFS. vmpi setup builds a base checkpoint by installing pi, along with any extensions listed in your ~/.pi/agent/settings.json, into a fresh VM and saving a disk snapshot. The pi bundle is assembled on the host with npm flags targeting the guest platform (--os=linux --cpu=<host arch> --libc=musl), so platform-specific optional dependencies resolve to binaries that work inside the Alpine VM regardless of the host OS. Each vmpi run resumes from that checkpoint, then mounts the workspace and ~/.pi via VFS providers (RealFSProvider).

The pi tarball is downloaded on the host (not inside the VM) because Gondolin's MITM proxy does not reliably handle large concurrent streaming downloads. The tarball is transferred into the VM via vm.fs.writeFile() which uses the virtio-serial control channel, a separate path from the network proxy that correctly handles large files.

Network policy is enforced via createHttpHooks, which intercepts all HTTP/TLS egress and blocks requests to unlisted hosts.

Session continuity

Pi stores sessions under ~/.pi/agent/sessions/ named after the project path. Because the project is always mounted at /workspace inside the VM, vmpi translates session directories on both sides:

  • Before launch: a symlink ~/.pi/agent/sessions/--workspace-- to host CWD session dir is created so pi finds prior sessions. Writes go directly to the host filesystem via the VFS mount.
  • After exit: the symlink is removed. Any Pi sessions written directly to the --workspace-- slot are merged back into the host CWD session dir.

Why no root required

Gondolin's QEMU backend runs entirely in userspace; no TAP devices, no nftables rules, no setuid jailer. vmpi runs as your normal user as long as /dev/kvm is group-readable (the default on most Linux distros with KVM enabled).

Limitations

  • Platforms: Tested on Linux (x86_64) and macOS (Apple Silicon). The guest VM always matches the host architecture (aarch64 hosts run the aarch64 guest image). Other platforms may need adjustments; pull requests welcome!
  • First run is slow: vmpi setup downloads the Gondolin guest image (~300 MB), downloads the pi tarball (~4 MB), and runs npm install inside the VM. Subsequent runs are much faster as they resume from the checkpoint created during setup.
  • Rootfs free space: Gondolin's Alpine rootfs image has limited free space (~90 MiB). vmpi setup automatically grows the image by rootfsExtraMb MiB (default: 128) whenever free space is below that threshold. This requires e2fsprogs on the host (see Requirements); without it the check is skipped with a warning. If setup still fails with a "disk full" error, increase rootfsExtraMb in your .vmpirc.json or via VMPI_ROOTFS_EXTRA_MB.
  • Session directory is tmpfs: /root, /tmp, and /var/log are tmpfs-backed in the guest and will be dropped from memory on quit. Sessions under /root/.pi are visible on the host via the VFS mount and are not lost when the VM closes.
  • Ctrl-z does not background vmpi: In a regular terminal, pressing Ctrl-z suspends Pi. This does not work with vmpi. As a workaround, run vmpi inside a terminal multiplexer like tmux, screen, or Zellij.