A containerized DevOps agent built on the Pi harness
(@earendil-works/pi-coding-agent). It operates and maintains self-describing
services — starting with the sibling llm-router: inspecting
its REST API and hosted Agent Skill, evaluating metrics and logs, and (with
human approval) acting on Docker lifecycle and configuration.
📄 Want the "why it's safe" story, for a non-technical audience? Read What an auditable AI agent looks like — control, traceability, and measured proof, with the eval numbers.
Pi is a minimal agent harness with four core tools (read/write/edit/bash)
and no built-in permission system — its own guidance for autonomous use is to
"deploy in a container and build a custom approval extension for side-effecting
tools." That is exactly this project:
- The container is the security boundary (non-root, no raw
docker.sock). - A maker-checker approval gate (
.pi/extensions/20-approval.ts) blocks every mutating tool until a human approves, and audits every decision as JSONL. - Docker access is scoped through a
docker-socket-proxyallowlist. - Pi's own brain is independent of the router it manages (local llama.cpp),
so it keeps working during a router outage; the router
/v1is only a fallback.
llm-router-net (existing)
├─ llm-router (managed service) :8000 /system/*, /admin/*, /v1
├─ prometheus :9090 time-series metrics (PromQL)
├─ docker-guard :2375 deny-by-default boundary filter
│ └─(docker-control, internal)→ docker-socket-proxy → /var/run/docker.sock:ro
└─ agent-pi (node, non-root)
• Pi harness + project extensions (.pi/extensions)
• reasons with local llama.cpp (independent of the router)
DOCKER_HOST=tcp://docker-guard:2375 (never mounts the socket; cannot
reach the proxy directly — the guard is the only Docker path)
docker-guard (src/docker/guard.ts, property-tested) allows ONLY the five real
DockerClient calls on allow-listed names — create/DELETE/exec/kill and
non-allow-listed names are refused at the boundary (Claim A; see
docs/THREAT_MODEL.md). Verify it live:
scripts/verify-boundary.sh (guard mode).
agent-pi manages N self-describing services, one descriptor per service in
services/<id>.yaml (identity, discovery paths, admin/auth, metrics, per-service
policy, allowlist, autonomy, skill). The watcher runs one monitor per enabled
service; each learns its thresholds/log-patterns from its own manifest. See
fixtures/dummy-service/ for a generic Tier-2 example.
| File | Role |
|---|---|
.pi/extensions/00-provider.ts |
Registers the local llama.cpp brain + router fallback. |
.pi/extensions/10-devops-tools.ts |
Read-only, service-scoped tools: service_health, service_manifest, service_metrics, service_capabilities, service_skill (digest-verified). |
.pi/extensions/30-docker-tools.ts |
docker_ps + docker_start/stop/restart (queued for approval, per-service allowlist). |
.pi/extensions/40-router-admin.ts |
llm-router config (Tier 3): guardrail/model toggles, reload, rollback. |
.pi/extensions/20-approval.ts |
Maker-checker gate for mutating built-ins + JSONL audit. |
Supporting libraries live in src/ (registry/, discovery/service.ts,
monitor.ts, notify/, audit.ts); Pi-side behavior in policy/*.yaml.
Requires Node 22 and a local llama.cpp OpenAI-compatible endpoint on :8888.
pnpm install --ignore-scripts
LLAMACPP_BASE_URL=http://localhost:8888/v1 \
ROUTER_URL=http://localhost:8000 \
OPENAI_API_KEY=sk-local \
PI_OFFLINE=1 \
node_modules/.bin/pi -a --offline --model llamacpp/Qwen3.5-9B-local \
-p "Call service_health for llm-router and summarize whether it is healthy."-a trusts the project-local .pi/ files for the run; --model provider/id
selects the local brain.
cp .env.example .env # adjust model / URLs / allowlist
docker compose up -d # starts agent-pi + docker-socket-proxy
docker exec -it agent-pi pi -a --model llamacpp/Qwen3.5-9B-local \
-p "check the llm-router health"agent-pi joins the existing llm-router-net, so it reaches llm-router:8000
and prometheus:9090 by name. It never mounts /var/run/docker.sock.
- Read-only tools run autonomously; every mutating action requires approval.
- Headless (no attached UI) → mutations are blocked by default.
- Docker access goes through
docker-guard(deny-by-default: only start/stop/ restart + inspect onALLOWED_CONTAINERS), which fronts the internal-onlydocker-socket-proxy. The container-name + verb allowlist is a boundary control, not just agent code. - All proposed/approved/denied/executed actions are appended to
logs/audit.jsonl.
Phase M0 (scaffold + read-only boot) is implemented and validated. See BACKLOG.md for the phased roadmap (observe → lifecycle → config → bounded autonomy).