Runs pi-agent in an isolated Docker container backed by a local llama.cpp LLM. Each agent session mounts a single Git repo as its workspace. The container has no general internet access — it can only reach the llama.cpp server and the web-skill proxy.
Host LAN
│ :9090 (llama.cpp web UI + API)
▼
┌──────────────────────────────────────────────────────┐
│ HOST │
│ │
│ ┌─────────────┐ llama-bridge (internal) │
│ │ llama-server│◄──────────────────────┐ │
│ │ :8080 │ │ │
│ └─────────────┘ ┌────────┴──────────┐ │
│ │ pi-agent │ │
│ ┌─────────────┐ web-internal│ (node:bookworm) │ │
│ │ web-skill │◄─────────────┤ /workspace bind │ │
│ │ :3000 │ (internal) └───────────────────┘ │
│ │ │ │ │
│ │ web-egress (normal bridge → internet) │
│ └─────────────┘ │
└──────────────────────────────────────────────────────┘
llama-bridgeandweb-internalare bothinternal: true— pi-agent has no default gateway and cannot initiate connections to the internet.web-skillis the only container with internet egress, acting as a controlled proxy for search and page fetch.- llama-server publishes port 9090 to all host interfaces for LAN access. Its egress is limited by the DNS-override hack in its own compose file (
dns: 127.0.0.1).
pi-agent/
├── docker-compose.yml # infrastructure networks + web-skill + agent service definition
├── Dockerfile # node:bookworm-slim + pi + Go + Bun + dev tools
├── pi-run # launch script: pi-run <path-to-repo>
├── pi-run-override.yml # compose override (npm-global read-only)
├── pi-install # one-off install/uninstall: pi-install npm:pkg
├── pi-install-all.sh # declarative sync: reads pi-extensions.txt and syncs
├── pi-install-override.yml # compose override (web-egress for npm access)
├── pi-extensions.txt # manifest of desired npm extensions
├── web-skill/
│ ├── Dockerfile # node:alpine
│ ├── package.json
│ └── server.js # GET /search?q= and GET /fetch?url= endpoints (DDG lite scraper)
└── settings/ # bind-mounted to ~/.pi inside the agent container
├── npm-global/ # installed npm packages (read-only at runtime)
├── npm-cache/ # npm cache (writable)
└── agent/
├── APPEND_SYSTEM.md # appended to the system prompt every session
├── settings.json # defaultProvider + defaultModel + packages list
└── models.json # local llama.cpp provider definition
The agent container ships with the following runtimes and CLI tools pre-installed:
| Category | Tools |
|---|---|
| Runtimes | Node.js 20, Go 1.24.x, Bun 1.2.x |
| CLI | ripgrep, fd, bat, jq, git, curl |
Go binaries are on PATH (/usr/local/go/bin), Bun is at /usr/local/bin/bun. Both are available for the agent to run type-checking, linting, compilation, and tests inside the container.
- Docker with Compose plugin
- The llama.cpp compose stack (separate repo/directory) already configured with
llama-bridgenetwork — see llama.cpp network setup below. - Your user must be in the
dockergroup (sudo usermod -aG docker $USER, then log out and back in).
Add this to your ~/.bashrc or ~/.zshrc:
export PATH="/path/to/pi-agent:$PATH"Then reload your shell:
source ~/.bashrc # or source ~/.zshrccd /path/to/pi-agent
docker compose buildpi-run automatically starts the infrastructure (web-skill, networks) on each invocation via docker compose up -d --no-recreate, so there is no separate "start infrastructure" step. The first pi-run will start web-skill if it isn't already running.
Note:
llama-bridgemust already exist (created by the llama.cpp compose stack) before runningpi-run. If you getnetwork llama-bridge not found, start the llama.cpp stack first.
Your llama.cpp docker-compose.yml must include the llama-bridge network so the agent container can reach it. Add the following to that file:
services:
llama-server:
# ... existing config unchanged ...
networks:
- private-net
- llama-bridge # add this
networks:
private-net:
driver: bridge
# keep existing dns: 127.0.0.1 on the service for egress blocking
llama-bridge:
driver: bridge
internal: true
name: llama-bridge # explicit name — pi-agent references thisAfter editing, recreate the llama-server container:
cd /path/to/llama-compose
docker compose up -dpi-run /path/to/your/repoThis launches an ephemeral pi-agent container with /path/to/your/repo mounted as /workspace. The container is removed when you exit the agent (Ctrl-D or /quit).
Settings in /path/to/pi-agent/settings/ persist across sessions — model selection, conversation history, and any other pi-agent config.
Inside pi-agent, use the /model command to switch between your two configured models:
| Alias | Use for |
|---|---|
local-fast |
Default — interactive use, quick edits |
local-planning |
Longer-horizon planning and reasoning tasks |
models.json reloads without a container restart when you run /model.
Defined in settings/agent/models.json. The agent reaches llama.cpp at http://llama-server:8080/v1 (Docker DNS resolves llama-server via the shared llama-bridge network). To change the model aliases, edit the id fields to match your llama.cpp models.config.toml.
settings/agent/settings.json controls the active provider and default model. Edit directly or use /settings inside pi-agent.
settings/agent/APPEND_SYSTEM.md is appended to the system prompt on every session. This is the reliable way to give the agent persistent global instructions — it currently contains the workspace path rule (/workspace is the repo root, no subdirectory navigation) and the web skill usage instructions (curl http://web-skill:3000/search?q=... and curl http://web-skill:3000/fetch?url=...).
Edit this file to adjust global agent behaviour without rebuilding the container.
Add an AGENTS.md to the root of any repo for project-specific instructions. Pi-agent loads it from the working directory at session start.
This setup runs on rootless Docker. The node:bookworm-slim base image declares USER node, which we explicitly override with USER root in our Dockerfile. This is intentional:
- Rootless Docker maps container UID 0 (root) to your host user (ajay, UID 1000)
- Container UID 1000 (node) would map to a high unmapped subUID — it doesn't own your files
- Running as container root means the agent owns its bind-mounted settings and workspace files on the host
There is no privilege escalation: container root is still just you on the host.
The agent container has no default internet access. Extensions are installed via scripts that temporarily open egress.
List desired extensions in pi-extensions.txt (one per line, npm: prefix, # for comments), then run:
pi-install-all.shThis reads the manifest, compares against what's currently installed, and does both installs and uninstalls in a single container invocation. If the list hasn't changed, it exits immediately with zero overhead.
Note: comparison is an exact string match on
npm:prefixed names. Extensions installed fromgit:orhttps://sources in the manifest will not match those entries — use the same format in the manifest as what appears insettings.json.
The npm-global/ directory is mounted read-only via pi-run-override.yml — the install override omits that constraint, so npm-global is writable through the parent settings mount during install/uninstall.
For a single install or uninstall without touching the manifest:
pi-install npm:some-extension # install
pi-install uninstall npm:some-extension # uninstallReplace npm:some-extension with any source pi install accepts (git:, https://, ssh://). The installed package lands in settings/ (the bind-mounted settings dir) and persists across container rebuilds and restarts.
If the extension is just a single .ts file with no npm dependencies, you can drop it directly into settings/agent/extensions/ instead.
The pi-coding-agent package is installed from npm during the Docker build. To update to a newer release:
cd /path/to/pi-agent
docker compose build --no-cache agentnetwork llama-bridge not found on docker compose up
The llama.cpp compose stack must be started first. That stack owns and creates the llama-bridge network.
/model shows no providers or wrong endpoint
Check that settings/agent/models.json is present and that llama-server is reachable: from inside an agent session, run bash curl -s http://llama-server:8080/health.
Web search returns empty results
DDG lite occasionally changes its HTML structure. Check the web-skill logs (docker compose logs web-skill) and test the endpoint manually from inside the agent container with curl -s http://web-skill:3000/search?q=test.