c276f7b0b1
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
64 lines
1.9 KiB
Python
64 lines
1.9 KiB
Python
"""Plan type for the Firecracker backend."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
|
|
from ...agent_provider import PromptMode
|
|
from .. import BottlePlan
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class FirecrackerBottlePlan(BottlePlan):
|
|
slug: str
|
|
forwarded_env: dict[str, str] = field(repr=False)
|
|
# Stamped by launch once the sidecar is up and its ports are
|
|
# published on the host-side TAP IP (empty at prepare time).
|
|
agent_proxy_url: str = ""
|
|
agent_git_gate_url: str = ""
|
|
agent_supervise_url: str = ""
|
|
|
|
@property
|
|
def container_name(self) -> str:
|
|
"""Instance name, reused for the sidecar container + VM run dir.
|
|
Matches the `bot-bottle-<slug>` convention the other backends
|
|
use so cleanup/enumerate discovery-by-prefix keeps working."""
|
|
return self.agent_provision.instance_name
|
|
|
|
@property
|
|
def image(self) -> str:
|
|
return self.agent_provision.image
|
|
|
|
@property
|
|
def dockerfile_path(self) -> str:
|
|
return self.agent_provision.dockerfile
|
|
|
|
@property
|
|
def prompt_file(self) -> Path:
|
|
return self.agent_provision.prompt_file
|
|
|
|
@property
|
|
def agent_command(self) -> str:
|
|
return self.agent_provision.command
|
|
|
|
@property
|
|
def agent_prompt_mode(self) -> PromptMode:
|
|
return self.agent_provision.prompt_mode
|
|
|
|
@property
|
|
def agent_provider_template(self) -> str:
|
|
return self.agent_provision.template
|
|
|
|
@property
|
|
def git_gate_insteadof_host(self) -> str:
|
|
if self.agent_git_gate_url.startswith("http://"):
|
|
return self.agent_git_gate_url.removeprefix("http://").rstrip("/")
|
|
return super().git_gate_insteadof_host
|
|
|
|
@property
|
|
def git_gate_insteadof_scheme(self) -> str:
|
|
if self.agent_git_gate_url.startswith("http://"):
|
|
return "http"
|
|
return super().git_gate_insteadof_scheme
|