refactor(backend): base owns the gateway-attach flow, fail hard (0081 review)
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>
This commit is contained in:
@@ -14,6 +14,7 @@ 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
|
||||
@@ -25,7 +26,7 @@ from .bottle_plan import MacosContainerBottlePlan
|
||||
|
||||
|
||||
class MacosContainerBottleBackend(
|
||||
BottleBackend["MacosContainerBottlePlan", "MacosContainerBottleCleanupPlan"]
|
||||
BottleBackend["MacosContainerBottlePlan", "MacosContainerBottleCleanupPlan", str]
|
||||
):
|
||||
"""Apple Container backend. Selected by
|
||||
`BOT_BOTTLE_BACKEND=macos-container` or
|
||||
@@ -117,9 +118,19 @@ class MacosContainerBottleBackend(
|
||||
def enumerate_active(self) -> Sequence[ActiveAgent]:
|
||||
return _enumerate.enumerate_active()
|
||||
|
||||
def attach_bottled_agents_to_gateway(self) -> None:
|
||||
from .infra_launch import attach_bottled_agents_to_gateway
|
||||
attach_bottled_agents_to_gateway()
|
||||
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
|
||||
|
||||
@@ -97,11 +97,23 @@ def ensure_gateway(
|
||||
return endpoint
|
||||
|
||||
|
||||
def _push_ca_to_container(name: str, ca_pem: str) -> None:
|
||||
"""Replace one running agent container's trusted gateway CA with `ca_pem`
|
||||
and rebuild its trust store via `container cp` + `container exec`.
|
||||
Unconditional install — there is one gateway, so no fingerprint match is
|
||||
needed. Raises `InfraLaunchError` on failure."""
|
||||
def running_agent_containers() -> list[str]:
|
||||
"""Every running agent container's name — the bottles the bring-up reconcile
|
||||
attaches to the fresh gateway (PRD 0081). Raises `EnumerationError` if the
|
||||
live set can't be determined (fail hard rather than reconcile a partial
|
||||
set)."""
|
||||
return [f"{CONTAINER_NAME_PREFIX}{agent.slug}" for agent in enumerate_active()]
|
||||
|
||||
|
||||
def push_ca_to_container(name: str, ca_pem: str) -> None:
|
||||
"""(Re)attach one running agent container to the current gateway: replace
|
||||
its trusted gateway CA with `ca_pem` and rebuild its trust store via
|
||||
`container cp` + `container exec` (PRD 0081). Unconditional install — there
|
||||
is one gateway, so no fingerprint match is needed.
|
||||
|
||||
Fail hard: raises `InfraLaunchError` (naming the container) on any failure —
|
||||
the base reconcile aggregates and surfaces it rather than leaving the agent
|
||||
silently unable to reach the gateway."""
|
||||
mkdir = container_mod.run_container_argv(
|
||||
["container", "exec", name, "mkdir", "-p",
|
||||
"/usr/local/share/ca-certificates"]
|
||||
@@ -131,32 +143,6 @@ def _push_ca_to_container(name: str, ca_pem: str) -> None:
|
||||
)
|
||||
|
||||
|
||||
def attach_bottled_agents_to_gateway(ca_pem: str | None = None) -> None:
|
||||
"""Reconcile every running agent container against the freshly-(re)created
|
||||
gateway (PRD 0081): replace each agent's trusted CA with the current gateway
|
||||
CA, so a gateway rebuild doesn't silently break their TLS egress (#510).
|
||||
|
||||
Per-container steps are best-effort: one failure is logged and skipped so it
|
||||
never blocks the others or the gateway coming up."""
|
||||
if ca_pem is None:
|
||||
ca_pem = MacosInfraService().ca_cert_pem()
|
||||
try:
|
||||
agents = list(enumerate_active())
|
||||
except (EnumerationError, OSError) as exc:
|
||||
info(f"CA reconcile skipped: {exc}")
|
||||
return
|
||||
reconciled = 0
|
||||
for agent in agents:
|
||||
name = f"{CONTAINER_NAME_PREFIX}{agent.slug}"
|
||||
try:
|
||||
_push_ca_to_container(name, ca_pem)
|
||||
reconciled += 1
|
||||
except (OSError, InfraLaunchError) as exc:
|
||||
info(f"CA reconcile skipped for {name}: {exc}")
|
||||
if reconciled:
|
||||
info(f"reconciled gateway CA into {reconciled} running macOS bottle(s)")
|
||||
|
||||
|
||||
def _reprovision_running_bottles(endpoint: GatewayEndpoint) -> None:
|
||||
"""Recover keys from live Apple containers and restore gateway tokens."""
|
||||
try:
|
||||
@@ -259,7 +245,8 @@ __all__ = [
|
||||
"GatewayEndpoint",
|
||||
"LaunchContext",
|
||||
"ensure_gateway",
|
||||
"attach_bottled_agents_to_gateway",
|
||||
"running_agent_containers",
|
||||
"push_ca_to_container",
|
||||
"live_source_ips",
|
||||
"register_agent",
|
||||
"deprovision_consolidated",
|
||||
|
||||
Reference in New Issue
Block a user