@the-agency/vmpi
Run pi sandboxed in a QEMU microVM via Gondolin
Package details
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.3.0- Published
- Apr 29, 2026
- Downloads
- 634/mo · 13/wk
- Author
- joshmock
- License
- MIT
- Types
- package
- Size
- 58.8 KB
- Dependencies
- 3 dependencies · 0 peers
Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
vmpi
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
/workspacevia VFS) ~/.piconfig (mounted read-only at/root/.pivia VFS)- LLM provider APIs (configurable network allowlist via HTTP hooks)
...and nothing else.
Resources: 512 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
~/.piconfig into the VM at the right paths - Install
piinside 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
- Linux (x86_64) with KVM support (
/dev/kvm) qemu-system-x86_64- Arch:
sudo pacman -S qemu-system-x86 - Ubuntu:
sudo apt install qemu-system-x86
- Arch:
qemu-img- Arch:
sudo pacman -S qemu-img - Ubuntu:
sudo apt install qemu-utils
- Arch:
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:
- Resumes an ephemeral VM from the base checkpoint (network: configured policy, VFS mounts)
- Mounts the current directory at
/workspace - Mounts
~/.piat/root/.pi - Runs
pi updateto install any pi packages listed in the config - Prepares Pi session history: symlinks
~/.pi/agent/sessions/subdirectory to host CWD session dir - Runs
pi [args]interactively inside the VM with a full PTY - Collects sessions written during the run back to the host
- Closes the VM when pi exits
vmpi setup:
- Downloads the pi tarball on the host (cached in
~/.vmpi/cache/) - Boots a fresh VM and writes the tarball into it
- Runs
npm install -ginside the VM - Creates a disk checkpoint at
~/.vmpi/base-checkpoint.qcow2
Configuration
vmpi can be configured with a config file. It searches for configuration files from your current directory up to the root directory with one of the following names:
.vmpirc.json.vmpirc.yaml.vmpirc.yml
Example .vmpirc.json
{
"memory": 512,
"cpus": 2,
"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:
{
"network": {
"providers": ["anthropic", "github"]
},
"guestPackages": ["github-cli"],
"secrets": {
"GITHUB_TOKEN": { "hosts": ["api.github.com", "github.com"] }
}
}
Options
| Key | Default | Description |
|---|---|---|
memory |
512 |
RAM in MiB |
cpus |
1 |
vCPU count |
piConfigDir |
~/.pi |
Path to the pi config directory on the host |
stateDir |
~/.vmpi |
Where vmpi stores the base checkpoint and tarball cache |
network.policy |
inferred | "allow-all", "deny-all", or "custom". Auto-set to "custom" when providers/domains are configured |
network.providers |
[] |
LLM provider names to allow (see below) |
network.allowedDomains |
[] |
Additional external domain patterns to allow |
network.localServices |
[] |
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. |
rootfsExtraMb |
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 |
[] |
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. |
secrets |
{} |
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 | Source |
|---|---|---|
github-copilot |
*.githubcopilot.com, api.github.com, copilot-proxy.githubusercontent.com |
docs.github.com/en/copilot/reference/copilot-allowlist-reference |
gemini |
generativelanguage.googleapis.com, oauth2.googleapis.com, www.googleapis.com |
ai.google.dev/gemini-api/docs/quickstart |
openai |
api.openai.com |
platform.openai.com/docs/api-reference |
anthropic |
api.anthropic.com |
docs.anthropic.com/en/api/getting-started |
ollama |
localhost, 127.0.0.1 |
(local service, no external network) |
github |
github.com, *.github.com, *.githubusercontent.com |
about-githubs-ip-addresses |
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. 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
- Linux x86_64 only: macOS and aarch64 are untested. The
q35machine type workaround is specific to Linux x86_64; other platforms may need adjustments. - First run is slow:
vmpi setupdownloads the Gondolin guest image (~300 MB), downloads the pi tarball (~4 MB), and runsnpm installinside 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 (~79 MiB).
vmpi setupautomatically grows the image byrootfsExtraMbMiB (default: 128) whenever free space is below that threshold. This requirese2fsprogs(sudo apt install e2fsprogs/sudo pacman -S e2fsprogs). If setup still fails with a "disk full" error, increaserootfsExtraMbin your.vmpirc.jsonor viaVMPI_ROOTFS_EXTRA_MB. - Session directory is tmpfs:
/root,/tmp, and/var/logare tmpfs-backed in the guest and will be dropped from memory on quit. Sessions under/root/.piare 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-Zsuspends Pi. This does not work withvmpi. As a workaround, runvmpiinside tmux, screen, or Zellij.