diff --git a/Dockerfile.gateway b/Dockerfile.gateway index 91d0c52..47ff584 100644 --- a/Dockerfile.gateway +++ b/Dockerfile.gateway @@ -8,8 +8,8 @@ # this image's mitmproxy / git / gitleaks payload. # # Collapses the prior per-daemon images (egress, git-gate, -# supervise) into one. A small stdlib-Python init supervisor at -# /app/gateway_init.py spawns all daemons, forwards SIGTERM, and +# supervise) into one. A small stdlib-Python init supervisor +# (`bot_bottle.gateway.bootstrap`) spawns all daemons, forwards SIGTERM, and # propagates per-daemon stdout/stderr to the container log with a # `[name]` prefix. See PRD 0024 for the rationale. # @@ -102,7 +102,7 @@ RUN pip install --no-cache-dir /src/ # WORKDIR here also creates /app so the shim + COPYs below can write into it # (nothing created /app before this point). WORKDIR /app -RUN printf 'from bot_bottle.gateway.egress_addon import addons\n' > /app/egress_addon.py +RUN printf 'from bot_bottle.gateway.egress.addon import addons\n' > /app/egress_addon.py COPY bot_bottle/egress_entrypoint.sh /app/egress-entrypoint.sh RUN chmod +x /app/egress-entrypoint.sh @@ -123,4 +123,4 @@ EXPOSE 8888 9099 9418 9420 9100 # PID 1 is the supervisor. It owns signal handling and exit-code # propagation; no `exec` chain in the entrypoint itself. -ENTRYPOINT ["python3", "-m", "bot_bottle.gateway.gateway_init"] +ENTRYPOINT ["python3", "-m", "bot_bottle.gateway.bootstrap"] diff --git a/bot_bottle/backend/egress_apply.py b/bot_bottle/backend/egress_apply.py index a198107..e174af3 100644 --- a/bot_bottle/backend/egress_apply.py +++ b/bot_bottle/backend/egress_apply.py @@ -11,7 +11,7 @@ from pathlib import Path from ..bottle_state import egress_state_dir from ..egress import EGRESS_ROUTES_FILENAME -from ..gateway.egress_addon_core import LOG_OFF, load_config +from ..gateway.egress.addon_core import LOG_OFF, load_config class EgressApplyError(RuntimeError): diff --git a/bot_bottle/backend/firecracker/infra_vm.py b/bot_bottle/backend/firecracker/infra_vm.py index a7ab12b..1542a60 100644 --- a/bot_bottle/backend/firecracker/infra_vm.py +++ b/bot_bottle/backend/firecracker/infra_vm.py @@ -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.gateway_init & + python3 -m bot_bottle.gateway.bootstrap & # Reap as PID 1; children are backgrounded, so `wait` blocks. while : ; do wait ; done diff --git a/bot_bottle/backend/macos_container/infra.py b/bot_bottle/backend/macos_container/infra.py index 85bc3f0..5bb7f75 100644 --- a/bot_bottle/backend/macos_container/infra.py +++ b/bot_bottle/backend/macos_container/infra.py @@ -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.gateway_init ) &\n" + f"python3 -m bot_bottle.gateway.bootstrap ) &\n" "while : ; do wait ; done\n" ) diff --git a/bot_bottle/backend/macos_container/launch.py b/bot_bottle/backend/macos_container/launch.py index d6e331d..ba34ff9 100644 --- a/bot_bottle/backend/macos_container/launch.py +++ b/bot_bottle/backend/macos_container/launch.py @@ -46,7 +46,7 @@ from ...bottle_state import ( ) from ...egress import Egress from ...git_gate import GitGate -from ...gateway.git_http_backend import DEFAULT_PORT as _GIT_HTTP_PORT +from ...gateway.git_gate.http_backend import DEFAULT_PORT as _GIT_HTTP_PORT from ...image_cache import check_stale from ...log import die, info, warn from .. import BottleImages diff --git a/bot_bottle/egress/__init__.py b/bot_bottle/egress/__init__.py index 2e392e3..37756ca 100644 --- a/bot_bottle/egress/__init__.py +++ b/bot_bottle/egress/__init__.py @@ -13,7 +13,7 @@ Layout: contract). The runtime enforcement (the mitmproxy addon) lives in -`bot_bottle.gateway.egress_addon*`. Public names are re-exported lazily via +`bot_bottle.gateway.egress.addon*`. Public names are re-exported lazily via `__getattr__`, so `from bot_bottle.egress import …` keeps working and importing `egress.plan` (the contract's dependency) stays light. """ diff --git a/bot_bottle/egress/plan.py b/bot_bottle/egress/plan.py index d62f068..ecca024 100644 --- a/bot_bottle/egress/plan.py +++ b/bot_bottle/egress/plan.py @@ -11,7 +11,7 @@ from __future__ import annotations from dataclasses import dataclass from pathlib import Path -from ..gateway.egress_addon_core import Route +from ..gateway.egress.addon_core import Route @dataclass(frozen=True) diff --git a/bot_bottle/egress/service.py b/bot_bottle/egress/service.py index c720bf2..76a8829 100644 --- a/bot_bottle/egress/service.py +++ b/bot_bottle/egress/service.py @@ -4,7 +4,7 @@ routes, render the gateway's `routes.yaml`, assign per-route token slots, and plant the exfil canary. The service also resolves the launch-time token values and the agent/gateway env entries the backend injects. The runtime enforcement -(the mitmproxy addon) lives in `bot_bottle.gateway.egress_addon*`. +(the mitmproxy addon) lives in `bot_bottle.gateway.egress.addon*`. """ from __future__ import annotations @@ -14,7 +14,7 @@ import secrets from pathlib import Path from typing import TYPE_CHECKING -from ..gateway.egress_addon_core import ( +from ..gateway.egress.addon_core import ( ON_MATCH_REDACT, HeaderMatch as CoreHeaderMatch, MatchEntry as CoreMatchEntry, diff --git a/bot_bottle/gateway/gateway_init.py b/bot_bottle/gateway/bootstrap.py similarity index 99% rename from bot_bottle/gateway/gateway_init.py rename to bot_bottle/gateway/bootstrap.py index 1e23be7..cd36179 100644 --- a/bot_bottle/gateway/gateway_init.py +++ b/bot_bottle/gateway/bootstrap.py @@ -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.gateway.git_http_backend")), - _DaemonSpec("supervise", ("python3", "-m", "bot_bottle.gateway.supervise_server")), + _DaemonSpec("git-http", ("python3", "-m", "bot_bottle.gateway.git_gate.http_backend")), + _DaemonSpec("supervise", ("python3", "-m", "bot_bottle.gateway.supervisor.server")), ) diff --git a/bot_bottle/gateway/egress/__init__.py b/bot_bottle/gateway/egress/__init__.py new file mode 100644 index 0000000..e406e60 --- /dev/null +++ b/bot_bottle/gateway/egress/__init__.py @@ -0,0 +1,9 @@ +"""Gateway-side (data-plane) egress service: the mitmproxy addon and its +pure decision core, DLP detectors, and route/DLP config parsing. + +These are the long-running data-plane pieces (loaded by mitmdump inside the +`bot-bottle-gateway` image), distinct from the host-side `bot_bottle.egress` +service that renders routes and prepares env. Import the concrete modules +directly (`from bot_bottle.gateway.egress.addon_core import ...`) — this +package deliberately does no eager work so leaf imports stay cheap. +""" diff --git a/bot_bottle/gateway/egress_addon.py b/bot_bottle/gateway/egress/addon.py similarity index 99% rename from bot_bottle/gateway/egress_addon.py rename to bot_bottle/gateway/egress/addon.py index eda9402..a5292f2 100644 --- a/bot_bottle/gateway/egress_addon.py +++ b/bot_bottle/gateway/egress/addon.py @@ -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.gateway.dlp_detectors import redact_tokens, strip_crlf -from bot_bottle.gateway.egress_addon_core import ( +from bot_bottle.gateway.egress.dlp_detectors import redact_tokens, strip_crlf +from bot_bottle.gateway.egress.addon_core import ( LOG_BLOCKS, LOG_FULL, DEFAULT_OUTBOUND_ON_MATCH, diff --git a/bot_bottle/gateway/egress_addon_core.py b/bot_bottle/gateway/egress/addon_core.py similarity index 99% rename from bot_bottle/gateway/egress_addon_core.py rename to bot_bottle/gateway/egress/addon_core.py index e4800d5..3d0bfca 100644 --- a/bot_bottle/gateway/egress_addon_core.py +++ b/bot_bottle/gateway/egress/addon_core.py @@ -16,11 +16,11 @@ 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. -from .egress_dlp_config import ( +from .dlp_config import ( DEFAULT_OUTBOUND_ON_MATCH, INBOUND_DETECTOR_NAMES, ON_MATCH_BLOCK, diff --git a/bot_bottle/gateway/egress_dlp_config.py b/bot_bottle/gateway/egress/dlp_config.py similarity index 100% rename from bot_bottle/gateway/egress_dlp_config.py rename to bot_bottle/gateway/egress/dlp_config.py diff --git a/bot_bottle/gateway/dlp_detectors.py b/bot_bottle/gateway/egress/dlp_detectors.py similarity index 99% rename from bot_bottle/gateway/dlp_detectors.py rename to bot_bottle/gateway/egress/dlp_detectors.py index 2ca286a..d0c8d1b 100644 --- a/bot_bottle/gateway/dlp_detectors.py +++ b/bot_bottle/gateway/egress/dlp_detectors.py @@ -19,7 +19,7 @@ from math import log2 from collections import Counter from urllib.parse import quote as url_quote -from .egress_addon_core import ScanResult +from .addon_core import ScanResult # --------------------------------------------------------------------------- diff --git a/bot_bottle/gateway/git_gate/__init__.py b/bot_bottle/gateway/git_gate/__init__.py new file mode 100644 index 0000000..67c9fe7 --- /dev/null +++ b/bot_bottle/gateway/git_gate/__init__.py @@ -0,0 +1,7 @@ +"""Gateway-side (data-plane) git-gate service: pure host-side hook rendering +(PRD 0008) and the smart-HTTP backend that fronts git-gate repos. + +The host-side git-gate service (provisioning, preflight) lives in +`bot_bottle.git_gate`; this is the data-plane counterpart. Import the concrete +modules directly (`from bot_bottle.gateway.git_gate.render import ...`). +""" diff --git a/bot_bottle/gateway/git_http_backend.py b/bot_bottle/gateway/git_gate/http_backend.py similarity index 100% rename from bot_bottle/gateway/git_http_backend.py rename to bot_bottle/gateway/git_gate/http_backend.py diff --git a/bot_bottle/gateway/git_gate_render.py b/bot_bottle/gateway/git_gate/render.py similarity index 99% rename from bot_bottle/gateway/git_gate_render.py rename to bot_bottle/gateway/git_gate/render.py index 67f055a..241ed9d 100644 --- a/bot_bottle/gateway/git_gate_render.py +++ b/bot_bottle/gateway/git_gate/render.py @@ -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. diff --git a/bot_bottle/gateway/supervisor/__init__.py b/bot_bottle/gateway/supervisor/__init__.py new file mode 100644 index 0000000..47b48b7 --- /dev/null +++ b/bot_bottle/gateway/supervisor/__init__.py @@ -0,0 +1,7 @@ +"""Gateway-side (data-plane) supervise service: the supervise daemon's HTTP +server (PRD 0013). + +The host-side supervise control lives in `bot_bottle.orchestrator.supervisor`; +this is the in-gateway daemon the agents reach. Import the concrete module +directly (`from bot_bottle.gateway.supervisor.server import ...`). +""" diff --git a/bot_bottle/gateway/supervise_server.py b/bot_bottle/gateway/supervisor/server.py similarity index 99% rename from bot_bottle/gateway/supervise_server.py rename to bot_bottle/gateway/supervisor/server.py index 520f1da..35d53c5 100644 --- a/bot_bottle/gateway/supervise_server.py +++ b/bot_bottle/gateway/supervisor/server.py @@ -58,7 +58,7 @@ import typing from dataclasses import dataclass from bot_bottle.constants import IDENTITY_HEADER -from bot_bottle.gateway.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.gateway.policy_resolver import PolicyResolveError, PolicyResolver diff --git a/bot_bottle/git_gate/__init__.py b/bot_bottle/git_gate/__init__.py index c902a1f..f6b6256 100644 --- a/bot_bottle/git_gate/__init__.py +++ b/bot_bottle/git_gate/__init__.py @@ -14,7 +14,7 @@ Layout: service delegates to. The rendering + the in-gateway hook execution live in -`bot_bottle.gateway.git_gate_render`. The public names are re-exported lazily +`bot_bottle.gateway.git_gate.render`. The public names are re-exported lazily via `__getattr__`, so `from bot_bottle.git_gate import …` keeps working and importing `git_gate.plan` (the contract's dependency) doesn't drag in the provisioning / forge-API code. @@ -31,7 +31,7 @@ if TYPE_CHECKING: provision_git_gate_dynamic_keys, revoke_git_gate_provisioned_keys, ) - from ..gateway.git_gate_render import ( + from ..gateway.git_gate.render import ( GIT_GATE_HOSTNAME, GIT_GATE_TIMEOUT_SECS, GitGateUpstream, @@ -55,17 +55,17 @@ _LAZY: dict[str, str] = { "revoke_git_gate_provisioned_keys": ".provision", "_provision_dynamic_key": ".provision", "_resolve_identity_file": ".provision", - "GIT_GATE_HOSTNAME": "..gateway.git_gate_render", - "GIT_GATE_TIMEOUT_SECS": "..gateway.git_gate_render", - "GitGateUpstream": "..gateway.git_gate_render", - "git_gate_upstreams_for_bottle": "..gateway.git_gate_render", - "git_gate_render_gitconfig": "..gateway.git_gate_render", - "git_gate_known_hosts_line": "..gateway.git_gate_render", - "git_gate_render_entrypoint": "..gateway.git_gate_render", - "git_gate_render_provision": "..gateway.git_gate_render", - "git_gate_render_hook": "..gateway.git_gate_render", - "git_gate_render_access_hook": "..gateway.git_gate_render", - "_gitconfig_validate_value": "..gateway.git_gate_render", + "GIT_GATE_HOSTNAME": "..gateway.git_gate.render", + "GIT_GATE_TIMEOUT_SECS": "..gateway.git_gate.render", + "GitGateUpstream": "..gateway.git_gate.render", + "git_gate_upstreams_for_bottle": "..gateway.git_gate.render", + "git_gate_render_gitconfig": "..gateway.git_gate.render", + "git_gate_known_hosts_line": "..gateway.git_gate.render", + "git_gate_render_entrypoint": "..gateway.git_gate.render", + "git_gate_render_provision": "..gateway.git_gate.render", + "git_gate_render_hook": "..gateway.git_gate.render", + "git_gate_render_access_hook": "..gateway.git_gate.render", + "_gitconfig_validate_value": "..gateway.git_gate.render", } diff --git a/bot_bottle/git_gate/plan.py b/bot_bottle/git_gate/plan.py index c5fe3bb..edd1bd2 100644 --- a/bot_bottle/git_gate/plan.py +++ b/bot_bottle/git_gate/plan.py @@ -10,7 +10,7 @@ from __future__ import annotations from dataclasses import dataclass from pathlib import Path -from ..gateway.git_gate_render import GitGateUpstream +from ..gateway.git_gate.render import GitGateUpstream @dataclass(frozen=True) diff --git a/bot_bottle/git_gate/provision.py b/bot_bottle/git_gate/provision.py index 007abd3..bae03a7 100644 --- a/bot_bottle/git_gate/provision.py +++ b/bot_bottle/git_gate/provision.py @@ -17,7 +17,7 @@ from ..bottle_state import globalize_slug from ..errors import MissingEnvVarError from ..log import info from ..manifest import ManifestBottle, ManifestGitEntry -from ..gateway.git_gate_render import GitGateUpstream +from ..gateway.git_gate.render import GitGateUpstream if TYPE_CHECKING: from .plan import GitGatePlan diff --git a/bot_bottle/git_gate/service.py b/bot_bottle/git_gate/service.py index edd592b..cfcc8aa 100644 --- a/bot_bottle/git_gate/service.py +++ b/bot_bottle/git_gate/service.py @@ -8,7 +8,7 @@ upstream before fetches. The agent never sees the upstream credential. `GitGate` is the host-side service the backend drives at launch: build the plan (`prepare`), provision / revoke the per-upstream deploy keys, and preflight the upstream host keys. The rendering it emits + the in-gateway hook execution live -in `bot_bottle.gateway.git_gate_render`. +in `bot_bottle.gateway.git_gate.render`. """ from __future__ import annotations @@ -16,7 +16,7 @@ from __future__ import annotations from pathlib import Path from ..manifest import Manifest, ManifestBottle -from ..gateway.git_gate_render import ( +from ..gateway.git_gate.render import ( GitGateUpstream, git_gate_known_hosts_line, git_gate_render_access_hook, diff --git a/scripts/critical-modules.txt b/scripts/critical-modules.txt index fa542f2..845e415 100644 --- a/scripts/critical-modules.txt +++ b/scripts/critical-modules.txt @@ -8,18 +8,18 @@ # # One module path per line, relative to the repo root. Blank lines and # `#` comments are ignored. -bot_bottle/egress_addon.py -bot_bottle/egress_addon_core.py -bot_bottle/dlp_detectors.py +bot_bottle/gateway/egress/addon.py +bot_bottle/gateway/egress/addon_core.py +bot_bottle/gateway/egress/dlp_detectors.py bot_bottle/egress.py bot_bottle/manifest.py bot_bottle/manifest_egress.py bot_bottle/manifest_agent.py bot_bottle/manifest_schema.py bot_bottle/git_gate.py -bot_bottle/git_gate_render.py +bot_bottle/gateway/git_gate/render.py bot_bottle/git_gate_provision.py -bot_bottle/git_http_backend.py +bot_bottle/gateway/git_gate/http_backend.py bot_bottle/supervise.py bot_bottle/yaml_subset.py bot_bottle/bottle_state.py diff --git a/tests/integration/test_gateway_image.py b/tests/integration/test_gateway_image.py index 213c93b..7c02a28 100644 --- a/tests/integration/test_gateway_image.py +++ b/tests/integration/test_gateway_image.py @@ -91,7 +91,7 @@ class TestGatewayImage(unittest.TestCase): # Probe that the package imports resolve inside the image. rc, out = self._run_in_image( "python3", "-c", - "from bot_bottle.supervisor import types; from bot_bottle.gateway import supervise_server; print('ok')", + "from bot_bottle.supervisor import types; from bot_bottle.gateway.supervisor import server as supervise_server; print('ok')", ) self.assertEqual(0, rc, msg=out) self.assertIn("ok", out) diff --git a/tests/unit/test_dlp_detectors.py b/tests/unit/test_dlp_detectors.py index d62fb4d..5310252 100644 --- a/tests/unit/test_dlp_detectors.py +++ b/tests/unit/test_dlp_detectors.py @@ -7,7 +7,7 @@ import base64 import gzip import unittest -from bot_bottle.gateway.dlp_detectors import ( +from bot_bottle.gateway.egress.dlp_detectors import ( ENTROPY_BLOCK_THRESHOLD, PARTIAL_MATCH_MIN_LEN, REDACT, @@ -465,17 +465,17 @@ class TestMatchedAndSafeTokens(unittest.TestCase): class TestStripCrlf(unittest.TestCase): def test_removes_url_encoded_crlf(self): - from bot_bottle.gateway.dlp_detectors import strip_crlf + from bot_bottle.gateway.egress.dlp_detectors import strip_crlf out = strip_crlf("next=%0d%0aX-Injected: evil") self.assertNotRegex(out, r"%0[dD]%0[aA]") def test_removes_literal_header_injection(self): - from bot_bottle.gateway.dlp_detectors import strip_crlf + from bot_bottle.gateway.egress.dlp_detectors import strip_crlf out = strip_crlf("value\r\nX-Injected: evil") self.assertIsNone(scan_crlf_injection(out)) def test_leaves_clean_text_unchanged(self): - from bot_bottle.gateway.dlp_detectors import strip_crlf + from bot_bottle.gateway.egress.dlp_detectors import strip_crlf self.assertEqual("/api/v1/data?q=hello", strip_crlf("/api/v1/data?q=hello")) class TestAlnumProjection(unittest.TestCase): diff --git a/tests/unit/test_egress.py b/tests/unit/test_egress.py index 9447549..2ec776a 100644 --- a/tests/unit/test_egress.py +++ b/tests/unit/test_egress.py @@ -344,7 +344,7 @@ class TestRenderRoutes(unittest.TestCase): self.assertEqual([], parse_yaml_subset(rendered)["routes"]) def test_round_trip_through_addon_core(self): - from bot_bottle.gateway.egress_addon_core import load_config + from bot_bottle.gateway.egress.addon_core import load_config b = _bottle([ {"host": "api.github.com", "auth": {"scheme": "Bearer", "token_ref": "GH_PAT"}, @@ -363,7 +363,7 @@ class TestRenderRoutes(unittest.TestCase): self.assertEqual("", addon_routes[2].auth_scheme) def test_dlp_round_trips(self): - from bot_bottle.gateway.egress_addon_core import load_config + from bot_bottle.gateway.egress.addon_core import load_config b = _bottle([{"host": "x.example", "dlp": { "outbound_detectors": ["token_patterns"], "inbound_detectors": False, @@ -375,7 +375,7 @@ class TestRenderRoutes(unittest.TestCase): self.assertEqual((), addon_routes[0].inbound_detectors) def test_outbound_on_match_round_trips(self): - from bot_bottle.gateway.egress_addon_core import load_config + from bot_bottle.gateway.egress.addon_core import load_config b = _bottle([{"host": "logs.example", "dlp": { "outbound_on_match": "redact", }}]) @@ -392,7 +392,7 @@ class TestRenderRoutes(unittest.TestCase): self.assertNotIn("outbound_on_match", rendered) def test_git_fetch_policy_round_trips(self): - from bot_bottle.gateway.egress_addon_core import load_config + from bot_bottle.gateway.egress.addon_core import load_config b = _bottle([{"host": "github.com", "git": {"fetch": True}}]) routes = egress_routes_for_bottle(b) rendered = egress_render_routes(routes) @@ -405,7 +405,7 @@ class TestRenderRoutes(unittest.TestCase): it, but the renderer in between dropped it — so the flag never reached the proxy and registry pulls kept failing with "unauthorized" while the config looked correct everywhere it was inspected.""" - from bot_bottle.gateway.egress_addon_core import load_config + from bot_bottle.gateway.egress.addon_core import load_config b = _bottle([{"host": "registry-1.docker.io", "preserve_auth": True}]) routes = egress_routes_for_bottle(b) rendered = egress_render_routes(routes) @@ -416,7 +416,7 @@ class TestRenderRoutes(unittest.TestCase): b = _bottle([{"host": "x.example"}]) rendered = egress_render_routes(egress_routes_for_bottle(b)) self.assertNotIn("preserve_auth", rendered) - from bot_bottle.gateway.egress_addon_core import load_config + from bot_bottle.gateway.egress.addon_core import load_config self.assertFalse(load_config(rendered).routes[0].preserve_auth) def test_log_zero_omitted_from_render(self): @@ -434,7 +434,7 @@ class TestRenderRoutes(unittest.TestCase): self.assertTrue(rendered.startswith(f"log: {level}\n")) def test_log_level_round_trips_to_addon_core(self): - from bot_bottle.gateway.egress_addon_core import load_config, LOG_FULL + from bot_bottle.gateway.egress.addon_core import load_config, LOG_FULL b = _bottle([{"host": "x.example"}]) routes = egress_routes_for_bottle(b) rendered = egress_render_routes(routes, log=LOG_FULL) @@ -444,7 +444,7 @@ class TestRenderRoutes(unittest.TestCase): def test_log_via_manifest_flows_to_render(self): from bot_bottle.manifest import ManifestIndex - from bot_bottle.gateway.egress_addon_core import load_config, LOG_BLOCKS + from bot_bottle.gateway.egress.addon_core import load_config, LOG_BLOCKS m = ManifestIndex.from_json_obj({ "bottles": {"dev": {"egress": { "log": 1, @@ -512,7 +512,7 @@ class TestRenderRoutesEscaping(unittest.TestCase): self.assertEqual('Bear"er', parsed[0]["inspect"]["auth_scheme"]) def test_path_value_with_double_quote_round_trips(self): - from bot_bottle.gateway.egress_addon_core import PathMatch, MatchEntry + from bot_bottle.gateway.egress.addon_core import PathMatch, MatchEntry routes = (EgressRoute( host="api.example", matches=(MatchEntry(paths=(PathMatch(type="prefix", value='/v1/"quoted"/'),)),), @@ -521,7 +521,7 @@ class TestRenderRoutesEscaping(unittest.TestCase): self.assertEqual('/v1/"quoted"/', parsed[0]["inspect"]["matches"][0]["paths"][0]["value"]) def test_header_value_with_double_quote_round_trips(self): - from bot_bottle.gateway.egress_addon_core import HeaderMatch, MatchEntry + from bot_bottle.gateway.egress.addon_core import HeaderMatch, MatchEntry routes = (EgressRoute( host="api.example", matches=(MatchEntry(headers=(HeaderMatch(name="x-h", value='val"ue'),)),), @@ -598,7 +598,7 @@ class TestCanaryGeneration(unittest.TestCase): self.assertNotEqual(plan_a.canary, plan_b.canary) def test_canary_detected_by_scan_known_secrets(self): - from bot_bottle.gateway.dlp_detectors import scan_known_secrets + from bot_bottle.gateway.egress.dlp_detectors import scan_known_secrets plan = self._make_plan() env = {plan.canary_env: plan.canary} diff --git a/tests/unit/test_egress_addon_core.py b/tests/unit/test_egress_addon_core.py index 593d6ca..a99e783 100644 --- a/tests/unit/test_egress_addon_core.py +++ b/tests/unit/test_egress_addon_core.py @@ -12,7 +12,7 @@ import unittest from pathlib import Path from urllib.parse import urlsplit -from bot_bottle.gateway.egress_addon_core import ( +from bot_bottle.gateway.egress.addon_core import ( LOG_BLOCKS, LOG_FULL, LOG_OFF, @@ -1377,15 +1377,15 @@ class TestScanOutboundEnhanced(unittest.TestCase): class TestOutboundDetectorNames(unittest.TestCase): def test_entropy_in_outbound_detector_names(self): - from bot_bottle.gateway.egress_addon_core import OUTBOUND_DETECTOR_NAMES + from bot_bottle.gateway.egress.addon_core import OUTBOUND_DETECTOR_NAMES self.assertIn("entropy", OUTBOUND_DETECTOR_NAMES) def test_known_secrets_in_outbound_detector_names(self): - from bot_bottle.gateway.egress_addon_core import OUTBOUND_DETECTOR_NAMES + from bot_bottle.gateway.egress.addon_core import OUTBOUND_DETECTOR_NAMES self.assertIn("known_secrets", OUTBOUND_DETECTOR_NAMES) def test_token_patterns_in_outbound_detector_names(self): - from bot_bottle.gateway.egress_addon_core import OUTBOUND_DETECTOR_NAMES + from bot_bottle.gateway.egress.addon_core import OUTBOUND_DETECTOR_NAMES self.assertIn("token_patterns", OUTBOUND_DETECTOR_NAMES) diff --git a/tests/unit/test_egress_addon_log_redaction.py b/tests/unit/test_egress_addon_log_redaction.py index 44af815..c6196d4 100644 --- a/tests/unit/test_egress_addon_log_redaction.py +++ b/tests/unit/test_egress_addon_log_redaction.py @@ -36,7 +36,7 @@ def _ensure_shims() -> None: _ensure_shims() -from bot_bottle.gateway.egress_addon import EgressAddon # noqa: E402 (import after shims) +from bot_bottle.gateway.egress.addon import EgressAddon # noqa: E402 (import after shims) # --------------------------------------------------------------------------- diff --git a/tests/unit/test_egress_addon_request_flow.py b/tests/unit/test_egress_addon_request_flow.py index a8a06d2..415bb8d 100644 --- a/tests/unit/test_egress_addon_request_flow.py +++ b/tests/unit/test_egress_addon_request_flow.py @@ -194,15 +194,15 @@ def _ensure_shims() -> None: _ensure_shims() -import bot_bottle.gateway.egress_addon as _ea_mod # noqa: E402 (after shims) -from bot_bottle.gateway.egress_addon import EgressAddon # noqa: E402 (after shims) -from bot_bottle.gateway.egress_addon import ( # noqa: E402 +import bot_bottle.gateway.egress.addon as _ea_mod # noqa: E402 (after shims) +from bot_bottle.gateway.egress.addon import EgressAddon # noqa: E402 (after shims) +from bot_bottle.gateway.egress.addon import ( # noqa: E402 DEFAULT_INBOUND_SCAN_LIMIT_BYTES, DEFAULT_TOKEN_ALLOW_TIMEOUT_SECONDS, _inbound_scan_limit_from_env, _token_allow_timeout_from_env, ) -from bot_bottle.gateway.egress_addon_core import ( # noqa: E402 +from bot_bottle.gateway.egress.addon_core import ( # noqa: E402 Config, LOG_BLOCKS, LOG_FULL, diff --git a/tests/unit/test_egress_core_parsing.py b/tests/unit/test_egress_core_parsing.py index 6a56e90..161efb4 100644 --- a/tests/unit/test_egress_core_parsing.py +++ b/tests/unit/test_egress_core_parsing.py @@ -8,7 +8,7 @@ from __future__ import annotations import unittest -from bot_bottle.gateway.egress_addon_core import ( +from bot_bottle.gateway.egress.addon_core import ( HeaderMatch, MatchEntry, PathMatch, diff --git a/tests/unit/test_egress_multitenant.py b/tests/unit/test_egress_multitenant.py index adabb3e..2aa5c86 100644 --- a/tests/unit/test_egress_multitenant.py +++ b/tests/unit/test_egress_multitenant.py @@ -4,7 +4,7 @@ from __future__ import annotations import unittest -from bot_bottle.gateway.egress_addon_core import ( +from bot_bottle.gateway.egress.addon_core import ( DENY_RESOLVER_ERROR, DENY_UNATTRIBUTED, DENY_UNPARSEABLE, diff --git a/tests/unit/test_firecracker_infra_vm.py b/tests/unit/test_firecracker_infra_vm.py index e0ae9bb..7dcf4ea 100644 --- a/tests/unit/test_firecracker_infra_vm.py +++ b/tests/unit/test_firecracker_infra_vm.py @@ -72,7 +72,7 @@ class TestBuildInfraRootfs(unittest.TestCase): self.assertIn("bot_bottle.orchestrator", init) # Gateway launches via the installed package (there is no # /app/gateway_init.py file since the daemons moved into bot_bottle). - self.assertIn("bot_bottle.gateway.gateway_init", init) + self.assertIn("bot_bottle.gateway.bootstrap", init) self.assertIn("export PATH=", init) # Persistent registry volume mounted at the DB dir before the CP starts. self.assertIn("/dev/vdb", init) diff --git a/tests/unit/test_gateway_init.py b/tests/unit/test_gateway_init.py index bb987ba..d43cfe6 100644 --- a/tests/unit/test_gateway_init.py +++ b/tests/unit/test_gateway_init.py @@ -1,6 +1,6 @@ """Unit: gateway data-plane init supervisor (PRD 0070; PRD 0024 bundle shape). -Tests both the helper functions in `bot_bottle.gateway.gateway_init` +Tests both the helper functions in `bot_bottle.gateway.bootstrap` and the supervisor's end-to-end signal / exit-code behavior. The end-to-end tests use real subprocesses (`sleep`, `/bin/sh -c '...'`) — short-lived, no docker required — so they run under `tests/unit/` @@ -18,7 +18,7 @@ import warnings from pathlib import Path from unittest.mock import patch -from bot_bottle.gateway.gateway_init import ( +from bot_bottle.gateway.bootstrap import ( _DaemonSpec, _Supervisor, _argv_for_daemon, @@ -489,7 +489,7 @@ class TestSupervisor(unittest.TestCase): time.sleep(0.3) # let `trap` register sup.request_shutdown(reason="test") - with patch("bot_bottle.gateway.gateway_init._GRACE_SECONDS", 0.3): + with patch("bot_bottle.gateway.bootstrap._GRACE_SECONDS", 0.3): rc = self._drive(sup, max_wait_s=4.0) # Process was SIGKILL'd → returncode -9 on POSIX. @@ -531,7 +531,7 @@ class TestMainEndToEnd(unittest.TestCase): helper = ( "import os, runpy, sys\n" - "from bot_bottle.gateway import gateway_init as si\n" + "from bot_bottle.gateway import bootstrap as si\n" "si._DAEMONS = (\n" f" si._DaemonSpec('alpha', ({SLEEP!r},'30')),\n" f" si._DaemonSpec('beta', ({SLEEP!r},'30')),\n" diff --git a/tests/unit/test_git_gate_provision_render.py b/tests/unit/test_git_gate_provision_render.py index b9bce15..d2ba9a7 100644 --- a/tests/unit/test_git_gate_provision_render.py +++ b/tests/unit/test_git_gate_provision_render.py @@ -4,7 +4,7 @@ from __future__ import annotations import unittest -from bot_bottle.gateway.git_gate_render import ( +from bot_bottle.gateway.git_gate.render import ( GitGateUpstream, git_gate_render_entrypoint, git_gate_render_provision, diff --git a/tests/unit/test_git_http_backend.py b/tests/unit/test_git_http_backend.py index 6c58772..89f162c 100644 --- a/tests/unit/test_git_http_backend.py +++ b/tests/unit/test_git_http_backend.py @@ -10,7 +10,7 @@ from pathlib import Path from unittest import mock from bot_bottle.git_gate import GIT_GATE_TIMEOUT_SECS -from bot_bottle.gateway.git_http_backend import GitHttpHandler, MAX_BODY_BYTES +from bot_bottle.gateway.git_gate.http_backend import GitHttpHandler, MAX_BODY_BYTES # The git-http backend is resolver-only: every request is attributed to a @@ -199,7 +199,7 @@ class TestGitHttpBackend(unittest.TestCase): subprocess.CompletedProcess(["git"], 0, backend_response, b""), ] with mock.patch( - "bot_bottle.gateway.git_http_backend.subprocess.run", + "bot_bottle.gateway.git_gate.http_backend.subprocess.run", side_effect=calls, ) as run: request = urllib.request.Request( @@ -265,7 +265,7 @@ class TestGitHttpBackend(unittest.TestCase): subprocess.CompletedProcess(["git"], 0, backend_response, b""), ] with mock.patch( - "bot_bottle.gateway.git_http_backend.subprocess.run", + "bot_bottle.gateway.git_gate.http_backend.subprocess.run", side_effect=calls, ) as run: req = urllib.request.Request( @@ -309,7 +309,7 @@ class TestGitHttpBackend(unittest.TestCase): denial = b"git-gate: upstream fetch failed; refusing to serve stale data\n" with mock.patch( - "bot_bottle.gateway.git_http_backend.subprocess.run", + "bot_bottle.gateway.git_gate.http_backend.subprocess.run", return_value=subprocess.CompletedProcess( ["hook"], 1, b"", denial, ), @@ -355,7 +355,7 @@ class TestGitHttpBackend(unittest.TestCase): self.addCleanup(server.server_close) with mock.patch( - "bot_bottle.gateway.git_http_backend.subprocess.run", + "bot_bottle.gateway.git_gate.http_backend.subprocess.run", return_value=subprocess.CompletedProcess( ["hook"], 2, b"", b"", ), @@ -402,7 +402,7 @@ class TestGitHttpBackend(unittest.TestCase): self.addCleanup(server.server_close) with mock.patch( - "bot_bottle.gateway.git_http_backend.subprocess.run", + "bot_bottle.gateway.git_gate.http_backend.subprocess.run", side_effect=PermissionError(13, "Permission denied"), ): buf = io.StringIO() @@ -461,7 +461,7 @@ class TestMalformedStatusHeader(unittest.TestCase): def _get_with_backend_response(self, cgi_response: bytes) -> int: with mock.patch( - "bot_bottle.gateway.git_http_backend.subprocess.run", + "bot_bottle.gateway.git_gate.http_backend.subprocess.run", return_value=mock.Mock(returncode=0, stdout=cgi_response), ): req = urllib.request.Request( @@ -545,7 +545,7 @@ class TestContentLengthBounds(unittest.TestCase): # With a valid Content-Length the handler proceeds into # git http-backend; that will fail (no real git repo) but the # status won't be 400 or 413. - with mock.patch("bot_bottle.gateway.git_http_backend.subprocess.run") as run: + with mock.patch("bot_bottle.gateway.git_gate.http_backend.subprocess.run") as run: run.return_value = mock.Mock( returncode=0, stdout=( diff --git a/tests/unit/test_git_http_multitenant.py b/tests/unit/test_git_http_multitenant.py index 16acff1..229e7df 100644 --- a/tests/unit/test_git_http_multitenant.py +++ b/tests/unit/test_git_http_multitenant.py @@ -10,7 +10,7 @@ from __future__ import annotations import unittest from pathlib import Path -from bot_bottle.gateway.git_http_backend import resolve_sandbox_root +from bot_bottle.gateway.git_gate.http_backend import resolve_sandbox_root from bot_bottle.gateway.policy_resolver import PolicyResolveError _BASE = Path("/git") diff --git a/tests/unit/test_macos_infra.py b/tests/unit/test_macos_infra.py index 7d80840..914ed1c 100644 --- a/tests/unit/test_macos_infra.py +++ b/tests/unit/test_macos_infra.py @@ -48,7 +48,7 @@ class TestInfraRun(unittest.TestCase): self.assertIn("bot_bottle.orchestrator", script) # Gateway launches via the installed package (there is no # /app/gateway_init.py file since the daemons moved into bot_bottle). - self.assertIn("bot_bottle.gateway.gateway_init", script) + self.assertIn("bot_bottle.gateway.bootstrap", script) self.assertIn("127.0.0.1", script) # they reach each other on loopback def test_db_is_a_container_only_volume(self) -> None: diff --git a/tests/unit/test_orchestrator_registration.py b/tests/unit/test_orchestrator_registration.py index ffb3752..738ecf8 100644 --- a/tests/unit/test_orchestrator_registration.py +++ b/tests/unit/test_orchestrator_registration.py @@ -7,7 +7,7 @@ import unittest from pathlib import Path from bot_bottle.egress import EgressPlan, EgressRoute -from bot_bottle.gateway.egress_addon_core import LOG_BLOCKS, load_config +from bot_bottle.gateway.egress.addon_core import LOG_BLOCKS, load_config from bot_bottle.orchestrator.registration import ( RegistrationInputs, egress_policy, diff --git a/tests/unit/test_supervise_server.py b/tests/unit/test_supervise_server.py index 0ae9ce5..52c575c 100644 --- a/tests/unit/test_supervise_server.py +++ b/tests/unit/test_supervise_server.py @@ -22,8 +22,8 @@ from bot_bottle.orchestrator import supervisor as _sv from bot_bottle.orchestrator.store import queue_store as _qs from bot_bottle.store import audit_store as _as -from bot_bottle.gateway import supervise_server # noqa: E402 -from bot_bottle.gateway.supervise_server import ( +from bot_bottle.gateway.supervisor import server as supervise_server # noqa: E402 +from bot_bottle.gateway.supervisor.server import ( ERR_INTERNAL, ERR_INVALID_PARAMS, ERR_INVALID_REQUEST,