diff --git a/Dockerfile.gateway b/Dockerfile.gateway index 8849d21..91d0c52 100644 --- a/Dockerfile.gateway +++ b/Dockerfile.gateway @@ -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.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_init"] +ENTRYPOINT ["python3", "-m", "bot_bottle.gateway.gateway_init"] diff --git a/bot_bottle/backend/docker/consolidated_launch.py b/bot_bottle/backend/docker/consolidated_launch.py index 9f9768a..482a30f 100644 --- a/bot_bottle/backend/docker/consolidated_launch.py +++ b/bot_bottle/backend/docker/consolidated_launch.py @@ -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 diff --git a/bot_bottle/backend/docker/gateway.py b/bot_bottle/backend/docker/gateway.py index 9bc9f93..edea8e7 100644 --- a/bot_bottle/backend/docker/gateway.py +++ b/bot_bottle/backend/docker/gateway.py @@ -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 diff --git a/bot_bottle/backend/egress_apply.py b/bot_bottle/backend/egress_apply.py index ed54cdf..a198107 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 ..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 cb64dd2..a7ab12b 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_init & + python3 -m bot_bottle.gateway.gateway_init & # Reap as PID 1; children are backgrounded, so `wait` blocks. while : ; do wait ; done diff --git a/bot_bottle/backend/macos_container/gateway.py b/bot_bottle/backend/macos_container/gateway.py index 3d296ff..fcac855 100644 --- a/bot_bottle/backend/macos_container/gateway.py +++ b/bot_bottle/backend/macos_container/gateway.py @@ -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 diff --git a/bot_bottle/backend/macos_container/infra.py b/bot_bottle/backend/macos_container/infra.py index 731a62a..85bc3f0 100644 --- a/bot_bottle/backend/macos_container/infra.py +++ b/bot_bottle/backend/macos_container/infra.py @@ -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" ) diff --git a/bot_bottle/backend/macos_container/launch.py b/bot_bottle/backend/macos_container/launch.py index 8902266..d54a20a 100644 --- a/bot_bottle/backend/macos_container/launch.py +++ b/bot_bottle/backend/macos_container/launch.py @@ -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 diff --git a/bot_bottle/egress.py b/bot_bottle/egress.py index 1abaf6a..b933bad 100644 --- a/bot_bottle/egress.py +++ b/bot_bottle/egress.py @@ -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, diff --git a/bot_bottle/orchestrator/gateway.py b/bot_bottle/gateway/__init__.py similarity index 100% rename from bot_bottle/orchestrator/gateway.py rename to bot_bottle/gateway/__init__.py diff --git a/bot_bottle/dlp_detectors.py b/bot_bottle/gateway/dlp_detectors.py similarity index 100% rename from bot_bottle/dlp_detectors.py rename to bot_bottle/gateway/dlp_detectors.py diff --git a/bot_bottle/egress_addon.py b/bot_bottle/gateway/egress_addon.py similarity index 99% rename from bot_bottle/egress_addon.py rename to bot_bottle/gateway/egress_addon.py index 8766623..112de8b 100644 --- a/bot_bottle/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.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, diff --git a/bot_bottle/egress_addon_core.py b/bot_bottle/gateway/egress_addon_core.py similarity index 99% rename from bot_bottle/egress_addon_core.py rename to bot_bottle/gateway/egress_addon_core.py index bf14fbb..e4800d5 100644 --- a/bot_bottle/egress_addon_core.py +++ b/bot_bottle/gateway/egress_addon_core.py @@ -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. diff --git a/bot_bottle/egress_dlp_config.py b/bot_bottle/gateway/egress_dlp_config.py similarity index 100% rename from bot_bottle/egress_dlp_config.py rename to bot_bottle/gateway/egress_dlp_config.py diff --git a/bot_bottle/gateway_init.py b/bot_bottle/gateway/gateway_init.py similarity index 98% rename from bot_bottle/gateway_init.py rename to bot_bottle/gateway/gateway_init.py index 7e9df7e..1e23be7 100644 --- a/bot_bottle/gateway_init.py +++ b/bot_bottle/gateway/gateway_init.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.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")), ) diff --git a/bot_bottle/git_gate_render.py b/bot_bottle/gateway/git_gate_render.py similarity index 98% rename from bot_bottle/git_gate_render.py rename to bot_bottle/gateway/git_gate_render.py index 04c038c..294d675 100644 --- a/bot_bottle/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. @@ -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 diff --git a/bot_bottle/git_http_backend.py b/bot_bottle/gateway/git_http_backend.py similarity index 99% rename from bot_bottle/git_http_backend.py rename to bot_bottle/gateway/git_http_backend.py index 21f924f..c4860e7 100644 --- a/bot_bottle/git_http_backend.py +++ b/bot_bottle/gateway/git_http_backend.py @@ -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 diff --git a/bot_bottle/policy_resolver.py b/bot_bottle/gateway/policy_resolver.py similarity index 100% rename from bot_bottle/policy_resolver.py rename to bot_bottle/gateway/policy_resolver.py diff --git a/bot_bottle/supervise_server.py b/bot_bottle/gateway/supervise_server.py similarity index 99% rename from bot_bottle/supervise_server.py rename to bot_bottle/gateway/supervise_server.py index aad06d1..26495a3 100644 --- a/bot_bottle/supervise_server.py +++ b/bot_bottle/gateway/supervise_server.py @@ -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 diff --git a/bot_bottle/git_gate.py b/bot_bottle/git_gate.py index d7c0f96..cfb8213 100644 --- a/bot_bottle/git_gate.py +++ b/bot_bottle/git_gate.py @@ -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, diff --git a/bot_bottle/git_gate_provision.py b/bot_bottle/git_gate_provision.py index 00f42fc..ac9eebd 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 .git_gate_render import GitGateUpstream +from .gateway.git_gate_render import GitGateUpstream if TYPE_CHECKING: from .git_gate import GitGatePlan diff --git a/bot_bottle/orchestrator/__init__.py b/bot_bottle/orchestrator/__init__.py index 3f03a7e..5346c00 100644 --- a/bot_bottle/orchestrator/__init__.py +++ b/bot_bottle/orchestrator/__init__.py @@ -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 diff --git a/bot_bottle/orchestrator/lifecycle.py b/bot_bottle/orchestrator/lifecycle.py index b75e87d..5c7b9c6 100644 --- a/bot_bottle/orchestrator/lifecycle.py +++ b/bot_bottle/orchestrator/lifecycle.py @@ -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, diff --git a/bot_bottle/orchestrator/rotate_ca.py b/bot_bottle/orchestrator/rotate_ca.py index 5495c77..5ae7d2d 100644 --- a/bot_bottle/orchestrator/rotate_ca.py +++ b/bot_bottle/orchestrator/rotate_ca.py @@ -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: diff --git a/bot_bottle/supervise.py b/bot_bottle/supervise.py index 96d25a3..d71253c 100644 --- a/bot_bottle/supervise.py +++ b/bot_bottle/supervise.py @@ -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: diff --git a/tests/integration/test_gateway_image.py b/tests/integration/test_gateway_image.py index 87e5d97..624bda9 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 import supervise, supervise_server; print('ok')", + "from bot_bottle import supervise; from bot_bottle.gateway import supervise_server; print('ok')", ) self.assertEqual(0, rc, msg=out) self.assertIn("ok", out) diff --git a/tests/integration/test_multitenant_isolation.py b/tests/integration/test_multitenant_isolation.py index 7ed3031..fdd8ea5 100644 --- a/tests/integration/test_multitenant_isolation.py +++ b/tests/integration/test_multitenant_isolation.py @@ -29,7 +29,7 @@ from bot_bottle.backend.docker.consolidated_launch import ( from bot_bottle.backend.docker.egress import EGRESS_PORT from bot_bottle.backend.docker.gateway_net import next_free_ip from bot_bottle.orchestrator.client import OrchestratorClient -from bot_bottle.orchestrator.gateway import GATEWAY_IMAGE, GATEWAY_NAME, GATEWAY_NETWORK +from bot_bottle.gateway import GATEWAY_IMAGE, GATEWAY_NAME, GATEWAY_NETWORK from bot_bottle.orchestrator.lifecycle import OrchestratorService from tests._docker import skip_unless_docker diff --git a/tests/unit/test_dlp_detectors.py b/tests/unit/test_dlp_detectors.py index 0cbd54f..d62fb4d 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.dlp_detectors import ( +from bot_bottle.gateway.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.dlp_detectors import strip_crlf + from bot_bottle.gateway.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.dlp_detectors import strip_crlf + from bot_bottle.gateway.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.dlp_detectors import strip_crlf + from bot_bottle.gateway.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 6d3e134..027df82 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.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.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.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.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.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.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.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.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.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.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.dlp_detectors import scan_known_secrets + from bot_bottle.gateway.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 a88d1ba..593d6ca 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.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.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.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.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 8b64f67..44af815 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.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 6896e9d..a8a06d2 100644 --- a/tests/unit/test_egress_addon_request_flow.py +++ b/tests/unit/test_egress_addon_request_flow.py @@ -194,22 +194,22 @@ def _ensure_shims() -> None: _ensure_shims() -import bot_bottle.egress_addon as _ea_mod # noqa: E402 (after shims) -from bot_bottle.egress_addon import EgressAddon # noqa: E402 (after shims) -from bot_bottle.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.egress_addon_core import ( # noqa: E402 +from bot_bottle.gateway.egress_addon_core import ( # noqa: E402 Config, LOG_BLOCKS, LOG_FULL, Route, route_to_yaml_dict, ) -from bot_bottle.policy_resolver import PolicyResolveError # noqa: E402 +from bot_bottle.gateway.policy_resolver import PolicyResolveError # noqa: E402 # --------------------------------------------------------------------------- diff --git a/tests/unit/test_egress_core_parsing.py b/tests/unit/test_egress_core_parsing.py index 7762a93..6a56e90 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.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 6d99e0d..adabb3e 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.egress_addon_core import ( +from bot_bottle.gateway.egress_addon_core import ( DENY_RESOLVER_ERROR, DENY_UNATTRIBUTED, DENY_UNPARSEABLE, @@ -12,7 +12,7 @@ from bot_bottle.egress_addon_core import ( resolve_client_config, resolve_client_context, ) -from bot_bottle.policy_resolver import PolicyResolveError +from bot_bottle.gateway.policy_resolver import PolicyResolveError class _FakeResolver: diff --git a/tests/unit/test_firecracker_infra_vm.py b/tests/unit/test_firecracker_infra_vm.py index a809344..e0ae9bb 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_init", init) + self.assertIn("bot_bottle.gateway.gateway_init", 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 17b5213..bb987ba 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_init` +Tests both the helper functions in `bot_bottle.gateway.gateway_init` 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_init import ( +from bot_bottle.gateway.gateway_init 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_init._GRACE_SECONDS", 0.3): + with patch("bot_bottle.gateway.gateway_init._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 import gateway_init as si\n" + "from bot_bottle.gateway import gateway_init 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.py b/tests/unit/test_git_gate.py index fd899f0..4befb5d 100644 --- a/tests/unit/test_git_gate.py +++ b/tests/unit/test_git_gate.py @@ -230,7 +230,7 @@ class TestHookRender(unittest.TestCase): # execute from the bare repo directory, so the embedded Python must # include /app and support both import layouts. self.assertIn('PYTHONPATH="/app${PYTHONPATH:+:$PYTHONPATH}"', hook) - self.assertIn("from bot_bottle.policy_resolver import PolicyResolver", hook) + self.assertIn("from bot_bottle.gateway.policy_resolver import PolicyResolver", hook) self.assertIn("from policy_resolver import PolicyResolver", hook) def test_inline_gitleaks_allow_fails_closed_without_supervisor(self): diff --git a/tests/unit/test_git_gate_provision_render.py b/tests/unit/test_git_gate_provision_render.py index 3fc1a07..b9bce15 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.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 405f0b0..6c58772 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.git_http_backend import GitHttpHandler, MAX_BODY_BYTES +from bot_bottle.gateway.git_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.git_http_backend.subprocess.run", + "bot_bottle.gateway.git_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.git_http_backend.subprocess.run", + "bot_bottle.gateway.git_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.git_http_backend.subprocess.run", + "bot_bottle.gateway.git_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.git_http_backend.subprocess.run", + "bot_bottle.gateway.git_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.git_http_backend.subprocess.run", + "bot_bottle.gateway.git_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.git_http_backend.subprocess.run", + "bot_bottle.gateway.git_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.git_http_backend.subprocess.run") as run: + with mock.patch("bot_bottle.gateway.git_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 b425f5f..16acff1 100644 --- a/tests/unit/test_git_http_multitenant.py +++ b/tests/unit/test_git_http_multitenant.py @@ -10,8 +10,8 @@ from __future__ import annotations import unittest from pathlib import Path -from bot_bottle.git_http_backend import resolve_sandbox_root -from bot_bottle.policy_resolver import PolicyResolveError +from bot_bottle.gateway.git_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 cf746dc..7d80840 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_init", script) + self.assertIn("bot_bottle.gateway.gateway_init", 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_gateway.py b/tests/unit/test_orchestrator_gateway.py index f712fae..b23162e 100644 --- a/tests/unit/test_orchestrator_gateway.py +++ b/tests/unit/test_orchestrator_gateway.py @@ -8,7 +8,7 @@ from pathlib import Path from unittest.mock import Mock, patch from bot_bottle.backend.docker.gateway import DockerGateway -from bot_bottle.orchestrator.gateway import ( +from bot_bottle.gateway import ( GATEWAY_CA_CERT, GATEWAY_NAME, GatewayError, diff --git a/tests/unit/test_orchestrator_lifecycle.py b/tests/unit/test_orchestrator_lifecycle.py index 2672853..1a65908 100644 --- a/tests/unit/test_orchestrator_lifecycle.py +++ b/tests/unit/test_orchestrator_lifecycle.py @@ -8,7 +8,7 @@ import urllib.error from pathlib import Path from unittest.mock import MagicMock, Mock, patch -from bot_bottle.orchestrator.gateway import GatewayError +from bot_bottle.gateway import GatewayError from bot_bottle.orchestrator.lifecycle import ( INFRA_NAME, INFRA_SOURCE_HASH_LABEL, diff --git a/tests/unit/test_orchestrator_registration.py b/tests/unit/test_orchestrator_registration.py index 846da4e..ffb3752 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.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_orchestrator_rotate_ca.py b/tests/unit/test_orchestrator_rotate_ca.py index 22199b8..3c2db89 100644 --- a/tests/unit/test_orchestrator_rotate_ca.py +++ b/tests/unit/test_orchestrator_rotate_ca.py @@ -8,7 +8,7 @@ from pathlib import Path from unittest.mock import Mock, patch from bot_bottle.orchestrator import rotate_ca -from bot_bottle.orchestrator.gateway import GATEWAY_NAME +from bot_bottle.gateway import GATEWAY_NAME from bot_bottle.orchestrator.lifecycle import INFRA_NAME from bot_bottle.paths import host_gateway_ca_dir from tests.unit import use_bottle_root diff --git a/tests/unit/test_policy_resolver.py b/tests/unit/test_policy_resolver.py index 83544ff..44178a3 100644 --- a/tests/unit/test_policy_resolver.py +++ b/tests/unit/test_policy_resolver.py @@ -7,7 +7,7 @@ import unittest import urllib.error from unittest.mock import MagicMock, patch -from bot_bottle.policy_resolver import ( +from bot_bottle.gateway.policy_resolver import ( CONTROL_AUTH_HEADER, CONTROL_AUTH_JWT_ENV, PolicyResolveError, @@ -15,7 +15,7 @@ from bot_bottle.policy_resolver import ( _control_auth_headers, ) -_URLOPEN = "bot_bottle.policy_resolver.urllib.request.urlopen" +_URLOPEN = "bot_bottle.gateway.policy_resolver.urllib.request.urlopen" def _resp(payload: object) -> MagicMock: diff --git a/tests/unit/test_supervise_server.py b/tests/unit/test_supervise_server.py index 3f596b0..7f0f21d 100644 --- a/tests/unit/test_supervise_server.py +++ b/tests/unit/test_supervise_server.py @@ -22,8 +22,8 @@ from bot_bottle import supervise as _sv from bot_bottle.store import queue_store as _qs from bot_bottle.store import audit_store as _as -from bot_bottle import supervise_server # noqa: E402 -from bot_bottle.supervise_server import ( +from bot_bottle.gateway import supervise_server # noqa: E402 +from bot_bottle.gateway.supervise_server import ( ERR_INTERNAL, ERR_INVALID_PARAMS, ERR_INVALID_REQUEST,