|
| 1 | +# apiary |
| 2 | + |
| 3 | +Run coding agents in hardware-isolated microVMs. Review every change before it touches your workspace. |
| 4 | + |
| 5 | +[](LICENSE) |
| 6 | +[](https://goreportcard.com/report/github.com/stacklok/apiary) |
| 7 | + |
| 8 | +<!-- TODO: Add a terminal recording / GIF demo here showing the full workflow --> |
| 9 | + |
| 10 | +## Why? |
| 11 | + |
| 12 | +Coding agents are powerful, but they need access to your workspace, your API keys, |
| 13 | +and the ability to run arbitrary code. That's a lot of trust to hand over. |
| 14 | + |
| 15 | +Containers help, but they share the host kernel. One escape and you're done. |
| 16 | + |
| 17 | +Enter **apiary**. It boots a lightweight microVM (via [libkrun](https://github.com/containers/libkrun) and KVM), |
| 18 | +mounts a copy-on-write snapshot of your workspace, forwards only the secrets you |
| 19 | +specify, and lets you review every file change before it lands. Hardware isolation |
| 20 | +with the feel of a local terminal. |
| 21 | + |
| 22 | +```bash |
| 23 | +apiary claude-code |
| 24 | +``` |
| 25 | + |
| 26 | +And that's it. You get a full interactive session with Claude Code running inside a |
| 27 | +VM. When the agent exits, you review the diff and accept or reject each file. |
| 28 | + |
| 29 | +## Features |
| 30 | + |
| 31 | +- **Hardware-isolated microVMs** -- KVM-backed VMs via libkrun, not just containers |
| 32 | +- **Workspace snapshot & review** -- COW snapshot so the agent never touches your real files; interactive per-file review with unified diffs when it's done |
| 33 | +- **Multi-agent support** -- Claude Code, Codex, and OpenCode out of the box, plus custom agents via config |
| 34 | +- **DNS-aware egress firewall** -- Three profiles (permissive, standard, locked) control what the VM can reach |
| 35 | +- **MCP tool proxy** -- Automatically discovers and proxies [ToolHive](https://github.com/stacklok/toolhive) MCP servers into the VM |
| 36 | +- **Git integration** -- Forwards tokens and SSH agent for git operations inside the VM |
| 37 | +- **Ephemeral security** -- Per-session SSH keys, localhost-only connections, non-overridable security patterns for sensitive files |
| 38 | +- **Zero persistent state** -- Each session is fully ephemeral; nothing lingers after cleanup |
| 39 | + |
| 40 | +## Quick Start |
| 41 | + |
| 42 | +### Prerequisites |
| 43 | + |
| 44 | +- Linux with KVM support (`/dev/kvm` must be accessible) |
| 45 | +- [Go 1.25.7+](https://go.dev/dl/) |
| 46 | +- [Task](https://taskfile.dev/) (task runner) |
| 47 | +- [libkrun-devel](https://github.com/containers/libkrun) installed |
| 48 | +- [propolis](https://github.com/stacklok/propolis) checked out at `../propolis` relative to apiary |
| 49 | +- An API key for your agent (e.g. `ANTHROPIC_API_KEY` for Claude Code) |
| 50 | + |
| 51 | +### Build |
| 52 | + |
| 53 | +```bash |
| 54 | +task build-dev |
| 55 | +``` |
| 56 | + |
| 57 | +This compiles the `apiary` binary (pure Go, no CGO) and the `propolis-runner` |
| 58 | +binary (requires libkrun-devel). Both land in `bin/`. |
| 59 | + |
| 60 | +### Run |
| 61 | + |
| 62 | +```bash |
| 63 | +export ANTHROPIC_API_KEY="sk-ant-..." |
| 64 | +apiary claude-code |
| 65 | +``` |
| 66 | + |
| 67 | +The workflow: |
| 68 | + |
| 69 | +1. Creates a COW snapshot of your current directory |
| 70 | +2. Boots a microVM with the Claude Code image |
| 71 | +3. Drops you into an interactive terminal session |
| 72 | +4. When you exit, shows a per-file diff review |
| 73 | +5. Accepted changes are flushed back to your workspace |
| 74 | + |
| 75 | +## Usage |
| 76 | + |
| 77 | +```bash |
| 78 | +# Run with a specific agent |
| 79 | +apiary claude-code |
| 80 | +apiary codex |
| 81 | +apiary opencode |
| 82 | + |
| 83 | +# Override resources |
| 84 | +apiary claude-code --cpus 4 --memory 4096 |
| 85 | + |
| 86 | +# Use a different workspace |
| 87 | +apiary claude-code --workspace /path/to/project |
| 88 | + |
| 89 | +# Disable snapshot isolation (mount workspace directly) |
| 90 | +apiary claude-code --no-review |
| 91 | + |
| 92 | +# Exclude files from snapshot |
| 93 | +apiary claude-code --exclude "*.log" --exclude "tmp/" |
| 94 | + |
| 95 | +# Lock down egress to LLM provider only |
| 96 | +apiary claude-code --egress-profile locked |
| 97 | + |
| 98 | +# Allow additional egress hosts |
| 99 | +apiary claude-code --allow-host "internal-api.example.com:443" |
| 100 | + |
| 101 | +# Disable MCP proxy |
| 102 | +apiary claude-code --no-mcp |
| 103 | + |
| 104 | +# Use a specific ToolHive group for MCP servers |
| 105 | +apiary claude-code --mcp-group "coding-tools" |
| 106 | + |
| 107 | +# List available agents |
| 108 | +apiary list |
| 109 | +``` |
| 110 | + |
| 111 | +## Configuration |
| 112 | + |
| 113 | +Apiary uses a three-level config system: CLI flags > per-workspace > global. CLI flags always win. |
| 114 | + |
| 115 | +### Global config |
| 116 | + |
| 117 | +`~/.config/apiary/config.yaml`: |
| 118 | + |
| 119 | +```yaml |
| 120 | +defaults: |
| 121 | + cpus: 4 |
| 122 | + memory: 4096 |
| 123 | + egress_profile: "standard" |
| 124 | + |
| 125 | +review: |
| 126 | + enabled: true |
| 127 | + exclude_patterns: |
| 128 | + - "*.log" |
| 129 | + - "build/" |
| 130 | + |
| 131 | +mcp: |
| 132 | + enabled: true |
| 133 | + group: "default" |
| 134 | + port: 4483 |
| 135 | + |
| 136 | +git: |
| 137 | + forward_token: true |
| 138 | + forward_ssh_agent: true |
| 139 | + |
| 140 | +agents: |
| 141 | + claude-code: |
| 142 | + env_forward: |
| 143 | + - ANTHROPIC_API_KEY |
| 144 | + - CLAUDE_* |
| 145 | + - GITHUB_TOKEN |
| 146 | +``` |
| 147 | +
|
| 148 | +### Per-workspace config |
| 149 | +
|
| 150 | +`.apiary.yaml` in your project root: |
| 151 | + |
| 152 | +```yaml |
| 153 | +defaults: |
| 154 | + cpus: 8 |
| 155 | + memory: 8192 |
| 156 | +
|
| 157 | +review: |
| 158 | + exclude_patterns: |
| 159 | + - "data/" |
| 160 | +``` |
| 161 | + |
| 162 | +Note that `review.enabled` is **ignored** in per-workspace config for security. |
| 163 | +An untrusted repo can't disable review on your behalf. |
| 164 | + |
| 165 | +Similarly, `egress_profile` in per-workspace config cannot widen the global profile. |
| 166 | + |
| 167 | +### Exclude patterns |
| 168 | + |
| 169 | +`.apiaryignore` in your project root uses gitignore syntax: |
| 170 | + |
| 171 | +```gitignore |
| 172 | +# Exclude build artifacts |
| 173 | +build/ |
| 174 | +dist/ |
| 175 | +
|
| 176 | +# But include the config |
| 177 | +!dist/config.json |
| 178 | +``` |
| 179 | + |
| 180 | +Security-sensitive patterns (`.env*`, `*.pem`, `.ssh/`, `.aws/`, etc.) are **always excluded** and cannot be negated. |
| 181 | + |
| 182 | +## Egress Firewall |
| 183 | + |
| 184 | +Each agent comes with DNS-aware egress policies. Three profiles are available: |
| 185 | + |
| 186 | +| Profile | What it allows | |
| 187 | +|---|---| |
| 188 | +| `permissive` | All outbound traffic, no restrictions | |
| 189 | +| `standard` (default) | LLM provider + common dev infrastructure (GitHub, npm, PyPI, Go proxy, Docker Hub, GHCR) | |
| 190 | +| `locked` | LLM provider only (e.g. `api.anthropic.com` for Claude Code) | |
| 191 | + |
| 192 | +```bash |
| 193 | +# Lock it down |
| 194 | +apiary claude-code --egress-profile locked |
| 195 | +
|
| 196 | +# Or open it up |
| 197 | +apiary claude-code --egress-profile permissive |
| 198 | +
|
| 199 | +# Add specific hosts to standard profile |
| 200 | +apiary claude-code --allow-host "my-registry.example.com:443" |
| 201 | +``` |
| 202 | + |
| 203 | +## Supported Agents |
| 204 | + |
| 205 | +| Agent | Command | Image | Default Resources | |
| 206 | +|---|---|---|---| |
| 207 | +| Claude Code | `apiary claude-code` | `ghcr.io/stacklok/apiary/claude-code` | 2 vCPUs, 2 GiB RAM | |
| 208 | +| Codex | `apiary codex` | `ghcr.io/stacklok/apiary/codex` | 2 vCPUs, 2 GiB RAM | |
| 209 | +| OpenCode | `apiary opencode` | `ghcr.io/stacklok/apiary/opencode` | 2 vCPUs, 2 GiB RAM | |
| 210 | + |
| 211 | +You can also define custom agents in your config: |
| 212 | + |
| 213 | +```yaml |
| 214 | +agents: |
| 215 | + my-agent: |
| 216 | + image: "ghcr.io/my-org/my-agent:latest" |
| 217 | + command: ["my-agent-binary"] |
| 218 | + cpus: 4 |
| 219 | + memory: 4096 |
| 220 | + env_forward: |
| 221 | + - MY_API_KEY |
| 222 | + - MY_AGENT_* |
| 223 | +``` |
| 224 | + |
| 225 | +## How It Works |
| 226 | + |
| 227 | +``` |
| 228 | +apiary claude-code |
| 229 | + │ |
| 230 | + ▼ |
| 231 | + Create COW snapshot of workspace |
| 232 | + │ |
| 233 | + ▼ |
| 234 | + Pull OCI image, extract rootfs, inject init binary + SSH keys |
| 235 | + │ |
| 236 | + ▼ |
| 237 | + Boot microVM (libkrun/KVM) with virtio-fs workspace mount |
| 238 | + │ |
| 239 | + ▼ |
| 240 | + Guest boots (apiary-init as PID 1): |
| 241 | + → Mount filesystems, configure networking |
| 242 | + → Start embedded SSH server |
| 243 | + → Wait for connection |
| 244 | + │ |
| 245 | + ▼ |
| 246 | + Interactive SSH session: |
| 247 | + source /etc/sandbox-env && cd /workspace && exec claude |
| 248 | + │ |
| 249 | + ▼ |
| 250 | + Agent exits → VM stopped |
| 251 | + │ |
| 252 | + ▼ |
| 253 | + SHA-256 diff → Interactive per-file review → Flush accepted changes |
| 254 | + │ |
| 255 | + ▼ |
| 256 | + Cleanup snapshot |
| 257 | +``` |
| 258 | +
|
| 259 | +The guest VM runs a custom Go init binary (`apiary-init`) as PID 1. No shell scripts, |
| 260 | +no external sshd, no iproute2. Everything the guest needs is compiled into a single |
| 261 | +binary that handles boot, networking, workspace mounting, and an embedded SSH server. |
| 262 | +
|
| 263 | +The workspace snapshot uses FICLONE on Linux for near-instant copy-on-write cloning. |
| 264 | +When the agent is done, a SHA-256 based differ detects changes, and the review UI |
| 265 | +shows unified diffs for each file. Accepted changes are flushed back with hash |
| 266 | +re-verification to prevent TOCTOU attacks. The VM is explicitly stopped before |
| 267 | +review begins, so the agent can't modify files during your review. |
| 268 | +
|
| 269 | +## Security Model |
| 270 | +
|
| 271 | +Apiary's isolation is built on several layers: |
| 272 | +
|
| 273 | +- **KVM hardware virtualization** -- The agent runs in a real VM, not a container with a shared kernel |
| 274 | +- **Ephemeral SSH keys** -- ECDSA P-256 keys generated per session, destroyed on exit |
| 275 | +- **Localhost-only networking** -- SSH port forwards bind to 127.0.0.1 only |
| 276 | +- **Non-overridable security patterns** -- Files like `.env`, `*.pem`, `.ssh/`, `.aws/` are always excluded from snapshots, even if `.apiaryignore` tries to negate them |
| 277 | +- **Shell-escaped environment injection** -- All forwarded values are single-quote escaped |
| 278 | +- **VM stopped before review** -- Prevents the agent from modifying files while you're reviewing |
| 279 | +- **Hash verification on flush** -- Files are re-hashed between diff and flush to catch any modifications |
| 280 | +- **Permission stripping** -- setuid, setgid, and sticky bits are stripped when flushing changes |
| 281 | +- **Path traversal protection** -- Symlinks are validated in-bounds before copying |
| 282 | +- **Per-workspace config restrictions** -- `review.enabled` and egress widening are ignored in `.apiary.yaml` |
| 283 | +
|
| 284 | +## Building from Source |
| 285 | +
|
| 286 | +```bash |
| 287 | +# Build everything (apiary + propolis-runner) |
| 288 | +task build-dev |
| 289 | +
|
| 290 | +# Build apiary only (pure Go, no CGO) |
| 291 | +task build |
| 292 | +
|
| 293 | +# Build guest init binary |
| 294 | +task build-init |
| 295 | +
|
| 296 | +# Run tests |
| 297 | +task test |
| 298 | +
|
| 299 | +# Lint |
| 300 | +task lint |
| 301 | +
|
| 302 | +# Format + lint + test |
| 303 | +task verify |
| 304 | +
|
| 305 | +# Build guest VM images (requires podman) |
| 306 | +task image-all |
| 307 | +``` |
| 308 | + |
| 309 | +Note: Always use `task` for building, testing, and linting. The Taskfile sets |
| 310 | +critical flags, ldflags, and environment variables that raw `go` commands miss. |
| 311 | + |
| 312 | +## Contributing |
| 313 | + |
| 314 | +Contributions are welcome! Please open an issue to discuss your idea before submitting a PR. |
| 315 | + |
| 316 | +The project follows strict DDD (Domain-Driven Design) layered architecture. |
| 317 | +See [CLAUDE.md](CLAUDE.md) for architecture details and coding conventions. |
| 318 | + |
| 319 | +## License |
| 320 | + |
| 321 | +[Apache-2.0](LICENSE) |
| 322 | + |
| 323 | +Copyright 2025 [Stacklok, Inc.](https://stacklok.com) |
0 commit comments