refactor(gateway): introduce the Gateway service ABC (docker impl)

Replace the docker backend's ad-hoc gateway wiring with the shared `Gateway`
service ABC (in the gateway package), the first of the two per-host service
classes that supersede the per-backend infra glue.

The contract is backend-neutral: `connect_to_orchestrator(url, gateway_token)`
binds the gateway to its control plane and brings it up, `address()` reports
the agent-facing proxy target, `ca_cert_pem()` vends the mitmproxy CA, and
`provisioning_transport()` hands back the exec/cp seam git-gate provisioning
stages per-bottle repos + deploy keys through. `GatewayProvisionError` +
`GatewayTransport` move to the ABC so every backend shares them.

Key trust-model change: the gateway no longer mints its own token. It never
holds the signing key (#469), so the orchestrator mints the role-scoped
`gateway` JWT and hands it in via `connect_to_orchestrator`; the gateway only
injects it. `DockerGateway.ensure_running` becomes `connect_to_orchestrator`
(stashing the URL + token as instance state), and the docker launch flow reads
`address()` / `provisioning_transport()` off the service rather than
re-deriving the container IP + transport itself.

macOS + firecracker gateways move under the ABC in following commits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 12:58:00 -04:00
parent 18d9b81add
commit 05df21f210
9 changed files with 205 additions and 97 deletions
+14 -5
View File
@@ -42,6 +42,7 @@ from ...gateway import (
GATEWAY_NETWORK,
GatewayError,
)
from ...orchestrator_auth import ROLE_GATEWAY, mint
from ...orchestrator.lifecycle import (
DEFAULT_PORT,
DEFAULT_STARTUP_TIMEOUT_SECONDS,
@@ -223,15 +224,17 @@ class DockerInfraService:
f"orchestrator container failed to start: {proc.stderr.strip()}"
)
def _gateway(self) -> DockerGateway:
def gateway(self) -> DockerGateway:
"""The data-plane gateway, dual-homed on the agent network + the control
network, resolving policy against the orchestrator by name."""
network, resolving policy against the orchestrator by name. Cheap to
reconstruct — the launch flow reads its `address()` / provisioning
transport off it, and `ensure_running` connects it to the control
plane."""
return DockerGateway(
self.gateway_image,
name=self._gateway_name,
network=self.network,
control_network=self.control_network,
orchestrator_url=ORCHESTRATOR_URL_IN_NETWORK,
build_context=self._repo_root,
dockerfile=None, # images are built by _build_images
)
@@ -265,8 +268,14 @@ class DockerInfraService:
)
# Bring up (or refresh) the gateway once the control plane it resolves
# against is healthy. `ensure_running` is itself idempotent.
self._gateway().ensure_running()
# against is healthy. The orchestrator (which holds the signing key)
# mints the role-scoped `gateway` JWT here and hands it to the gateway,
# which never sees the key (#469). `connect_to_orchestrator` is
# idempotent.
gateway_token = mint(ROLE_GATEWAY, host_orchestrator_token())
self.gateway().connect_to_orchestrator(
ORCHESTRATOR_URL_IN_NETWORK, gateway_token,
)
return self.url
def stop(self) -> None: