feat(backend): reconcile running bottles' CA on gateway bring-up (0081)

Add `attach_bottled_agents_to_gateway()` as an `@abc.abstractmethod` on
`BottleBackend` and implement the first reconcile step across all three
backends: on a gateway cold boot, replace every already-running agent's
trusted CA with the freshly-minted gateway CA. Reconciling running bottles
is a backend responsibility (only the backend can enumerate its agents and
reach them — firecracker over SSH, docker/macOS over exec/cp), so making it
an abstract method keeps the fix cross-backend by construction.

The gateway rootfs is ephemeral, so a rebuild mints a new CA that every
running bottle distrusts (SSL verification fails — #510). This reconcile is
what distributes the fresh CA, so a routine gateway rebuild now doubles as a
free CA rotation. Per-bottle steps are best-effort: one unreachable or
malformed bottle is logged and skipped, never blocking the others.

Trigger — `Gateway.connect_to_orchestrator` now returns a cold-boot bool
(True when it actually (re)brought the gateway up, False when a healthy
current gateway was left untouched); each infra `ensure_running` gates the
reconcile on it, so it fires exactly on cold boot and never on an adopt.

Git-gate re-provision and egress-token restore fold into this same reconcile
on the same trigger in follow-up PRs (#516).

Refs #516. Addresses #510.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-26 23:54:27 +00:00
committed by didericis
parent 314b30c013
commit 36f594b770
21 changed files with 488 additions and 15 deletions
@@ -104,11 +104,15 @@ class MacosGateway(Gateway):
container_mod.build_image(
self.image_ref, str(self._repo_root), dockerfile="Dockerfile.gateway")
def connect_to_orchestrator(self, orchestrator_url: str, gateway_token: str) -> None:
def connect_to_orchestrator(self, orchestrator_url: str, gateway_token: str) -> bool:
"""Bind the gateway to this orchestrator and (re)start it, dual-homed on
the agent + control networks, resolving policy against `orchestrator_url`
(the orchestrator's control-network address — Apple has no container
DNS) and presenting `gateway_token`."""
DNS) and presenting `gateway_token`.
Always returns True: this recreates the gateway container unconditionally
(a cold boot), so the caller reconciles running bottles against it
(PRD 0081)."""
self._orchestrator_url = orchestrator_url
self._gateway_token = gateway_token
# Fail closed on a missing policy source or token: the resolver-only data
@@ -156,6 +160,7 @@ class MacosGateway(Gateway):
f"gateway container failed to start: "
f"{(result.stderr or '').strip() or '<no stderr>'}"
)
return True
def is_running(self) -> bool:
return container_mod.container_is_running(self.name)