feat(firecracker): add Linux microVM backend to replace smolmachines
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:
@@ -16,6 +16,7 @@ from . import list as _list_mod
|
||||
from .cleanup import cmd_cleanup
|
||||
from .commit import cmd_commit
|
||||
from .edit import cmd_edit
|
||||
from .firecracker import cmd_firecracker
|
||||
from .info import cmd_info
|
||||
from .init import cmd_init
|
||||
from .resume import cmd_resume
|
||||
@@ -28,6 +29,7 @@ COMMANDS = {
|
||||
"cleanup": cmd_cleanup,
|
||||
"commit": cmd_commit,
|
||||
"edit": cmd_edit,
|
||||
"firecracker": cmd_firecracker,
|
||||
"info": cmd_info,
|
||||
"init": cmd_init,
|
||||
"list": cmd_list,
|
||||
@@ -43,6 +45,7 @@ def usage() -> None:
|
||||
sys.stderr.write(" cleanup stop and remove all active bot-bottle containers\n")
|
||||
sys.stderr.write(" commit snapshot a running bottle's container state to a Docker image\n")
|
||||
sys.stderr.write(" edit open an agent in vim for editing\n")
|
||||
sys.stderr.write(" firecracker one-time network setup for the Firecracker backend\n")
|
||||
sys.stderr.write(" info print env, skills, and prompt details for a named agent\n")
|
||||
sys.stderr.write(" init interactively create a new agent and add it to bot-bottle.json\n")
|
||||
sys.stderr.write(" list list available agents or active containers\n")
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
"""`firecracker` CLI command — one-time network setup helper.
|
||||
|
||||
`./cli.py firecracker setup` prints the host-appropriate config for the
|
||||
privileged, one-time network pool (TAP devices + isolation nftables
|
||||
table) the Firecracker backend needs. On NixOS it prints a declarative
|
||||
module to paste into your config (imperative rules don't survive
|
||||
nixos-rebuild); elsewhere it prints the sudo command for the bundled
|
||||
setup script. `./cli.py firecracker status` reports what's present.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from ..backend.firecracker import netpool
|
||||
|
||||
|
||||
def _is_nixos() -> bool:
|
||||
if Path("/etc/NIXOS").exists():
|
||||
return True
|
||||
try:
|
||||
return "ID=nixos" in Path("/etc/os-release").read_text()
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def _cmd_setup() -> int:
|
||||
slots = netpool.all_slots()
|
||||
sys.stderr.write(
|
||||
f"Firecracker network pool: {len(slots)} slots "
|
||||
f"({slots[0].iface}..{slots[-1].iface}), base {netpool.ip_base()}.\n"
|
||||
f"This is a one-time privileged setup (needs root once).\n\n"
|
||||
)
|
||||
if _is_nixos():
|
||||
sys.stderr.write(
|
||||
"Detected NixOS. Paste this module into your configuration and "
|
||||
"`nixos-rebuild switch` (imperative rules would not survive a "
|
||||
"rebuild):\n\n"
|
||||
)
|
||||
sys.stdout.write(netpool.render_nixos_module())
|
||||
else:
|
||||
sys.stderr.write("Run the one-time setup as root:\n\n")
|
||||
sys.stdout.write(netpool.render_shell_setup() + "\n")
|
||||
sys.stderr.write(
|
||||
"\n(On NixOS, use the declarative module instead — this host "
|
||||
"was not detected as NixOS.)\n"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def _cmd_status() -> int:
|
||||
ok = True
|
||||
if netpool.nft_table_present():
|
||||
sys.stderr.write(f"nft table inet {netpool.NFT_TABLE}: present\n")
|
||||
else:
|
||||
sys.stderr.write(f"nft table inet {netpool.NFT_TABLE}: MISSING\n")
|
||||
ok = False
|
||||
missing = netpool.missing_taps()
|
||||
total = netpool.pool_size()
|
||||
if missing:
|
||||
sys.stderr.write(f"TAP pool: {total - len(missing)}/{total} present "
|
||||
f"(missing: {', '.join(missing)})\n")
|
||||
ok = False
|
||||
else:
|
||||
sys.stderr.write(f"TAP pool: {total}/{total} present\n")
|
||||
if not ok:
|
||||
sys.stderr.write("\nRun: ./cli.py firecracker setup\n")
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
def cmd_firecracker(args: list[str]) -> int:
|
||||
sub = args[0] if args else ""
|
||||
if sub == "setup":
|
||||
return _cmd_setup()
|
||||
if sub == "status":
|
||||
return _cmd_status()
|
||||
sys.stderr.write("usage: firecracker {setup|status}\n")
|
||||
return 2
|
||||
Reference in New Issue
Block a user