"""The shared gateway-attach reconcile flow (PRD 0081). `BottleBackend.attach_bottled_agents_to_gateway()` delegates here so every backend reconciles running bottles against a freshly-booted gateway *the same way* — the control flow + fail-hard policy live in one place and backends override only the primitives (see ADR 0006). Kept out of `backend/base.py` so the backend contract module stays lean (the base.py size guardrail). """ from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING, Any from .base import InfraLaunchError if TYPE_CHECKING: from .base import BottleBackend @dataclass(frozen=True) class GatewayAttachResources: """Everything a running bottle needs to (re)attach to the current gateway on a cold boot (PRD 0081). Gathered once per reconcile and handed to every bottle. The CA now; egress secrets and git-gate state join as the reconcile grows (#516).""" ca_pem: str def reconcile_running_bottles(backend: "BottleBackend[Any, Any, Any]") -> None: """Reconcile every already-running bottle against the freshly-(re)booted gateway: gather the attach resources once, then attach every live bottle. Fail hard, never skip: a bottle that silently can't reach the fresh gateway (its egress just starts failing TLS) is worse than a loud bring-up failure, so any attach failure aborts bring-up. Every bottle is attempted first and the failures are raised together, so one bring-up surfaces the full blast radius rather than one bottle at a time.""" # Enumerate the host's running bottles BEFORE gathering resources: with none # to reattach, the reconcile is a no-op, and gathering resources first would # read the current gateway's CA for nothing. That read targets the per-host # singleton gateway, so a bring-up of a *non-default* instance with no host # bottles (an isolated integration test's `-itest-` gateway) would otherwise # fail looking for the default `bot-bottle-orch-gateway` (#519 review). bottles = backend._running_bottles() if not bottles: return resources = backend._gateway_attach_resources() failures: list[str] = [] for bottle in bottles: try: backend._attach_bottle_to_gateway(bottle, resources) except InfraLaunchError as exc: failures.append(str(exc)) if failures: raise InfraLaunchError( f"could not attach {len(failures)} running bottle(s) to the " f"freshly-booted gateway:\n " + "\n ".join(failures) ) __all__ = ["GatewayAttachResources", "reconcile_running_bottles"]