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
240 lines
8.3 KiB
Python
240 lines
8.3 KiB
Python
"""Unit: backend selection + cross-backend enumeration (issue #77).
|
|
|
|
`get_bottle_backend(name)` resolves a backend by explicit name,
|
|
env var, or default. `enumerate_active_agents()` walks every
|
|
registered backend and concatenates their `ActiveAgent`
|
|
listings — the CLI and dashboard both go through this so adding
|
|
a backend lights it up in both places."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
from bot_bottle import backend as backend_mod
|
|
from bot_bottle.backend import (
|
|
ActiveAgent,
|
|
enumerate_active_agents,
|
|
get_bottle_backend,
|
|
known_backend_names,
|
|
)
|
|
|
|
|
|
class TestGetBottleBackend(unittest.TestCase):
|
|
def test_explicit_name_wins_over_env(self):
|
|
with patch.dict(os.environ, {"BOT_BOTTLE_BACKEND": "smolmachines"}):
|
|
b = get_bottle_backend("docker")
|
|
self.assertEqual("docker", b.name)
|
|
|
|
def test_env_var_fallback(self):
|
|
with patch.dict(os.environ, {"BOT_BOTTLE_BACKEND": "smolmachines"}):
|
|
b = get_bottle_backend()
|
|
self.assertEqual("smolmachines", b.name)
|
|
|
|
def test_default_macos_container_when_available(self):
|
|
class _FakeBackend:
|
|
name = "macos-container"
|
|
|
|
def is_available(self) -> bool:
|
|
return True
|
|
|
|
with patch.dict(os.environ, {}, clear=True), \
|
|
patch.object(backend_mod, "_BACKENDS", {
|
|
"macos-container": _FakeBackend(),
|
|
"smolmachines": _FakeBackend(),
|
|
}):
|
|
b = get_bottle_backend()
|
|
self.assertEqual("macos-container", b.name)
|
|
|
|
def test_default_smolmachines_when_macos_container_unavailable(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),
|
|
"smolmachines": _FakeBackend("smolmachines", False),
|
|
}):
|
|
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):
|
|
get_bottle_backend("nonexistent")
|
|
|
|
|
|
class TestKnownBackendNames(unittest.TestCase):
|
|
def test_returns_backends_sorted(self):
|
|
self.assertEqual(
|
|
("docker", "firecracker", "macos-container", "smolmachines"),
|
|
known_backend_names(),
|
|
)
|
|
|
|
|
|
class TestEnumerateActiveAgents(unittest.TestCase):
|
|
"""Combines each backend's `enumerate_active`. Each backend's
|
|
implementation has its own tests (`test_docker_enumerate_active`,
|
|
`test_smolmachines_*`); this just asserts the aggregator stitches
|
|
them together."""
|
|
|
|
def test_concatenates_per_backend(self):
|
|
a = ActiveAgent(
|
|
backend_name="docker", slug="a-1", agent_name="impl",
|
|
started_at="", services=("egress",),
|
|
)
|
|
b = ActiveAgent(
|
|
backend_name="smolmachines", slug="b-2", agent_name="research",
|
|
started_at="", services=(),
|
|
)
|
|
|
|
class _FakeBackend:
|
|
def __init__(self, items: object, available: object = True) -> None: # type: ignore
|
|
self._items = items
|
|
self._available = available
|
|
|
|
def is_available(self):
|
|
return self._available
|
|
|
|
def enumerate_active(self):
|
|
return self._items
|
|
|
|
with patch.object(
|
|
backend_mod, "_BACKENDS",
|
|
{"docker": _FakeBackend([a]), "smolmachines": _FakeBackend([b])},
|
|
):
|
|
self.assertEqual([a, b], enumerate_active_agents())
|
|
|
|
def test_sorts_by_started_at_then_slug_across_backends(self):
|
|
newer = ActiveAgent(
|
|
backend_name="docker", slug="docker-new", agent_name="impl",
|
|
started_at="2026-06-02T12:00:00Z", services=(),
|
|
)
|
|
tie_b = ActiveAgent(
|
|
backend_name="docker", slug="b-slug", agent_name="review",
|
|
started_at="2026-06-02T11:00:00Z", services=(),
|
|
)
|
|
missing_metadata = ActiveAgent(
|
|
backend_name="smolmachines", slug="missing-metadata",
|
|
agent_name="?", started_at="", services=(),
|
|
)
|
|
tie_a = ActiveAgent(
|
|
backend_name="smolmachines", slug="a-slug", agent_name="research",
|
|
started_at="2026-06-02T11:00:00Z", services=(),
|
|
)
|
|
|
|
class _FakeBackend:
|
|
def __init__(self, items: object) -> None: # type: ignore
|
|
self._items = items
|
|
|
|
def is_available(self) -> bool:
|
|
return True
|
|
|
|
def enumerate_active(self) -> object:
|
|
return self._items
|
|
|
|
with patch.object(
|
|
backend_mod, "_BACKENDS",
|
|
{
|
|
"docker": _FakeBackend([newer, tie_b]),
|
|
"smolmachines": _FakeBackend([missing_metadata, tie_a]),
|
|
},
|
|
):
|
|
self.assertEqual(
|
|
[missing_metadata, tie_a, tie_b, newer],
|
|
enumerate_active_agents(),
|
|
)
|
|
|
|
def test_empty_when_no_backends_have_active(self):
|
|
class _FakeBackend:
|
|
def is_available(self):
|
|
return True
|
|
|
|
def enumerate_active(self):
|
|
return []
|
|
|
|
with patch.object(
|
|
backend_mod, "_BACKENDS",
|
|
{"docker": _FakeBackend(), "smolmachines": _FakeBackend()},
|
|
):
|
|
self.assertEqual([], enumerate_active_agents())
|
|
|
|
def test_skips_unavailable_backends(self):
|
|
# If a backend's runtime isn't installed (smolvm missing on
|
|
# a docker-only host, or docker missing on a smolmachines-
|
|
# only host), the cross-backend enumerator skips it rather
|
|
# than dying — `has_backend` gates the iteration.
|
|
present = ActiveAgent(
|
|
backend_name="docker", slug="a-1", agent_name="impl",
|
|
started_at="", services=(),
|
|
)
|
|
hidden = ActiveAgent(
|
|
backend_name="smolmachines", slug="x", agent_name="x",
|
|
started_at="", services=(),
|
|
)
|
|
|
|
class _FakeBackend:
|
|
def __init__(self, items: object, available: object) -> None: # type: ignore
|
|
self._items = items
|
|
self._available = available
|
|
|
|
def is_available(self) -> object:
|
|
return self._available
|
|
|
|
def enumerate_active(self):
|
|
return self._items
|
|
|
|
with patch.object(
|
|
backend_mod, "_BACKENDS",
|
|
{
|
|
"docker": _FakeBackend([present], available=True),
|
|
"smolmachines": _FakeBackend([hidden], available=False),
|
|
},
|
|
):
|
|
self.assertEqual([present], enumerate_active_agents())
|
|
|
|
|
|
class TestHasBackend(unittest.TestCase):
|
|
def test_known_backend_consults_is_available(self):
|
|
class _FakeBackend:
|
|
def is_available(self):
|
|
return False
|
|
|
|
with patch.object(
|
|
backend_mod, "_BACKENDS", {"docker": _FakeBackend()},
|
|
):
|
|
from bot_bottle.backend import has_backend
|
|
self.assertFalse(has_backend("docker"))
|
|
|
|
def test_unknown_backend_returns_false(self):
|
|
from bot_bottle.backend import has_backend
|
|
self.assertFalse(has_backend("nonexistent"))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|