refactor: repo reorganization for clearer separation of concerns #477

Merged
didericis merged 30 commits from refactor/repo-reorg into fix/db-off-data-plane-469 2026-07-24 19:24:56 -04:00
47 changed files with 87 additions and 87 deletions
Showing only changes of commit f77023db1d - Show all commits
+2 -2
View File
@@ -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"]
@@ -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:
+1 -1
View File
@@ -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)
@@ -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
+4 -4
View File
@@ -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):
+11 -11
View File
@@ -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}
+4 -4
View File
@@ -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)
@@ -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)
# ---------------------------------------------------------------------------
+5 -5
View File
@@ -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
# ---------------------------------------------------------------------------
+1 -1
View File
@@ -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,
+2 -2
View File
@@ -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:
+1 -1
View File
@@ -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)
+4 -4
View File
@@ -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"
+1 -1
View File
@@ -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):
+1 -1
View File
@@ -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,
+8 -8
View File
@@ -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=(
+2 -2
View File
@@ -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")
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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:
+2 -2
View File
@@ -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,