Three bugs fixed:
1. PTY connections failed because home directory was hardcoded as
/home/{username} instead of reading from /etc/passwd. For root,
this produced /home/root/ which doesn't exist — CWD validation
rejected every PTY Start request without explicit cwd. Fixed all
6 locations to use user.dir from nix::unistd::User.
2. MMDS polling silently failed to parse metadata because the
logs_collector_address field lacked #[serde(default)]. The host
agent only sends instanceID + envID — missing "address" field
caused every deserialize attempt to fail, so .WRENN_SANDBOX_ID
and .WRENN_TEMPLATE_ID were never written. Also added error
logging and create_dir_all before file writes.
3. Metrics CPU values were non-deterministic because a fresh
sysinfo::System was created per request with a 100ms sleep
between reads. Replaced with a background thread that samples
CPU at fixed 1-second intervals via a persistent System instance,
matching gopsutil's internal caching behavior. Metrics endpoint
now reads cached atomic values — no blocking, consistent window.
Also: close master PTY fd in child pre_exec, add process.Start
request logging, bump version to 0.2.0.
Wrenn
Secure infrastructure for AI
Prerequisites
- Linux host with
/dev/kvmaccess (bare metal or nested virt) - Firecracker binary at
/usr/local/bin/firecracker - PostgreSQL
- Go 1.25+
- Rust 1.88+ with
x86_64-unknown-linux-musltarget (rustup target add x86_64-unknown-linux-musl) - pnpm (for frontend)
- Docker (for dev infra and rootfs builds)
Build
make build # outputs to builds/
Produces three binaries: wrenn-cp (control plane), wrenn-agent (host agent), envd (guest agent).
Host setup
The host agent needs a kernel, a minimal rootfs image, and working directories on the host machine.
Directory structure
/var/lib/wrenn/
├── kernels/
│ └── vmlinux # uncompressed Linux kernel (not bzImage)
├── images/
│ └── minimal/
│ └── rootfs.ext4 # base rootfs (all other templates snapshot from this)
├── sandboxes/ # per-sandbox CoW files (created at runtime)
└── snapshots/ # pause/hibernate snapshot files (created at runtime)
Create the directories:
sudo mkdir -p /var/lib/wrenn/{kernels,images/minimal,sandboxes,snapshots}
Kernel
Place an uncompressed vmlinux kernel at /var/lib/wrenn/kernels/vmlinux. Versioned kernels (vmlinux-{semver}) are also supported — the agent picks the latest by semver.
Minimal rootfs
The minimal rootfs is the base image that all other templates (Python, Node, etc.) are built on top of via device-mapper snapshots. It must contain:
| Package | Why |
|---|---|
socat |
Bidirectional relay for port forwarding |
chrony |
Time sync from KVM PTP clock (/dev/ptp0) |
tini |
PID 1 zombie reaper (injected by build script, not apt) |
sudo |
User privilege management inside the guest |
wget |
HTTP fetching |
curl |
HTTP client |
ca-certificates |
TLS certificate verification |
To build a rootfs from a Docker container:
-
Create and configure a container with the required packages:
docker run -it --name wrenn-minimal debian:bookworm bash # Inside the container: apt update && apt install -y socat chrony sudo wget curl ca-certificates exit -
Export to a rootfs image (builds envd, injects wrenn-init + tini, shrinks to minimum size):
sudo bash scripts/rootfs-from-container.sh wrenn-minimal minimal
To update an existing rootfs after changing envd or wrenn-init.sh:
bash scripts/update-minimal-rootfs.sh
This rebuilds envd via make build-envd and copies the fresh binaries into the mounted rootfs image.
IP forwarding
sudo sysctl -w net.ipv4.ip_forward=1
Configure
Copy .env.example to .env and edit:
# Required
DATABASE_URL=postgres://wrenn:wrenn@localhost:5432/wrenn?sslmode=disable
# Control plane
WRENN_CP_LISTEN_ADDR=:8000
CP_HOST_AGENT_ADDR=http://localhost:50051
# Host agent
WRENN_HOST_LISTEN_ADDR=:50051
WRENN_DIR=/var/lib/wrenn
Development
make dev # Start PostgreSQL (Docker), run migrations, start control plane
make dev-agent # Start host agent (separate terminal, sudo)
make dev-frontend # Vite dev server with HMR (port 5173)
make check # fmt + vet + lint + test
Host registration
Hosts must be registered with the control plane before they can serve sandboxes.
-
Create a host record (via API or dashboard):
curl -X POST http://localhost:8000/v1/hosts \ -H "Authorization: Bearer $JWT_TOKEN" \ -H "Content-Type: application/json" \ -d '{"type": "regular"}'This returns a
registration_token(valid for 1 hour). -
Start the host agent with the registration token and its externally-reachable address:
sudo WRENN_CP_URL=http://localhost:8000 \ ./builds/wrenn-agent \ --register <token-from-step-1> \ --address <host-ip>:50051On first startup the agent sends its specs (arch, CPU, memory, disk) to the control plane, receives a long-lived host JWT, and saves it to
$WRENN_DIR/host-token. -
Subsequent startups don't need
--register— the agent loads the saved JWT automatically:sudo ./builds/wrenn-agent --address <host-ip>:50051 -
If registration fails (e.g., network error after token was consumed), regenerate a token:
curl -X POST http://localhost:8000/v1/hosts/$HOST_ID/token \ -H "Authorization: Bearer $JWT_TOKEN"Then restart the agent with the new token.
The agent sends heartbeats to the control plane every 30 seconds.
See CLAUDE.md for full architecture documentation.