3c0d2fb66f
Addresses review on #519 (@didericis 6143 + the fail-hard direction over the codex skip). Base owns the flow (6143 / ADR 0006). `attach_bottled_agents_to_gateway()` is now a concrete method on `BottleBackend` that delegates to `gateway_attach.reconcile_running_bottles`; backends override only three primitives — `_gateway_attach_resources()`, `_running_bottles()`, `_attach_bottle_to_gateway()`. The shared control flow + error policy live in one place so a backend can't drift onto a bespoke loop or a silent skip. Keeps the reconcile flow + `GatewayAttachResources` out of `backend/base.py` (the size guardrail) in a dedicated `backend/gateway_attach.py`. New ADR 0006 records the "shared behaviour in the base backend, subclasses override primitives" theme. Fail hard, never skip (the maintainer's direction over the codex review's skip). Any attach failure now aborts bring-up instead of being logged and skipped: a bottle that silently can't reach the fresh gateway (its egress just starts failing TLS) is worse than a loud failure. Every bottle is attempted and the failures are raised together (aggregate `InfraLaunchError`) so one bring-up surfaces the full blast radius. Resource gathering (the CA fetch) also fails hard. PRD 0081 goal + design updated to match (reversed from the earlier "tolerate per-bottle failures" draft). Bumps the base.py guardrail cap 580->600 for the new contract surface (the delegator + 3 abstract primitives); the flow itself lives in gateway_attach.py. Refs #516, #519. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
142 lines
4.7 KiB
Python
142 lines
4.7 KiB
Python
"""FirecrackerBottleBackend — Linux microVM implementation.
|
|
|
|
Replaces smolmachines on Linux (issue #342): mature KVM-based
|
|
isolation via Firecracker, SSH control over a point-to-point TAP, and a
|
|
fail-closed nftables egress boundary. Selected by
|
|
`BOT_BOTTLE_BACKEND=firecracker` or `--backend=firecracker`.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import io
|
|
from contextlib import contextmanager, redirect_stderr
|
|
from pathlib import Path
|
|
from typing import Generator, Sequence
|
|
|
|
from ...agent_provider import AgentProvisionPlan
|
|
from ...egress import EgressPlan
|
|
from ...env import ResolvedEnv
|
|
from ...git_gate import GitGatePlan
|
|
from ...manifest import Manifest
|
|
from ...supervisor.plan import SupervisePlan
|
|
from .. import ActiveAgent, BottleBackend, BottleImages, BottleSpec
|
|
from ..gateway_attach import GatewayAttachResources
|
|
from . import cleanup as _cleanup
|
|
from . import enumerate as _enumerate
|
|
from . import launch as _launch
|
|
from . import resolve_plan as _resolve_plan
|
|
from . import util as _util
|
|
from .bottle import FirecrackerBottle
|
|
from .bottle_cleanup_plan import FirecrackerBottleCleanupPlan
|
|
from .bottle_plan import FirecrackerBottlePlan
|
|
|
|
|
|
class FirecrackerBottleBackend(
|
|
BottleBackend["FirecrackerBottlePlan", "FirecrackerBottleCleanupPlan", "Path"]
|
|
):
|
|
name = "firecracker"
|
|
|
|
@classmethod
|
|
def is_available(cls) -> bool:
|
|
return _util.is_available()
|
|
|
|
@classmethod
|
|
def is_host_capable(cls) -> bool:
|
|
"""Linux + KVM, regardless of whether the `firecracker` binary
|
|
is installed. Drives default-backend selection so a capable host
|
|
without the binary still lands on firecracker and gets an
|
|
install pointer at launch."""
|
|
return _util.is_host_capable()
|
|
|
|
@classmethod
|
|
def setup(cls) -> int:
|
|
from . import setup as _setup
|
|
return _setup.setup()
|
|
|
|
@classmethod
|
|
def status(cls, *, quiet: bool = False) -> int:
|
|
from . import setup as _setup
|
|
if quiet:
|
|
with redirect_stderr(io.StringIO()):
|
|
return _setup.status()
|
|
return _setup.status()
|
|
|
|
@classmethod
|
|
def teardown(cls) -> int:
|
|
from . import setup as _setup
|
|
return _setup.teardown()
|
|
|
|
def _preflight(self) -> None:
|
|
_resolve_plan.preflight()
|
|
|
|
def _build_guest_env(self, resolved_env: ResolvedEnv) -> dict[str, str]:
|
|
return _resolve_plan.build_guest_env(resolved_env)
|
|
|
|
def _resolve_plan(
|
|
self,
|
|
spec: BottleSpec,
|
|
*,
|
|
manifest: Manifest,
|
|
slug: str,
|
|
resolved_env: ResolvedEnv,
|
|
agent_provision_plan: AgentProvisionPlan,
|
|
egress_plan: EgressPlan,
|
|
git_gate_plan: GitGatePlan,
|
|
supervise_plan: SupervisePlan | None,
|
|
stage_dir: Path,
|
|
) -> FirecrackerBottlePlan:
|
|
return _resolve_plan.resolve_plan(
|
|
spec,
|
|
manifest=manifest,
|
|
slug=slug,
|
|
resolved_env=resolved_env,
|
|
agent_provision_plan=agent_provision_plan,
|
|
egress_plan=egress_plan,
|
|
supervise_plan=supervise_plan,
|
|
git_gate_plan=git_gate_plan,
|
|
stage_dir=stage_dir,
|
|
)
|
|
|
|
def _build_or_load_images(self, plan: FirecrackerBottlePlan) -> BottleImages:
|
|
return BottleImages(agent=_launch.build_or_load_agent_base(plan))
|
|
|
|
def prelaunch_checks(self, plan: FirecrackerBottlePlan) -> None:
|
|
_launch.stale_checks(plan)
|
|
|
|
@contextmanager
|
|
def _launch_impl(
|
|
self, plan: FirecrackerBottlePlan, images: BottleImages,
|
|
) -> Generator[FirecrackerBottle, None, None]:
|
|
assert isinstance(images.agent, Path)
|
|
with _launch.launch(plan, images.agent, provision=self.provision) as bottle:
|
|
yield bottle
|
|
|
|
def prepare_cleanup(self) -> FirecrackerBottleCleanupPlan:
|
|
return _cleanup.prepare_cleanup()
|
|
|
|
def cleanup(self, plan: FirecrackerBottleCleanupPlan) -> None:
|
|
_cleanup.cleanup(plan)
|
|
|
|
def enumerate_active(self) -> Sequence[ActiveAgent]:
|
|
return _enumerate.enumerate_active()
|
|
|
|
def _gateway_attach_resources(self) -> GatewayAttachResources:
|
|
from .gateway import FirecrackerGateway
|
|
return GatewayAttachResources(ca_pem=FirecrackerGateway().ca_cert_pem())
|
|
|
|
def _running_bottles(self) -> Sequence[Path]:
|
|
return _cleanup.live_run_dirs()
|
|
|
|
def _attach_bottle_to_gateway(
|
|
self, bottle: Path, resources: GatewayAttachResources,
|
|
) -> None:
|
|
from .infra_launch import attach_ca_to_agent_vm
|
|
attach_ca_to_agent_vm(bottle, resources.ca_pem)
|
|
|
|
def supervise_mcp_url(self, plan: FirecrackerBottlePlan) -> str:
|
|
return plan.agent_supervise_url
|
|
|
|
def ensure_orchestrator(self) -> str:
|
|
from .infra import FirecrackerInfraService
|
|
return FirecrackerInfraService().ensure_running()
|