Skip to content
 
 

Repository files navigation

pi coding agent in an Apple Container

ZBoy - A sovereign, local coding agent on macOS.

A sovereign AI agent capable of running on 16G mac. It is fun!
The pi coding agent runs in a disposable Apple container micro-VM and talks to a local MLX model on the host via oMLX — no Node, no npm, no agent binary on your work machine. Built for Rails 8 applications with integrated browser & messaging support, but capable of building and maintaining anything.

platform runtime agent model inference status license


Overview

A modern coding agent reads your files, runs shell commands, and installs whatever it decides it needs. On a work machine in a regulated context that is an unacceptable blast radius. This repository contains a runnable setup that closes it:

  • Inference stays native on the host — MLX needs Apple Silicon's Metal/ANE, which a Linux VM does not expose. The default model is MLX-Qwen3.5-9B-GLM5.1-Distill-v1-6bit, served locally via oMLX.
  • The agent runtime is sandboxed in its own VM — Apple container gives each container a lightweight VM, not shared-kernel namespaces.
  • The host stays clean — no Node, no npm, no pi binary; the agent lives only inside an image and is discarded on exit.

The full step-by-step walkthrough is the article en-pi-apple-container.md.

Architecture

┌─────────────────────────────┐        ┌──────────────────────────────┐
│ Host (macOS, Apple Silicon) │        │ Apple Container (Linux VM)   │
│                             │        │                              │
│  oMLX server                │◄──────►│  pi-coding-agent             │
│  /v1/chat/completions       │ Bridge │  (Node 22, ripgrep, git,     │
│  MLX-Qwen3.5-9B-GLM5.1-     │        │   Ruby, Rails, ffmpeg,       │
│    Distill-v1-6bit          │        │   SQLite)                    │
└─────────────────────────────┘        │  Workspace: /workspace       │
                                      └──────────────────────────────┘
  • Inference runs on the host via oMLX (no Metal/ANE in a Linux VM).
  • Tool-calling sandbox runs in the container — a clean split between model runtime and agent runtime.
  • pi reaches the host only over the container bridge; the gateway IP is discovered at runtime.

Repository structure

.
├── Containerfile                   # node:22-bookworm-slim + pi, Ruby, Rails, ffmpeg, SQLite
├── pi-config/
│   ├── AGENTS.md                   # global agent rules (container variant)
│   ├── models.json                 # provider + model definition (oMLX)
│   ├── settings.json               # pi runtime settings + package list
│   ├── npm/                        # extension packages (npm-installed)
│   └── extensions/
│       └── protected-paths/
│           └── index.ts            # tool-call guardrail for sensitive paths
└── scripts/
    ├── build.sh                    # container build
    ├── run.sh                      # container run with the right mounts
    ├── acp.sh                      # expose pi as an ACP server (for Zed, etc.)
    └── shell.sh                    # interactive shell in the same container environment

pi-config/ is mounted into the container at runtime as the agent's config directory.

Prerequisites

  • macOS 26 (Tahoe) on Apple Silicon (recommended). Container-to-host networking is the linchpin; older macOS limits it severely.
  • Apple container CLI (container --version must answer).
  • oMLX running on the host, serving an OpenAI-compatible /v1/chat/completions endpoint bound to 0.0.0.0 (not 127.0.0.1). The default model is MLX-Qwen3.5-9B-GLM5.1-Distill-v1-6bit.
  • No Node and no npm on the host — that is the point; the agent lives only in the image.

Quickstart

1. Build

./scripts/build.sh

2. Find the bridge IP

container run --rm --entrypoint sh pi-coding-agent:local \
  -c "ip route | awk '/default/ {print \$3}'"

Update baseUrl in pi-config/models.json if the printed IP differs (keep the :8888/v1 suffix).

3. Run

PROJECT_DIR=~/projects/your-repo ./scripts/run.sh

That's it. run.sh mounts pi-config/ → agent config and $PROJECT_DIR/workspace. The VM is discarded on exit.

4. Shell (optional)

To open an interactive Linux shell in the same container environment (same mounts, resource limits, and pi user) without starting the agent:

PROJECT_DIR=~/projects/your-repo ./scripts/shell.sh

Useful for debugging, inspecting the filesystem, or running commands manually inside the container.

Editor integration via ACP

ZBoy also exposes the containerised pi as an ACP (Agent Client Protocol) server, letting you use it directly from editors like Zed instead of the terminal. The script scripts/acp.sh bridges ACP JSON-RPC 2.0 over stdio between the editor and pi-acp running inside the container — same mounts, same sandbox, same model.

Run

PROJECT_DIR=~/projects/your-repo ./scripts/acp.sh

No TTY is allocated (ACP is raw stdio), and the pi-acp session-map is persisted to pi-config/pi-acp/ so sessions survive across restarts.

Zed setup

Add the following to your Zed settings.json:

"agent_servers": {
  "pi": {
    "type": "custom",
    "command": "/path/to/ZBoy/scripts/acp.sh",
    "args": [],
    "env": {
      "PROJECT_DIR": "/path/to/your/project"
    }
  }
}

Replace /path/to/ZBoy with the absolute path to your ZBoy checkout, and set PROJECT_DIR to the project you want the agent to work on. The editor will spawn acp.sh automatically when you start a new agent panel.

Configuration

Models — pi-config/models.json

Defines the mlx-local provider pointing at the oMLX server. apiKey is "not-required" (local server, no secret to leak). The model id must match exactly what oMLX reports — currently MLX-Qwen3.5-9B-GLM5.1-Distill-v1-6bit.

Global agent rules — pi-config/AGENTS.md

Loaded into every session: runs in a container, host not directly reachable, file ops only in /workspace, no external calls without explicit instruction, and tool discipline (read before edit, write only for new files).

Extension — protected-paths

Hooks pi's tool_call event and forces confirmation (or hard-denies) for sensitive paths (~/.ssh, ~/.aws, .env, credentials.json, *.pem, etc.). Defense-in-depth for the day someone widens a mount.

Messenger bridge — pi-messenger-bridge

Bridges common messengers (Telegram, WhatsApp, Slack, Discord, Matrix) into pi so remote users can interact with the agent from their phone or chat app.

Installed as an npm extension — listed in pi-config/settings.json under packages and installed in pi-config/npm/. No changes to the Containerfile are needed; the package is picked up at runtime from the mounted config volume.

Supported transports

Transport Setup
Telegram Create a bot via @BotFather, set PI_TELEGRAM_TOKEN
WhatsApp Run /msg-bridge configure whatsapp, scan QR code
Slack Create a Socket Mode app, set PI_SLACK_BOT_TOKEN + PI_SLACK_APP_TOKEN
Discord Create a bot in the Developer Portal, enable Message Content Intent, set PI_DISCORD_TOKEN
Matrix Register a bot account, set PI_MATRIX_HOMESERVER + PI_MATRIX_ACCESS_TOKEN

Usage

# Interactive setup menu (inside a pi session)
/msg-bridge

# Configure a specific transport
/msg-bridge configure telegram

# Connect to all configured transports
/msg-bridge connect

# Check status
/msg-bridge status

Credentials can be set via environment variables (prefixed PI_*) or stored in ~/.pi/msg-bridge.json. Environment variables take precedence. New users authenticate via a 6-digit challenge code displayed in the pi terminal.

See the pi-messenger-bridge README for full documentation.

Troubleshooting

Symptom Cause & fix
Requests hang, no error Local Network permission not granted. System Settings → Privacy & Security → Local Network — enable the container runtime, then reopen.
"Can't reach the model" oMLX bound to loopback. Bind to 0.0.0.0.
Connection refused Wrong bridge IP. Re-run step 2 and update models.json.
Files owned by UID 1000 Expected. Container writes as pi (UID 1000). Acceptable in the pi edit-workflow.
Agent answers but never edits No native tool-calling. Verify the model supports OpenAI function-calling.
ACP server doesn't respond Smoke-test the handshake. Run echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientInfo":{"name":"cli-test","version":"1.0"},"protocolVersion":1}}' | PROJECT_DIR=. ./scripts/acp.sh — a JSON-RPC response with agentInfo means pi-acp is alive inside the container.

Notes & caveats

  • MLX stays on the host. No Metal/ANE inside a Linux VM.
  • Bridge IP is environment-dependent — discovered at runtime, never hardcoded.
  • API keys are never committed. models.json uses "not-required" for local oMLX servers.

How practical is it?

You can see a screencast here which shows as-is inference and performance on a 16G machine.

You can also watch a demo of a long-running agent session via Discord, showing remote interaction with the agent through the messenger bridge.

License

Licensed under the MIT License — see LICENSE.

Acknowledgements

This project was inspired by A sovereign coding agent on macOS — pi in an Apple Container, zero npm on the host by Michael Hannecke.

About

ZBoy - A sovereign, local coding agent on macOS.

Topics

Resources

Stars

Watchers

Forks

Contributors

Languages