refactor(gateway): move the data-plane daemons into a bot_bottle.gateway package
test / integration-docker (pull_request) Successful in 11s
test / unit (pull_request) Successful in 43s
lint / lint (push) Successful in 56s
test / integration-firecracker (pull_request) Successful in 3m19s
test / coverage (pull_request) Successful in 19s
test / publish-infra (pull_request) Has been skipped
tracker-policy-pr / check-pr (pull_request) Successful in 7s

Separate the gateway (data plane) from the orchestrator (control plane) at the
module level. The gateway runtime files move out of the package root — and the
backend-neutral Gateway lifecycle ABC + GATEWAY_* constants move out of
orchestrator/ — into a new bot_bottle/gateway/ package:

  gateway/__init__.py      (was orchestrator/gateway.py: Gateway ABC + consts
                            + rotate_gateway_ca)
  gateway/gateway_init.py  (the PID-1 daemon supervisor)
  gateway/egress_addon.py, egress_addon_core.py, egress_dlp_config.py,
          dlp_detectors.py            (the egress mitmproxy daemon)
  gateway/git_http_backend.py         (the git-http daemon)
  gateway/git_gate_render.py          (the git-gate pre-receive rendering)
  gateway/supervise_server.py         (the supervise MCP daemon)
  gateway/policy_resolver.py          (the data-plane control-plane RPC client)

orchestrator/ now holds only control-plane files. The shared plan/types/auth
layer (egress.py=EgressPlan, git_gate.py=GitGatePlan, supervise.py,
supervise_types.py, control_auth.py) and the launch-time git-gate provisioning
helpers stay at root, so orchestrator/ and backend/ still own them.

Because these daemons are invoked as `python3 -m bot_bottle.<name>`, loaded flat
by mitmproxy, and referenced in Dockerfile.gateway, the move updates more than
Python imports: the `-m` invocations (firecracker/macOS infra scripts), the
Dockerfile.gateway addon shim + ENTRYPOINT, gateway_init's _DAEMONS module
paths, and the git-gate CGI heredocs all now point at bot_bottle.gateway.*.

No behavior change; full unit suite green (2251).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-24 13:25:10 -04:00
parent a845cba925
commit f77023db1d
47 changed files with 87 additions and 87 deletions
@@ -20,7 +20,7 @@ from ...docker_cmd import run_docker
from ...egress import EgressPlan
from ...git_gate import GitGatePlan
from ...orchestrator.client import OrchestratorClient
from ...orchestrator.gateway import GATEWAY_NETWORK
from ...gateway import GATEWAY_NETWORK
from ...orchestrator.lifecycle import INFRA_NAME, OrchestratorService
from ...orchestrator.secret_store import ENV_VAR_SECRET_NAME
from ...orchestrator.reprovision import reprovision_bottles
+1 -1
View File
@@ -11,7 +11,7 @@ from ...paths import (
host_control_plane_token,
host_gateway_ca_dir,
)
from ...orchestrator.gateway import (
from ...gateway import (
Gateway, GATEWAY_IMAGE, GATEWAY_NAME, GATEWAY_NETWORK, GATEWAY_DOCKERFILE,
REPO_ROOT, GATEWAY_LABEL, MITMPROXY_HOME, DEFAULT_CA_TIMEOUT_SECONDS,
CA_POLL_SECONDS, GATEWAY_CA_CERT, GatewayError
+1 -1
View File
@@ -11,7 +11,7 @@ from pathlib import Path
from ..bottle_state import egress_state_dir
from ..egress import EGRESS_ROUTES_FILENAME
from ..egress_addon_core import LOG_OFF, load_config
from ..gateway.egress_addon_core import LOG_OFF, load_config
class EgressApplyError(RuntimeError):
+1 -1
View File
@@ -542,7 +542,7 @@ BOT_BOTTLE_ROOT=/var/lib/bot-bottle BOT_BOTTLE_CONTROL_PLANE_TOKEN="$CP_KEY" pyt
BOT_BOTTLE_GATEWAY_DAEMONS=egress,git-http,supervise \\
BOT_BOTTLE_ORCHESTRATOR_URL=http://127.0.0.1:{CONTROL_PLANE_PORT} \\
BOT_BOTTLE_CONTROL_AUTH_JWT="$GW_JWT" \\
python3 -m bot_bottle.gateway_init &
python3 -m bot_bottle.gateway.gateway_init &
# Reap as PID 1; children are backgrounded, so `wait` blocks.
while : ; do wait ; done
@@ -12,7 +12,7 @@ from __future__ import annotations
import os
from ...orchestrator.gateway import GatewayError
from ...gateway import GatewayError
from . import util as container_mod
# The shared host-only network the infra container and every agent bottle sit
+2 -2
View File
@@ -41,7 +41,7 @@ from dataclasses import dataclass
from pathlib import Path
from ... import log
from ...orchestrator.gateway import GATEWAY_CA_CERT, MITMPROXY_HOME
from ...gateway import GATEWAY_CA_CERT, MITMPROXY_HOME
from ...orchestrator.lifecycle import (
DEFAULT_PORT,
DEFAULT_STARTUP_TIMEOUT_SECONDS,
@@ -107,7 +107,7 @@ def _init_script(port: int) -> str:
# control-plane RPC and never opens bot-bottle.db (PRD 0070 / #469).
f"( cd /app && BOT_BOTTLE_GATEWAY_DAEMONS={_GATEWAY_DAEMONS} "
f"BOT_BOTTLE_ORCHESTRATOR_URL=http://127.0.0.1:{port} "
f"python3 -m bot_bottle.gateway_init ) &\n"
f"python3 -m bot_bottle.gateway.gateway_init ) &\n"
"while : ; do wait ; done\n"
)
+1 -1
View File
@@ -52,7 +52,7 @@ from ...git_gate import (
provision_git_gate_dynamic_keys,
revoke_git_gate_provisioned_keys,
)
from ...git_http_backend import DEFAULT_PORT as _GIT_HTTP_PORT
from ...gateway.git_http_backend import DEFAULT_PORT as _GIT_HTTP_PORT
from ...image_cache import check_stale
from ...log import die, info, warn
from .. import BottleImages
+1 -1
View File
@@ -16,7 +16,7 @@ from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING
from .egress_addon_core import (
from .gateway.egress_addon_core import (
ON_MATCH_REDACT,
HeaderMatch as CoreHeaderMatch,
MatchEntry as CoreMatchEntry,
@@ -16,8 +16,8 @@ import typing
from mitmproxy import http # type: ignore[import-not-found] # pylint: disable=import-error
from bot_bottle.constants import IDENTITY_HEADER
from bot_bottle.dlp_detectors import redact_tokens, strip_crlf
from bot_bottle.egress_addon_core import (
from bot_bottle.gateway.dlp_detectors import redact_tokens, strip_crlf
from bot_bottle.gateway.egress_addon_core import (
LOG_BLOCKS,
LOG_FULL,
DEFAULT_OUTBOUND_ON_MATCH,
@@ -40,7 +40,7 @@ from bot_bottle.egress_addon_core import (
scan_inbound,
scan_outbound,
)
from bot_bottle.policy_resolver import PolicyResolveError, PolicyResolver
from bot_bottle.gateway.policy_resolver import PolicyResolveError, PolicyResolver
from bot_bottle.supervise_types import (
STATUS_APPROVED,
STATUS_MODIFIED,
@@ -16,7 +16,7 @@ import re
import typing
from dataclasses import dataclass
from .yaml_subset import YamlSubsetError, parse_yaml_subset
from ..yaml_subset import YamlSubsetError, parse_yaml_subset
# DLP detector-config parsing lives in a sibling module. Re-exported below
# so existing `from egress_addon_core import ON_MATCH_*` callers keep working.
@@ -100,8 +100,8 @@ _DAEMONS: tuple[_DaemonSpec, ...] = (
)),
_DaemonSpec("egress", ("/bin/sh", "/app/egress-entrypoint.sh")),
_DaemonSpec("git-gate", ("/bin/sh", "/git-gate-entrypoint.sh")),
_DaemonSpec("git-http", ("python3", "-m", "bot_bottle.git_http_backend")),
_DaemonSpec("supervise", ("python3", "-m", "bot_bottle.supervise_server")),
_DaemonSpec("git-http", ("python3", "-m", "bot_bottle.gateway.git_http_backend")),
_DaemonSpec("supervise", ("python3", "-m", "bot_bottle.gateway.supervise_server")),
)
@@ -14,8 +14,8 @@ import shlex
from dataclasses import dataclass
from pathlib import Path
from .constants import GIT_GATE_TIMEOUT_SECS, IDENTITY_HEADER
from .manifest import ManifestBottle, ManifestGitEntry
from ..constants import GIT_GATE_TIMEOUT_SECS, IDENTITY_HEADER
from ..manifest import ManifestBottle, ManifestGitEntry
# Short network alias for git-gate inside the gateway. The
# agent's `.gitconfig` insteadOf rewrites resolve through this name.
@@ -282,7 +282,7 @@ from pathlib import Path
# identity_token), resolved server-side, so the proposal lands under the
# calling bottle exactly as a direct write once did.
try:
from bot_bottle.policy_resolver import PolicyResolver, PolicyResolveError
from bot_bottle.gateway.policy_resolver import PolicyResolver, PolicyResolveError
from bot_bottle.supervise_types import TOOL_GITLEAKS_ALLOW
except ImportError:
from policy_resolver import PolicyResolver, PolicyResolveError
@@ -374,7 +374,7 @@ import sys
# Non-blocking poll over the control plane. A decided proposal is archived
# server-side on read, so no separate archive step is needed here.
try:
from bot_bottle.policy_resolver import PolicyResolver, PolicyResolveError
from bot_bottle.gateway.policy_resolver import PolicyResolver, PolicyResolveError
except ImportError:
from policy_resolver import PolicyResolver, PolicyResolveError
@@ -27,7 +27,7 @@ from pathlib import Path
from urllib.parse import urlsplit
from bot_bottle.constants import GIT_GATE_TIMEOUT_SECS, IDENTITY_HEADER
from bot_bottle.policy_resolver import PolicyResolveError, PolicyResolver
from bot_bottle.gateway.policy_resolver import PolicyResolveError, PolicyResolver
DEFAULT_PORT = 9420
@@ -58,10 +58,10 @@ import typing
from dataclasses import dataclass
from bot_bottle.constants import IDENTITY_HEADER
from bot_bottle.egress_addon_core import (
from bot_bottle.gateway.egress_addon_core import (
LOG_OFF, load_config, resolve_client_context, route_to_yaml_dict,
)
from bot_bottle.policy_resolver import PolicyResolveError, PolicyResolver
from bot_bottle.gateway.policy_resolver import PolicyResolveError, PolicyResolver
from bot_bottle import supervise as _sv
+1 -1
View File
@@ -39,7 +39,7 @@ from .manifest import ManifestBottle
# Rendering and the deploy-key lifecycle live in sibling modules; the
# names are re-exported here (see __all__) so existing
# `from bot_bottle.git_gate import …` callers are unchanged.
from .git_gate_render import (
from .gateway.git_gate_render import (
GIT_GATE_HOSTNAME,
GIT_GATE_TIMEOUT_SECS,
GitGateUpstream,
+1 -1
View File
@@ -17,7 +17,7 @@ from .bottle_state import globalize_slug
from .errors import MissingEnvVarError
from .log import info
from .manifest import ManifestBottle, ManifestGitEntry
from .git_gate_render import GitGateUpstream
from .gateway.git_gate_render import GitGateUpstream
if TYPE_CHECKING:
from .git_gate import GitGatePlan
+1 -1
View File
@@ -38,7 +38,7 @@ from .broker import (
verify_request,
)
from .docker_broker import DockerBroker, DockerBrokerError
from .gateway import Gateway, GatewayError
from ..gateway import Gateway, GatewayError
from .service import Orchestrator
from .control_plane import ControlPlaneServer, dispatch, make_server
+1 -1
View File
@@ -31,7 +31,7 @@ from ..paths import (
host_control_plane_token,
host_gateway_ca_dir,
)
from .gateway import (
from ..gateway import (
GATEWAY_DOCKERFILE,
GATEWAY_IMAGE,
GATEWAY_NETWORK,
+1 -1
View File
@@ -25,7 +25,7 @@ from pathlib import Path
from ..docker_cmd import run_docker
from ..paths import host_gateway_ca_dir
from .gateway import GATEWAY_NAME, rotate_gateway_ca
from ..gateway import GATEWAY_NAME, rotate_gateway_ca
from .lifecycle import INFRA_NAME
# The containers whose mitmproxy would still be serving the old CA from memory:
+2 -2
View File
@@ -1,7 +1,7 @@
"""Per-bottle supervise plane (PRD 0013).
The supervise plane is the per-bottle MCP daemon plus its host-side
queue/audit support. The daemon (bot_bottle.supervise_server)
queue/audit support. The daemon (bot_bottle.gateway.supervise_server)
sits on the bottle's internal network and exposes MCP tools the agent
calls when it needs an operator-reviewed egress change:
@@ -18,7 +18,7 @@ the response and returns `{status, notes}` to the agent.
This module defines the host-side library: dataclasses for the queue
record shapes, queue read/write helpers, the audit log writer, and the
diff renderer. The in-gateway daemon lives in
bot_bottle/supervise_server.py; the supervise daemon's container
bot_bottle/gateway/supervise_server.py; the supervise daemon's container
lifecycle is owned by the gateway (PRD 0024).
For 0013 the supervisor's approval handlers are deliberately no-ops: