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>
137 lines
4.6 KiB
Python
137 lines
4.6 KiB
Python
"""MacosContainerBottleBackend — Apple Container implementation."""
|
|
|
|
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 ...supervisor.plan import SupervisePlan
|
|
from ...manifest import Manifest
|
|
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 _container
|
|
from .bottle import MacosContainerBottle
|
|
from .bottle_cleanup_plan import MacosContainerBottleCleanupPlan
|
|
from .bottle_plan import MacosContainerBottlePlan
|
|
|
|
|
|
class MacosContainerBottleBackend(
|
|
BottleBackend["MacosContainerBottlePlan", "MacosContainerBottleCleanupPlan", str]
|
|
):
|
|
"""Apple Container backend. Selected by
|
|
`BOT_BOTTLE_BACKEND=macos-container` or
|
|
`--backend=macos-container`."""
|
|
|
|
name = "macos-container"
|
|
supports_nested_containers = True
|
|
|
|
@classmethod
|
|
def is_available(cls) -> bool:
|
|
return _container.is_available()
|
|
|
|
@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,
|
|
) -> MacosContainerBottlePlan:
|
|
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 prelaunch_checks(self, plan: MacosContainerBottlePlan) -> None:
|
|
_launch.stale_checks(plan)
|
|
|
|
def _build_or_load_images(self, plan: MacosContainerBottlePlan) -> BottleImages:
|
|
return _launch.build_or_load_images(plan)
|
|
|
|
@contextmanager
|
|
def _launch_impl(
|
|
self, plan: MacosContainerBottlePlan, images: BottleImages
|
|
) -> Generator[MacosContainerBottle, None, None]:
|
|
with _launch.launch(plan, images, provision=self.provision) as bottle:
|
|
yield bottle
|
|
|
|
def ensure_orchestrator(self) -> str:
|
|
"""Bring up the per-host pair (orchestrator + gateway containers) and
|
|
return its control-plane URL — the on-demand entry point operator tools
|
|
(`supervise`) call when no control plane is running yet. Mirrors
|
|
firecracker's infra bring-up."""
|
|
from .infra import MacosInfraService
|
|
return MacosInfraService().ensure_running()
|
|
|
|
def prepare_cleanup(self) -> MacosContainerBottleCleanupPlan:
|
|
return _cleanup.prepare_cleanup()
|
|
|
|
def cleanup(self, plan: MacosContainerBottleCleanupPlan) -> None:
|
|
_cleanup.cleanup(plan)
|
|
|
|
def enumerate_active(self) -> Sequence[ActiveAgent]:
|
|
return _enumerate.enumerate_active()
|
|
|
|
def _gateway_attach_resources(self) -> GatewayAttachResources:
|
|
from .infra import MacosInfraService
|
|
return GatewayAttachResources(ca_pem=MacosInfraService().ca_cert_pem())
|
|
|
|
def _running_bottles(self) -> Sequence[str]:
|
|
from .infra_launch import running_agent_containers
|
|
return running_agent_containers()
|
|
|
|
def _attach_bottle_to_gateway(
|
|
self, bottle: str, resources: GatewayAttachResources,
|
|
) -> None:
|
|
from .infra_launch import push_ca_to_container
|
|
push_ca_to_container(bottle, resources.ca_pem)
|
|
|
|
def supervise_mcp_url(self, plan: MacosContainerBottlePlan) -> str:
|
|
return plan.agent_supervise_url
|