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
@@ -20,13 +20,15 @@ IMAGE = "busybox"
class TestDockerGatewayIntegration(unittest.TestCase):
def setUp(self) -> None:
self.name = "bot-bottle-orch-gateway-itest-" + secrets.token_hex(4)
# Resolver-only data plane (PRD 0070) requires an orchestrator URL to
# run; busybox never dials it, so a placeholder is enough here.
self.sc = DockerGateway(
IMAGE, name=self.name, orchestrator_url="http://orchestrator:9000",
)
self.sc = DockerGateway(IMAGE, name=self.name)
self.addCleanup(self.sc.stop)
def _connect(self) -> None:
# Resolver-only data plane (PRD 0070) requires an orchestrator URL + a
# pre-minted `gateway` token to run; busybox never dials the orchestrator
# or presents the token, so placeholders are enough here.
self.sc.connect_to_orchestrator("http://orchestrator:9000", "placeholder-token")
def _count(self) -> int:
proc = subprocess.run(
["docker", "ps", "-a", "--filter", f"name=^{self.name}$",
@@ -36,10 +38,10 @@ class TestDockerGatewayIntegration(unittest.TestCase):
return sum(1 for n in proc.stdout.split() if n == self.name)
def test_ensure_is_idempotent_singleton(self) -> None:
self.sc.ensure_running()
self._connect()
self.assertEqual(1, self._count())
# A second ensure must not spawn a second container — one per host.
self.sc.ensure_running()
# A second connect must not spawn a second container — one per host.
self._connect()
self.assertEqual(1, self._count())
self.sc.stop()
self.assertEqual(0, self._count())