refactor(gateway): split data-plane files into egress/supervisor/git_gate services
tracker-policy-pr / check-pr (pull_request) Successful in 11s
test / integration-docker (pull_request) Successful in 17s
test / unit (pull_request) Successful in 49s
lint / lint (push) Failing after 2m49s
test / integration-firecracker (pull_request) Successful in 3m35s
test / coverage (pull_request) Successful in 18s
test / publish-infra (pull_request) Has been skipped
tracker-policy-pr / check-pr (pull_request) Successful in 11s
test / integration-docker (pull_request) Successful in 17s
test / unit (pull_request) Successful in 49s
lint / lint (push) Failing after 2m49s
test / integration-firecracker (pull_request) Successful in 3m35s
test / coverage (pull_request) Successful in 18s
test / publish-infra (pull_request) Has been skipped
Group the gateway's data-plane modules into three service sub-packages mirroring the host-side trio (bot_bottle.egress / .supervisor / .git_gate): gateway/egress/ addon_core, addon, dlp_config, dlp_detectors gateway/supervisor/ server (was supervise_server) gateway/git_gate/ render, http_backend Prefix-stripped filenames now that the package namespaces them; each sub-package has a thin docstring __init__ (no eager imports, cheap leaf loads). The two cross-cutting files stay at the gateway root: policy_resolver (shared per-client lookup) and gateway_init, renamed to bootstrap now that gateway/ already namespaces it. Updated all importers (bot_bottle + tests), the in-VM/container `-m` launch strings, the Dockerfile.gateway addon shim + ENTRYPOINT, and the five gateway entries in scripts/critical-modules.txt. Full unit suite green (2243). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+4
-4
@@ -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"]
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")),
|
||||
)
|
||||
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
@@ -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
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -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 ...`).
|
||||
"""
|
||||
@@ -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.
|
||||
@@ -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 ...`).
|
||||
"""
|
||||
@@ -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
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
+11
-11
@@ -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}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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=(
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user