refactor(orchestrator): macOS orchestrator under the Orchestrator ABC

Extract the Apple-Container control-plane lifecycle out of `MacosInfraService`
into `MacosOrchestrator` (backend/macos_container/orchestrator.py): the
container run on the host-only control network, the container-only DB volume,
the source-hash recreate gate, health polling against the resolved
control-network address, and `probe_orchestrator_url`. `url()` == `gateway_url()`
here (Apple has no container DNS, so one resolved address serves the CLI and the
gateway).

`MacosInfraService` now composes `orchestrator()` + `gateway()`: ensure the
networks, build both images (each service self-builds via `ensure_built` — added
to `MacosGateway` too), bring the orchestrator up first, then connect the gateway
with the orchestrator-minted token. `ORCHESTRATOR_IMAGE` + the DB-volume constant
move to the orchestrator module (with back-compat aliases where imported).

Container-lifecycle tests split into test_macos_orchestrator; test_macos_infra
now covers the composition.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 14:44:21 -04:00
parent cc4e29c3da
commit 5465379654
5 changed files with 486 additions and 301 deletions
+13 -7
View File
@@ -14,6 +14,7 @@ share: the network names, the gateway image, and the network-creation helper.
from __future__ import annotations
import os
from pathlib import Path
from ...gateway import (
DEFAULT_CA_TIMEOUT_SECONDS,
@@ -50,9 +51,8 @@ GATEWAY_LABEL = "bot-bottle-mac-infra=1"
GATEWAY_DAEMONS = "egress,git-http,supervise"
GATEWAY_IMAGE = os.environ.get("BOT_BOTTLE_GATEWAY_IMAGE", "bot-bottle-gateway:latest")
ORCHESTRATOR_IMAGE = os.environ.get(
"BOT_BOTTLE_ORCHESTRATOR_IMAGE", "bot-bottle-orchestrator:latest"
)
_REPO_ROOT = Path(__file__).resolve().parents[3]
def ensure_networks(
@@ -72,9 +72,9 @@ class MacosGateway(Gateway):
"""The consolidated gateway as a single Apple container, triple-homed on the
NAT egress, host-only agent, and control networks.
Images are built by the infra service (`ensure_built` here is the ABC's
no-op); the networks are ensured there too. `connect_to_orchestrator` runs
the container carrying the mitmproxy CA + the pre-minted `gateway` token."""
`ensure_built` builds `Dockerfile.gateway`; the networks are ensured by the
composer. `connect_to_orchestrator` runs the container carrying the mitmproxy
CA + the pre-minted `gateway` token."""
def __init__(
self,
@@ -84,18 +84,25 @@ class MacosGateway(Gateway):
network: str = GATEWAY_NETWORK,
egress_network: str = GATEWAY_EGRESS_NETWORK,
control_network: str = CONTROL_NETWORK,
repo_root: Path = _REPO_ROOT,
) -> None:
self.image_ref = image_ref
self.name = name
self.network = network
self.egress_network = egress_network
self.control_network = control_network
self._repo_root = repo_root
# Set by `connect_to_orchestrator`: the URL the daemons resolve policy
# against + the pre-minted `gateway` token they present. The gateway
# never mints, so it never holds the signing key (#469).
self._orchestrator_url = ""
self._gateway_token = ""
def ensure_built(self) -> None:
"""Build the data-plane image from `Dockerfile.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:
"""Bind the gateway to this orchestrator and (re)start it, dual-homed on
the agent + control networks, resolving policy against `orchestrator_url`
@@ -198,7 +205,6 @@ __all__ = [
"GATEWAY_LABEL",
"GATEWAY_DAEMONS",
"GATEWAY_IMAGE",
"ORCHESTRATOR_IMAGE",
"GatewayError",
"DEFAULT_CA_TIMEOUT_SECONDS",
"ensure_networks",