Files
bot-bottle/docs/prds/0069-firecracker-native-docker-free.md
T
didericis 3b1b716822 docs(prd): 0070 per-host orchestrator service
Fold the per-bottle sidecar bundle into a single persistent per-host
orchestrator: runs the sidecar functions (egress/git-gate/supervise),
coordinates with the console, and brokers agent launches. Virtualized
from the start with backend-native isolation (fc VM / apple ctr / docker
ctr), fronted by a single backend-agnostic contract; per-backend
variation lives on BottleBackend, not a parallel Orchestrator hierarchy.

Leads with the security review (secret concentration, shared fate, the
launch-broker-as-new-privileged-core, and the source-IP attribution
invariant each backend must enforce). Proposes one SQLite DB owned by the
orchestrator for runtime state (slot leases, approvals, registry) —
distinct from build-time constants (flat .env) and user config
(declarative ~/.bot-bottle). Sequences docker -> firecracker -> macos,
developing the service as a plain-process dev-harness before the VM.

Supersedes 0069's Stage-1/4 sidecar framing; depends on 0069's nix-built
fixed images. Tracking issue #351.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WBMWTEtQdJ4W5UrWuLHCck
2026-07-14 01:28:32 -04:00

165 lines
7.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PRD 0069: Firecracker-native, Docker-free backend
- **Status:** Draft (partially superseded)
- **Author:** Claude
- **Created:** 2026-07-12
- **Issue:** #348
> **Superseded in part by [PRD 0070](0070-per-host-orchestrator.md) (#351):**
> the sidecar-consolidation framing here (Stage 1, per-host sidecar; Stage 4,
> sidecar-as-VM) is taken over by 0070's per-host orchestrator. This PRD still
> owns the docker-free **image-building** work — Stage 2 (nix-built fixed
> images, a dependency of 0070) and Stage 3 (in-VM Dockerfile builder).
## Summary
Make the Firecracker backend depend on **firecracker + KVM only**, removing
Docker from the host. Two moves get us there: run the **sidecar bundle as a
persistent, per-host service** (eventually a Firecracker VM) instead of a
per-bottle container, and **build agent rootfs images without a host Docker
daemon** (nix for the fixed images; an in-VM builder for user Dockerfiles).
## Motivation
Two operator-facing costs and one security constraint:
- **Resource cost.** Every bottle spins up its own sidecar bundle (egress
mitmproxy + git-gate + supervise). N bottles → N heavy bundles.
- **Operational simplicity.** Per-launch `docker run` churn, a Docker daemon
to keep healthy, and Docker's iptables to coexist with.
- **Privilege / minimal-runner.** The backend needs the Docker socket
(rootfs export + sidecar containers), and membership in the `docker`
group is root-equivalent. This blocks a genuinely unprivileged run: a
dedicated CI-runner user (or any confined caller) is effectively root as
long as Docker is required. See the paused work in
`nix/gitea-runner.nix` / the coverage CI gate — it can't be "minimal"
until Docker is gone.
Removing Docker collapses all three.
## Where Docker is used today (inventory)
1. **Rootfs source**`docker build` (agent image from a Dockerfile) then
`docker create` + `docker export | tar``mke2fs -d` (see
`bot_bottle/backend/firecracker/util.py:build_base_rootfs_dir` and
`docker_image_id`).
2. **Sidecar bundle**`docker run bot-bottle-sidecars-<slug>` per bottle
(egress / git-gate / supervise); the VM reaches it at the host TAP IP via
DNAT (`launch.py`, `resolve_common`).
3. **Digests / cache keys**`docker image inspect` for the rootfs cache
key.
## Goals
- Host prerequisites for the Firecracker backend become **firecracker,
`/dev/kvm`, iproute2, nftables** — no Docker daemon, no `docker` group.
- Launch is **rootless** (open the pre-created TAP pool + KVM; no socket).
- One sidecar per host instead of one per bottle (resource + ops win).
## Non-goals
- Changing the macOS (Apple Container) or legacy Docker backends.
- Removing OCI/Dockerfile support for agent images — users keep writing
Dockerfiles; only the *host* stops needing a Docker daemon.
## Design (staged)
Ordered so each stage is independently valuable and de-risks the next.
### Stage 1 — Sidecar: persistent + per-host, source-IP-keyed
One sidecar bundle per host, shared by all bottles, with per-bottle policy
keyed on the **source IP** of incoming traffic. This is safe here because
the point-to-point `/31` TAP + the `bot_bottle_fc` nft table make the source
IP of anything from `bbfcN` *provably* that bottle's guest IP (no spoofing,
no cross-bottle traffic) — so the sidecar can attribute a request to a
bottle with confidence a shared bridge could not offer.
Per-service, sharing differs:
- **supervise** — host-level is a clean win (unified approval queue, ~no
secrets). Do first.
- **egress (mitmproxy)** — shareable via a client-IP addon that selects the
per-bottle allowlist / DLP / token-injection. Higher blast radius: one
process now holds every bottle's upstream tokens (see Security).
- **git-gate** — most secret-dense (per-repo deploy keys) and least
naturally shareable (git carries no source-IP-scoped auth). Keep
per-bottle unless there's a strong reason.
Needs a small **control plane**: add/remove a bottle's routes/keys/proposals
on launch/teardown with live reload, replacing "config baked at launch,
torn down at exit."
Can ship as a container first (quick resource/ops win) and become a VM in
Stage 4.
### Stage 2 — Fixed images built with nix (no Docker)
The images bot-bottle *ships* — the sidecar, the agent base, and the builder
(Stage 3) — are built declaratively with nix (`nixos-generators` /
`make-ext4-fs` / `pkgs.dockerTools` for the rootfs), producing an ext4 or
tar with correct ownership. Removes Docker for everything we own and gives
the rootless-rootfs correctness (#347) for free on these images.
### Stage 3 — User Dockerfiles built in a builder VM (the unlock)
The variable part — a user's own agent Dockerfile — builds **inside a
throwaway (or persistent) Firecracker builder VM** running `buildah`/`podman`
(rootless, daemonless) or a full in-guest dockerd. The host runs no Docker.
Bonus: an untrusted Dockerfile executes in a disposable VM, which is *more*
isolated than `docker build` on the host.
With this, launch touches no host Docker → the backend is rootless → the
dedicated CI-runner user needs no `docker` group. **This is the stage that
unblocks the minimal-runner / coverage-CI work.**
Open problems to solve here (prototype first):
- **Build cache.** No Docker layer cache; a persistent cache disk on the
builder VM (or content-addressed rootfs cache) so rebuilds aren't full
re-`apt`.
- **Build-time egress.** `apt`/pulls need network → through the sidecar,
which is itself built earlier → nix-built fixed images break the
chicken-and-egg (nothing needs Docker to come up).
### Stage 4 — Sidecar (and builder) as Firecracker VMs
Full firecracker-native: the sidecar is a VM on its own TAP; agent VM →
sidecar VM is VM-to-VM, so the host forwards `bbfcN` → sidecar TAP and the
nft table grows forward rules (today it *drops* all non-DNAT'd egress). A
**per-host** sidecar VM makes the boot/memory overhead amortized and gives a
stable IP every agent points at.
## Security considerations
- **Secret concentration.** Per-bottle sidecars isolate secrets at the
process boundary — each holds only its bottle's tokens/keys. A host
sidecar concentrates *all* bottles' secrets in one long-lived process and
shifts isolation to **application-level** (correct source-IP keying). A
single attribution bug leaks bottle A's token into bottle B's request — a
class of bug that can't exist per-bottle. Mitigation: lean on the
unspoofable TAP+nft attribution; consider keeping the most secret-dense
service (git-gate) per-bottle.
- **Build isolation improves.** Running untrusted Dockerfiles in a
disposable VM is stronger than host `docker build`.
- **Shared fate.** A host sidecar crash/compromise now affects every bottle.
## Open questions
- Build cache design (per-builder-VM disk vs content-addressed host cache).
- VM-to-VM routing + the nft forward rules for a sidecar VM.
- Control-plane shape for dynamic per-bottle sidecar config + live reload.
- Whether egress is worth sharing given the secret-concentration tradeoff,
or only supervise (+ keep egress/git-gate per-bottle for now).
## Rollout / relation to other work
- Stages 12 are high-value and comparatively cheap; **Stage 3 is the
unlock** for rootless launch and the paused dedicated-runner work; Stage 4
is the pure finish and the most networking effort.
- Prototype Stage 3 first ("Dockerfile → agent rootfs, inside a Firecracker
VM, cached, with build-time egress"): if that's ergonomic and fast, the
rest follows.
- Related: #347 (rootless rootfs ownership — subsumed by nix-built fixed
images + the in-VM builder), and the deferred coverage CI gate / gitea
runner (blocked on Stage 3).