3b418580a9
Addresses PR #78 review feedback: - New `has_backend(name)` on the backend package + abstract `BottleBackend.is_available()` on each concrete subclass. Replaces inline `shutil.which("docker") is None` checks in docker/cleanup.py:178 and smolmachines/enumerate.py:73. Docker → `shutil.which("docker") is not None`; smolmachines → `smolvm.is_available()`. Cross-backend `enumerate_active_ agents()` skips backends whose `is_available()` is False so a docker-only host doesn't fail when iterating past smolmachines (and vice versa). - Move docker `enumerate_active` + parser helpers out of cleanup.py into a new `backend/docker/enumerate.py`, mirroring the smolmachines/enumerate.py layout. cleanup.py is now purely about prepare_cleanup / cleanup; the active-listing concern owns its own file. - Drop the `ActiveAgent = ActiveBottle` alias in dashboard.py. The canonical name is `ActiveAgent` (the thing running inside a bottle is always called "agent" in this codebase; the bottle is the container). Renamed `enumerate_active_bottles` → `enumerate_active_agents` to match. Tests: - `test_backend_selection.TestEnumerateActiveAgents .test_skips_unavailable_backends` locks down the `is_available()`-gated iteration. - New `TestHasBackend` covers `has_backend("docker")` consulting the backend's `is_available`, and unknown-name → False. - Existing tests follow the rename; the docker-availability- side-effect test in `test_docker_enumerate_active` moves up to the cross-backend layer (where the gate lives now). 607 unit tests pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
88 lines
2.8 KiB
Python
88 lines
2.8 KiB
Python
"""SmolmachinesBottleBackend — the smolmachines implementation of
|
|
BottleBackend (PRD 0023)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from contextlib import contextmanager
|
|
from pathlib import Path
|
|
from typing import Generator, Sequence
|
|
|
|
from .. import ActiveAgent, BottleBackend, BottleSpec
|
|
from . import enumerate as _enumerate
|
|
from . import launch as _launch
|
|
from . import prepare as _prepare
|
|
from . import smolvm as _smolvm
|
|
from .bottle import SmolmachinesBottle
|
|
from .bottle_cleanup_plan import SmolmachinesBottleCleanupPlan
|
|
from .bottle_plan import SmolmachinesBottlePlan
|
|
from .provision import ca as _ca
|
|
from .provision import git as _git
|
|
from .provision import prompt as _prompt
|
|
from .provision import skills as _skills
|
|
from .provision import supervise as _supervise
|
|
|
|
|
|
class SmolmachinesBottleBackend(
|
|
BottleBackend["SmolmachinesBottlePlan", "SmolmachinesBottleCleanupPlan"]
|
|
):
|
|
"""smolmachines backend. Selected by
|
|
`CLAUDE_BOTTLE_BACKEND=smolmachines`."""
|
|
|
|
name = "smolmachines"
|
|
|
|
@classmethod
|
|
def is_available(cls) -> bool:
|
|
"""`smolvm` on PATH. The backend additionally needs macOS
|
|
for libkrun + TSI, but `enumerate_active` / `cleanup` are
|
|
host-shell ops that gracefully no-op on Linux too — the
|
|
runtime check happens at `prepare`."""
|
|
return _smolvm.is_available()
|
|
|
|
def _resolve_plan(
|
|
self, spec: BottleSpec, *, stage_dir: Path
|
|
) -> SmolmachinesBottlePlan:
|
|
return _prepare.resolve_plan(spec, stage_dir=stage_dir)
|
|
|
|
@contextmanager
|
|
def launch(
|
|
self, plan: SmolmachinesBottlePlan
|
|
) -> Generator[SmolmachinesBottle, None, None]:
|
|
with _launch.launch(plan, provision=self.provision) as bottle:
|
|
yield bottle
|
|
|
|
def provision_ca(
|
|
self, plan: SmolmachinesBottlePlan, target: str
|
|
) -> None:
|
|
_ca.provision_ca(plan, target)
|
|
|
|
def provision_prompt(
|
|
self, plan: SmolmachinesBottlePlan, target: str
|
|
) -> str | None:
|
|
return _prompt.provision_prompt(plan, target)
|
|
|
|
def provision_skills(
|
|
self, plan: SmolmachinesBottlePlan, target: str
|
|
) -> None:
|
|
_skills.provision_skills(plan, target)
|
|
|
|
def provision_git(
|
|
self, plan: SmolmachinesBottlePlan, target: str
|
|
) -> None:
|
|
_git.provision_git(plan, target)
|
|
|
|
def provision_supervise(
|
|
self, plan: SmolmachinesBottlePlan, target: str
|
|
) -> None:
|
|
_supervise.provision_supervise(plan, target)
|
|
|
|
def prepare_cleanup(self) -> SmolmachinesBottleCleanupPlan:
|
|
return SmolmachinesBottleCleanupPlan()
|
|
|
|
def cleanup(self, plan: SmolmachinesBottleCleanupPlan) -> None:
|
|
del plan
|
|
# Nothing to clean in chunks 1-3 — see
|
|
# SmolmachinesBottleCleanupPlan docstring.
|
|
|
|
def enumerate_active(self) -> Sequence[ActiveAgent]:
|
|
return _enumerate.enumerate_active()
|