feat(firecracker): add Linux microVM backend to replace smolmachines
lint / lint (push) Successful in 2m4s
test / unit (pull_request) Successful in 59s
test / integration (pull_request) Successful in 18s
test / coverage (pull_request) Failing after 1m7s

Adds a Firecracker-based backend for Linux, providing mature KVM-based
microVM isolation to replace smolmachines/libkrun (issue #342, closes
the dead-end tracked in #332).

Architecture:
- Guest control over SSH (dropbear injected into the rootfs) on a
  point-to-point TAP link. `ssh -t` forwards SIGWINCH natively, so no
  resize bridge is needed.
- Networking: a one-time, root-provisioned pool of user-owned TAP
  devices (no shared bridge → no docker0/virbr0/cni0 collisions) plus a
  dedicated `table inet bot_bottle_fc` nftables table (independent of
  Docker/ufw/firewalld rules). `./cli.py firecracker setup` prints the
  host-appropriate config (NixOS module or sudo script).
- Rootfs: `docker export` → ext4 via `mke2fs -d` (rootless, no mount),
  cached by image digest; per-bottle SSH pubkey + IP passed via the
  kernel cmdline.
- Sidecar: reuses the Docker bundle, published on the slot's host TAP IP.
- Fail-closed isolation: TAP pool verified at preflight; the egress
  boundary is proven empirically post-boot (before the agent runs) by a
  canary probe — the VM must fail to reach the host directly, or launch
  is refused.

Linux hosts with Firecracker + KVM now default to this backend;
macOS stays on macos-container.

Not yet validated end-to-end on live hardware (requires the one-time
network pool). Unit tests + pyright pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G8p32HJgPoS1hLPWubbftM
This commit is contained in:
2026-07-11 10:32:55 -04:00
parent 814c7338a1
commit c276f7b0b1
21 changed files with 2309 additions and 5 deletions
+19 -1
View File
@@ -64,6 +64,24 @@ class TestGetBottleBackend(unittest.TestCase):
b = get_bottle_backend()
self.assertEqual("smolmachines", b.name)
def test_default_firecracker_when_macos_unavailable_but_fc_available(self):
class _FakeBackend:
def __init__(self, name: str, available: bool) -> None:
self.name = name
self._available = available
def is_available(self) -> bool:
return self._available
with patch.dict(os.environ, {}, clear=True), \
patch.object(backend_mod, "_BACKENDS", {
"macos-container": _FakeBackend("macos-container", False),
"firecracker": _FakeBackend("firecracker", True),
"smolmachines": _FakeBackend("smolmachines", True),
}):
b = get_bottle_backend()
self.assertEqual("firecracker", b.name)
def test_unknown_dies(self):
with patch.object(backend_mod, "die", side_effect=SystemExit("die")):
with self.assertRaises(SystemExit):
@@ -73,7 +91,7 @@ class TestGetBottleBackend(unittest.TestCase):
class TestKnownBackendNames(unittest.TestCase):
def test_returns_backends_sorted(self):
self.assertEqual(
("docker", "macos-container", "smolmachines"),
("docker", "firecracker", "macos-container", "smolmachines"),
known_backend_names(),
)