Compare commits

..

2 Commits

Author SHA1 Message Date
didericis-codex f5fdc0ea72 fix: satisfy pyright for log redaction tests
lint / lint (push) Successful in 2m42s
test / unit (pull_request) Successful in 54s
test / integration (pull_request) Successful in 26s
2026-06-25 00:15:45 -04:00
didericis-claude ca1f14b855 fix(egress): strip injected Authorization and redact bodies in LOG_FULL path
_log_request and _log_response wrote headers and bodies to stderr verbatim.
_log_request also included the sidecar-injected upstream Authorization value,
exposing live bearer tokens on every allowed request under LOG_FULL.

Apply redact_tokens to all header values and bodies in both log functions;
exclude the authorization header from _log_request entirely since its value
is always a live sidecar-injected credential by the time _log_request runs.

Closes #257
2026-06-25 00:15:45 -04:00
25 changed files with 96 additions and 158 deletions
+2 -1
View File
@@ -61,6 +61,7 @@ class AgentProviderRuntime:
prompt_mode: PromptMode prompt_mode: PromptMode
bypass_args: tuple[str, ...] bypass_args: tuple[str, ...]
resume_args: tuple[str, ...] resume_args: tuple[str, ...]
remote_control_args: tuple[str, ...]
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -390,7 +391,7 @@ def prompt_args(
if prompt_mode == "append_file": if prompt_mode == "append_file":
return ["--append-system-prompt-file", prompt_path] return ["--append-system-prompt-file", prompt_path]
if prompt_mode == "read_prompt_file": if prompt_mode == "read_prompt_file":
if argv and ("resume" in argv or "remote-control" in argv): if argv and "resume" in argv:
return [] return []
return [f"Read and follow the instructions in {prompt_path}."] return [f"Read and follow the instructions in {prompt_path}."]
if prompt_mode == "print_read_prompt_file": if prompt_mode == "print_read_prompt_file":
+2 -1
View File
@@ -109,8 +109,9 @@ class BottlePlan(ABC):
def workspace_plan(self) -> WorkspacePlan: def workspace_plan(self) -> WorkspacePlan:
return workspace_plan(self.spec, guest_home=self.guest_home) return workspace_plan(self.spec, guest_home=self.guest_home)
def print(self) -> None: def print(self, *, remote_control: bool) -> None:
"""Render the y/N preflight summary to stderr.""" """Render the y/N preflight summary to stderr."""
del remote_control
spec = self.spec spec = self.spec
manifest = self.manifest manifest = self.manifest
agent = manifest.agent agent = manifest.agent
+2 -6
View File
@@ -11,7 +11,7 @@ from pathlib import Path
from ..bottle_state import egress_state_dir from ..bottle_state import egress_state_dir
from ..egress import EGRESS_ROUTES_FILENAME from ..egress import EGRESS_ROUTES_FILENAME
from ..egress_addon_core import LOG_OFF, load_config from ..egress_addon_core import load_routes
class EgressApplyError(RuntimeError): class EgressApplyError(RuntimeError):
@@ -33,15 +33,11 @@ class EgressApplicator(ABC):
@staticmethod @staticmethod
def validate_routes_content(content: str) -> None: def validate_routes_content(content: str) -> None:
try: try:
config = load_config(content) load_routes(content)
except ValueError as e: except ValueError as e:
raise EgressApplyError( raise EgressApplyError(
f"proposed routes.yaml is not valid: {e}" f"proposed routes.yaml is not valid: {e}"
) from e ) from e
if config.log != LOG_OFF:
raise EgressApplyError(
"proposed routes.yaml must not change egress logging"
)
@staticmethod @staticmethod
def _routes_path(slug: str) -> Path: def _routes_path(slug: str) -> Path:
@@ -68,11 +68,6 @@ def build_image(ref: str, context: str, *, dockerfile: str = "") -> None:
_ensure_builder_dns() _ensure_builder_dns()
args = [_CONTAINER, "build", "-t", ref, "--dns", dns_server()] args = [_CONTAINER, "build", "-t", ref, "--dns", dns_server()]
if dockerfile: if dockerfile:
# `container build` resolves -f relative to the current working
# directory, not the build context. Anchor a relative Dockerfile to
# the context so builds work from any cwd.
if not os.path.isabs(dockerfile):
dockerfile = os.path.join(context, dockerfile)
args.extend(["-f", dockerfile]) args.extend(["-f", dockerfile])
args.append(context) args.append(context)
subprocess.run(args, check=True) subprocess.run(args, check=True)
+2
View File
@@ -28,6 +28,7 @@ from .start import _launch_bottle
def cmd_resume(argv: list[str]) -> int: def cmd_resume(argv: list[str]) -> int:
parser = argparse.ArgumentParser(prog=f"{PROG} resume", add_help=True) parser = argparse.ArgumentParser(prog=f"{PROG} resume", add_help=True)
parser.add_argument("--dry-run", action="store_true") parser.add_argument("--dry-run", action="store_true")
parser.add_argument("--remote-control", action="store_true")
parser.add_argument( parser.add_argument(
"identity", "identity",
help="bottle identity from a prior `start` (see its session-end output)", help="bottle identity from a prior `start` (see its session-end output)",
@@ -55,5 +56,6 @@ def cmd_resume(argv: list[str]) -> int:
return _launch_bottle( return _launch_bottle(
spec, spec,
dry_run=args.dry_run, dry_run=args.dry_run,
remote_control=args.remote_control,
backend_name=backend_name, backend_name=backend_name,
) )
+10 -4
View File
@@ -42,6 +42,7 @@ def cmd_start(argv: list[str]) -> int:
parser = argparse.ArgumentParser(prog=f"{PROG} start", add_help=True) parser = argparse.ArgumentParser(prog=f"{PROG} start", add_help=True)
parser.add_argument("--dry-run", action="store_true") parser.add_argument("--dry-run", action="store_true")
parser.add_argument("--cwd", action="store_true", help="copy host cwd into the running bottle") parser.add_argument("--cwd", action="store_true", help="copy host cwd into the running bottle")
parser.add_argument("--remote-control", action="store_true")
parser.add_argument( parser.add_argument(
"--backend", "--backend",
choices=known_backend_names(), choices=known_backend_names(),
@@ -88,6 +89,7 @@ def cmd_start(argv: list[str]) -> int:
return _launch_bottle( return _launch_bottle(
spec, spec,
dry_run=dry_run, dry_run=dry_run,
remote_control=args.remote_control,
backend_name=backend_name, backend_name=backend_name,
) )
@@ -132,7 +134,7 @@ def prepare_with_preflight(
def attach_agent( def attach_agent(
bottle: Bottle, *, resume: bool = False, bottle: Bottle, *, remote_control: bool = False, resume: bool = False,
agent_provider_template: str = "claude", agent_provider_template: str = "claude",
startup_args: tuple[str, ...] = (), startup_args: tuple[str, ...] = (),
) -> int: ) -> int:
@@ -151,6 +153,8 @@ def attach_agent(
"(Ctrl-D or 'exit' to leave; container will be removed)" "(Ctrl-D or 'exit' to leave; container will be removed)"
) )
agent_args = list(runtime.bypass_args) agent_args = list(runtime.bypass_args)
if remote_control:
agent_args.extend(runtime.remote_control_args)
agent_args.extend(startup_args) agent_args.extend(startup_args)
if resume: if resume:
agent_args.extend(runtime.resume_args) agent_args.extend(runtime.resume_args)
@@ -214,9 +218,9 @@ def _text_prompt_yes() -> bool:
return reply in ("y", "Y", "yes", "YES") return reply in ("y", "Y", "yes", "YES")
def _text_render_preflight(): def _text_render_preflight(*, remote_control: bool):
def _render(plan: DockerBottlePlan) -> None: def _render(plan: DockerBottlePlan) -> None:
plan.print() plan.print(remote_control=remote_control)
return _render return _render
@@ -224,6 +228,7 @@ def _launch_bottle(
spec: BottleSpec, spec: BottleSpec,
*, *,
dry_run: bool, dry_run: bool,
remote_control: bool,
backend_name: str | None = None, backend_name: str | None = None,
) -> int: ) -> int:
"""Shared launch core for `start` and `resume`. Builds the plan, """Shared launch core for `start` and `resume`. Builds the plan,
@@ -235,7 +240,7 @@ def _launch_bottle(
plan, identity = prepare_with_preflight( plan, identity = prepare_with_preflight(
spec, spec,
stage_dir=stage_dir, stage_dir=stage_dir,
render_preflight=_text_render_preflight(), render_preflight=_text_render_preflight(remote_control=remote_control),
prompt_yes=_text_prompt_yes, prompt_yes=_text_prompt_yes,
dry_run=dry_run, dry_run=dry_run,
backend_name=backend_name, backend_name=backend_name,
@@ -248,6 +253,7 @@ def _launch_bottle(
agent_provider_template = getattr(plan, "agent_provider_template", "claude") agent_provider_template = getattr(plan, "agent_provider_template", "claude")
exit_code = attach_agent( exit_code = attach_agent(
bottle, bottle,
remote_control=remote_control,
agent_provider_template=agent_provider_template, agent_provider_template=agent_provider_template,
startup_args=plan.agent_provision.startup_args, startup_args=plan.agent_provision.startup_args,
) )
@@ -91,6 +91,7 @@ _RUNTIME = AgentProviderRuntime(
prompt_mode="append_file", prompt_mode="append_file",
bypass_args=("--dangerously-skip-permissions",), bypass_args=("--dangerously-skip-permissions",),
resume_args=("--continue",), resume_args=("--continue",),
remote_control_args=("--remote-control",),
) )
+6 -9
View File
@@ -1,12 +1,12 @@
# bot-bottle Codex provider image. # bot-bottle Codex provider image.
# #
# Mirrors the default Claude image shape: Node LTS, git/network tooling, # Mirrors the default Claude image shape: Node LTS, git/network tooling,
# non-root node user, and the provider CLI installed for that user. # non-root node user, and the provider CLI installed globally.
FROM node:22-slim FROM node:22-slim
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y --no-install-recommends git ca-certificates curl procps \ && apt-get install -y --no-install-recommends git ca-certificates curl \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# App-specific deps. Python isn't required by codex itself # App-specific deps. Python isn't required by codex itself
@@ -17,15 +17,12 @@ RUN apt-get update \
&& apt-get install -y --no-install-recommends python3 python3-pip python3-venv \ && apt-get install -y --no-install-recommends python3 python3-pip python3-venv \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
RUN npm install -g --no-fund --no-audit @openai/codex@0.136.0 \
&& npm cache clean --force
USER node USER node
WORKDIR /home/node WORKDIR /home/node
ENV PATH="/home/node/.local/bin:${PATH}" RUN mkdir -p /home/node/.codex
# Remote-control support requires the standalone Codex install layout
# under ~/.codex/packages/standalone/current. The npm package can run
# the TUI, but remote-control commands expect this installer-owned path.
RUN mkdir -p /home/node/.codex \
&& curl -fsSL https://chatgpt.com/codex/install.sh | sh
CMD ["codex"] CMD ["codex"]
@@ -55,6 +55,7 @@ _RUNTIME = AgentProviderRuntime(
prompt_mode="read_prompt_file", prompt_mode="read_prompt_file",
bypass_args=("--dangerously-bypass-approvals-and-sandbox",), bypass_args=("--dangerously-bypass-approvals-and-sandbox",),
resume_args=("resume", "--last"), resume_args=("resume", "--last"),
remote_control_args=(),
) )
+1
View File
@@ -166,6 +166,7 @@ _RUNTIME = AgentProviderRuntime(
prompt_mode="append_system_prompt", prompt_mode="append_system_prompt",
bypass_args=(), bypass_args=(),
resume_args=(), resume_args=(),
remote_control_args=(),
) )
+10
View File
@@ -439,6 +439,15 @@ def route_to_yaml_dict(r: Route) -> dict[str, object]:
return d return d
def load_routes(text: str) -> tuple[Route, ...]:
"""Parse YAML text → routes."""
try:
payload = parse_yaml_subset(text)
except YamlSubsetError as e:
raise ValueError(f"routes payload: invalid YAML: {e}") from e
return parse_routes(payload)
def parse_config(payload: object) -> "Config": def parse_config(payload: object) -> "Config":
"""Parse a full egress config payload (top-level log level + routes).""" """Parse a full egress config payload (top-level log level + routes)."""
if not isinstance(payload, dict): if not isinstance(payload, dict):
@@ -853,6 +862,7 @@ __all__ = [
"is_git_push_request", "is_git_push_request",
"is_git_fetch_request", "is_git_fetch_request",
"load_config", "load_config",
"load_routes",
"match_route", "match_route",
"outbound_scan_headers", "outbound_scan_headers",
"parse_config", "parse_config",
+3 -8
View File
@@ -47,11 +47,11 @@ from pathlib import Path
try: try:
# Same-directory imports inside the bundle container; these files are # Same-directory imports inside the bundle container; these files are
# COPYed flat under /app by Dockerfile.sidecars. # COPYed flat under /app by Dockerfile.sidecars.
from egress_addon_core import LOG_OFF, load_config from egress_addon_core import load_routes
import supervise as _sv import supervise as _sv
except ModuleNotFoundError: except ModuleNotFoundError:
# Package imports for host-side tests and tooling. # Package imports for host-side tests and tooling.
from .egress_addon_core import LOG_OFF, load_config from .egress_addon_core import load_routes
from . import supervise as _sv from . import supervise as _sv
@@ -297,17 +297,12 @@ def validate_proposed_file(tool: str, content: str) -> None:
pass pass
elif tool in (_sv.TOOL_EGRESS_ALLOW, _sv.TOOL_EGRESS_BLOCK): elif tool in (_sv.TOOL_EGRESS_ALLOW, _sv.TOOL_EGRESS_BLOCK):
try: try:
config = load_config(content) load_routes(content)
except ValueError as e: except ValueError as e:
raise _RpcError( raise _RpcError(
ERR_INVALID_PARAMS, ERR_INVALID_PARAMS,
f"{tool}: proposed routes.yaml is not valid: {e}", f"{tool}: proposed routes.yaml is not valid: {e}",
) from e ) from e
if config.log != LOG_OFF:
raise _RpcError(
ERR_INVALID_PARAMS,
f"{tool}: proposed routes.yaml must not change egress logging",
)
else: else:
raise _RpcError(ERR_INVALID_PARAMS, f"unknown tool {tool!r}") raise _RpcError(ERR_INVALID_PARAMS, f"unknown tool {tool!r}")
@@ -1,6 +1,6 @@
# PRD 0064: LOG_FULL egress logging credential redaction # PRD prd-new: LOG_FULL egress logging credential redaction
- **Status:** Active - **Status:** Draft
- **Author:** claude - **Author:** claude
- **Created:** 2026-06-25 - **Created:** 2026-06-25
- **Issue:** #257 - **Issue:** #257
@@ -1,4 +1,4 @@
# PRD 0063: Strengthen outbound exfiltration detection # PRD prd-new: Strengthen outbound exfiltration detection
- **Status:** Active - **Status:** Active
- **Author:** claude - **Author:** claude
-21
View File
@@ -102,27 +102,6 @@ class TestAttachAgent(unittest.TestCase):
bottle.argv, bottle.argv,
) )
def test_remote_control_is_provider_startup_arg(self):
class Bottle:
argv: list[str] = []
def exec_agent(self, argv: list[str], *, tty: bool = True) -> int:
self.argv = list(argv)
return 0
bottle = Bottle()
exit_code = start_mod.attach_agent(
bottle, # type: ignore[arg-type]
agent_provider_template="codex",
startup_args=("remote-control",),
)
self.assertEqual(0, exit_code)
self.assertEqual(
["--dangerously-bypass-approvals-and-sandbox", "remote-control"],
bottle.argv,
)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
@@ -29,9 +29,6 @@ from bot_bottle.supervise import SupervisePlan
_URL = "http://supervise:9100/" _URL = "http://supervise:9100/"
_CODEX_DOCKERFILE = (
Path(__file__).resolve().parents[2] / "bot_bottle/contrib/codex/Dockerfile"
)
def _make_bottle(exec_result: ExecResult | None = None) -> MagicMock: def _make_bottle(exec_result: ExecResult | None = None) -> MagicMock:
@@ -279,12 +276,6 @@ class TestCodexProvision(unittest.TestCase):
) )
class TestCodexDockerfile(unittest.TestCase):
def test_installs_procps_for_remote_control_pid_management(self):
dockerfile = _CODEX_DOCKERFILE.read_text()
self.assertIn("procps", dockerfile)
class TestCodexSuperviseMcp(unittest.TestCase): class TestCodexSuperviseMcp(unittest.TestCase):
def test_noop_when_supervise_disabled(self): def test_noop_when_supervise_disabled(self):
bottle = _make_bottle() bottle = _make_bottle()
-10
View File
@@ -136,16 +136,6 @@ class TestClaudeArgv(unittest.TestCase):
argv, argv,
) )
def test_codex_remote_control_startup_arg_does_not_receive_initial_prompt(self):
argv = _codex_bottle("/home/node/.bot-bottle-prompt.txt").agent_argv(
["--dangerously-bypass-approvals-and-sandbox", "remote-control"],
)
self.assertEqual(
["docker", "exec", "-it", "bot-bottle-dev-abc", "codex",
"--dangerously-bypass-approvals-and-sandbox", "remote-control"],
argv,
)
def test_codex_resume_does_not_append_initial_prompt(self): def test_codex_resume_does_not_append_initial_prompt(self):
argv = _codex_bottle("/home/node/.bot-bottle-prompt.txt").agent_argv( argv = _codex_bottle("/home/node/.bot-bottle-prompt.txt").agent_argv(
["--dangerously-bypass-approvals-and-sandbox", "resume", "--last"], ["--dangerously-bypass-approvals-and-sandbox", "resume", "--last"],
@@ -31,6 +31,7 @@ class _Provider(AgentProvider):
return AgentProviderRuntime( return AgentProviderRuntime(
template="test", command="test", image="", template="test", command="test", image="",
prompt_mode="append_file", bypass_args=(), resume_args=(), prompt_mode="append_file", bypass_args=(), resume_args=(),
remote_control_args=(),
) )
def provision_plan(self, **kwargs): # type: ignore[override] def provision_plan(self, **kwargs): # type: ignore[override]
raise NotImplementedError raise NotImplementedError
+8 -8
View File
@@ -322,7 +322,7 @@ class TestRenderRoutes(unittest.TestCase):
self.assertEqual([], parse_yaml_subset(rendered)["routes"]) self.assertEqual([], parse_yaml_subset(rendered)["routes"])
def test_round_trip_through_addon_core(self): def test_round_trip_through_addon_core(self):
from bot_bottle.egress_addon_core import load_config from bot_bottle.egress_addon_core import load_routes
b = _bottle([ b = _bottle([
{"host": "api.github.com", {"host": "api.github.com",
"auth": {"scheme": "Bearer", "token_ref": "GH_PAT"}, "auth": {"scheme": "Bearer", "token_ref": "GH_PAT"},
@@ -333,7 +333,7 @@ class TestRenderRoutes(unittest.TestCase):
{"host": "api.anthropic.com"}, {"host": "api.anthropic.com"},
]) ])
routes = egress_routes_for_bottle(b) routes = egress_routes_for_bottle(b)
addon_routes = load_config(egress_render_routes(routes)).routes addon_routes = load_routes(egress_render_routes(routes))
self.assertEqual(3, len(addon_routes)) self.assertEqual(3, len(addon_routes))
self.assertEqual("Bearer", addon_routes[0].auth_scheme) self.assertEqual("Bearer", addon_routes[0].auth_scheme)
self.assertEqual("EGRESS_TOKEN_0", addon_routes[0].token_env) self.assertEqual("EGRESS_TOKEN_0", addon_routes[0].token_env)
@@ -341,26 +341,26 @@ class TestRenderRoutes(unittest.TestCase):
self.assertEqual("", addon_routes[2].auth_scheme) self.assertEqual("", addon_routes[2].auth_scheme)
def test_dlp_round_trips(self): def test_dlp_round_trips(self):
from bot_bottle.egress_addon_core import load_config from bot_bottle.egress_addon_core import load_routes
b = _bottle([{"host": "x.example", "dlp": { b = _bottle([{"host": "x.example", "dlp": {
"outbound_detectors": ["token_patterns"], "outbound_detectors": ["token_patterns"],
"inbound_detectors": False, "inbound_detectors": False,
}}]) }}])
routes = egress_routes_for_bottle(b) routes = egress_routes_for_bottle(b)
rendered = egress_render_routes(routes) rendered = egress_render_routes(routes)
addon_routes = load_config(rendered).routes addon_routes = load_routes(rendered)
self.assertEqual(("token_patterns",), addon_routes[0].outbound_detectors) self.assertEqual(("token_patterns",), addon_routes[0].outbound_detectors)
self.assertEqual((), addon_routes[0].inbound_detectors) self.assertEqual((), addon_routes[0].inbound_detectors)
def test_outbound_on_match_round_trips(self): def test_outbound_on_match_round_trips(self):
from bot_bottle.egress_addon_core import load_config from bot_bottle.egress_addon_core import load_routes
b = _bottle([{"host": "logs.example", "dlp": { b = _bottle([{"host": "logs.example", "dlp": {
"outbound_on_match": "redact", "outbound_on_match": "redact",
}}]) }}])
routes = egress_routes_for_bottle(b) routes = egress_routes_for_bottle(b)
rendered = egress_render_routes(routes) rendered = egress_render_routes(routes)
self.assertIn('outbound_on_match: "redact"', rendered) self.assertIn('outbound_on_match: "redact"', rendered)
addon_routes = load_config(rendered).routes addon_routes = load_routes(rendered)
self.assertEqual("redact", addon_routes[0].outbound_on_match) self.assertEqual("redact", addon_routes[0].outbound_on_match)
def test_outbound_on_match_default_omitted_from_render(self): def test_outbound_on_match_default_omitted_from_render(self):
@@ -370,12 +370,12 @@ class TestRenderRoutes(unittest.TestCase):
self.assertNotIn("outbound_on_match", rendered) self.assertNotIn("outbound_on_match", rendered)
def test_git_fetch_policy_round_trips(self): def test_git_fetch_policy_round_trips(self):
from bot_bottle.egress_addon_core import load_config from bot_bottle.egress_addon_core import load_routes
b = _bottle([{"host": "github.com", "git": {"fetch": True}}]) b = _bottle([{"host": "github.com", "git": {"fetch": True}}])
routes = egress_routes_for_bottle(b) routes = egress_routes_for_bottle(b)
rendered = egress_render_routes(routes) rendered = egress_render_routes(routes)
self.assertEqual({"fetch": True}, self._parsed(routes)[0]["git"]) self.assertEqual({"fetch": True}, self._parsed(routes)[0]["git"])
addon_routes = load_config(rendered).routes addon_routes = load_routes(rendered)
self.assertTrue(addon_routes[0].git_fetch) self.assertTrue(addon_routes[0].git_fetch)
def test_log_zero_omitted_from_render(self): def test_log_zero_omitted_from_render(self):
+42 -27
View File
@@ -32,6 +32,7 @@ from bot_bottle.egress_addon_core import (
is_git_fetch_request, is_git_fetch_request,
is_git_push_request, is_git_push_request,
load_config, load_config,
load_routes,
match_route, match_route,
outbound_scan_headers, outbound_scan_headers,
parse_config, parse_config,
@@ -288,6 +289,47 @@ class TestParseDlp(unittest.TestCase):
}]}) }]})
# --- load_routes ---------------------------------------------------------
class TestLoadRoutes(unittest.TestCase):
def test_yaml_text_round_trip(self):
routes = load_routes(
'routes:\n'
' - host: "api.example"\n'
)
self.assertEqual(1, len(routes))
self.assertEqual("api.example", routes[0].host)
def test_full_route_shape_parses(self):
routes = load_routes(
'routes:\n'
' - host: "api.example"\n'
' auth_scheme: "Bearer"\n'
' token_env: "EGRESS_TOKEN_0"\n'
' matches:\n'
' - paths:\n'
' - value: "/v1/"\n'
' - type: "exact"\n'
' value: "/messages"\n'
)
self.assertEqual(1, len(routes))
r = routes[0]
self.assertEqual("api.example", r.host)
self.assertEqual("Bearer", r.auth_scheme)
self.assertEqual("EGRESS_TOKEN_0", r.token_env)
self.assertEqual(1, len(r.matches))
self.assertEqual(2, len(r.matches[0].paths))
def test_empty_routes_list(self):
routes = load_routes("routes: []\n")
self.assertEqual((), routes)
def test_invalid_yaml_raises_value_error(self):
with self.assertRaises(ValueError):
load_routes("routes:\n\t- host: x\n")
# --- load_config / parse_config ------------------------------------------ # --- load_config / parse_config ------------------------------------------
@@ -336,33 +378,6 @@ class TestLoadConfig(unittest.TestCase):
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
parse_config("not a dict") parse_config("not a dict")
def test_empty_routes_list(self):
cfg = load_config("routes: []\n")
self.assertEqual((), cfg.routes)
def test_full_route_shape_parses(self):
cfg = load_config(
'routes:\n'
' - host: "api.example"\n'
' auth_scheme: "Bearer"\n'
' token_env: "EGRESS_TOKEN_0"\n'
' matches:\n'
' - paths:\n'
' - value: "/v1/"\n'
' - type: "exact"\n'
' value: "/messages"\n'
)
r = cfg.routes[0]
self.assertEqual("api.example", r.host)
self.assertEqual("Bearer", r.auth_scheme)
self.assertEqual("EGRESS_TOKEN_0", r.token_env)
self.assertEqual(1, len(r.matches))
self.assertEqual(2, len(r.matches[0].paths))
def test_invalid_yaml_raises_value_error(self):
with self.assertRaises(ValueError):
load_config("routes:\n\t- host: x\n")
# --- evaluate_matches --------------------------------------------------- # --- evaluate_matches ---------------------------------------------------
-9
View File
@@ -54,15 +54,6 @@ class TestValidateRoutesContent(unittest.TestCase):
' auth_scheme: "Bearer"\n' ' auth_scheme: "Bearer"\n'
) )
def test_rejects_log_full(self):
with self.assertRaises(EgressApplyError) as cm:
applicator.validate_routes_content(
'log: 2\n'
'routes:\n'
' - host: "x.example"\n'
)
self.assertIn("must not change egress logging", str(cm.exception))
class TestApplyRoutesChange(unittest.TestCase): class TestApplyRoutesChange(unittest.TestCase):
def setUp(self): def setUp(self):
-27
View File
@@ -73,33 +73,6 @@ resolver #2
) )
self.assertTrue(run.call_args_list[-1].kwargs["check"]) self.assertTrue(run.call_args_list[-1].kwargs["check"])
def test_build_image_anchors_relative_dockerfile_to_context(self):
status = util.subprocess.CompletedProcess(
args=[],
returncode=0,
stdout=(
'[{"status":{"state":"running"},'
'"configuration":{"dns":{"nameservers":["9.9.9.9"]}}}]'
),
stderr="",
)
with patch.object(util.subprocess, "run", return_value=status) as run, \
patch.object(util.os, "environ", {
"BOT_BOTTLE_MACOS_CONTAINER_DNS": "9.9.9.9",
}):
util.build_image(
"bot-bottle-sidecars:latest",
"/repo",
dockerfile="Dockerfile.sidecars",
)
self.assertEqual(
[
"container", "build", "-t", "bot-bottle-sidecars:latest",
"--dns", "9.9.9.9", "-f", "/repo/Dockerfile.sidecars", "/repo",
],
run.call_args_list[-1].args[0],
)
def test_commit_container_execs_tar_and_builds_image(self): def test_commit_container_execs_tar_and_builds_image(self):
# stderr is bytes because subprocess.run uses stderr=PIPE without text=True # stderr is bytes because subprocess.run uses stderr=PIPE without text=True
completed = util.subprocess.CompletedProcess( completed = util.subprocess.CompletedProcess(
+1 -1
View File
@@ -130,7 +130,7 @@ def _capture_print(plan: DockerBottlePlan | SmolmachinesBottlePlan) -> list[str]
orig = sys.stderr orig = sys.stderr
sys.stderr = buf sys.stderr = buf
try: try:
plan.print() plan.print(remote_control=False)
finally: finally:
sys.stderr = orig sys.stderr = orig
return buf.getvalue().splitlines() return buf.getvalue().splitlines()
@@ -42,6 +42,7 @@ class _Provider(AgentProvider):
return AgentProviderRuntime( return AgentProviderRuntime(
template="test", command="test", image="", template="test", command="test", image="",
prompt_mode="append_file", bypass_args=(), resume_args=(), prompt_mode="append_file", bypass_args=(), resume_args=(),
remote_control_args=(),
) )
def provision_plan(self, **kwargs): # type: ignore[override] def provision_plan(self, **kwargs): # type: ignore[override]
raise NotImplementedError raise NotImplementedError
-9
View File
@@ -67,15 +67,6 @@ class TestValidation(unittest.TestCase):
with self.assertRaises(_RpcError): with self.assertRaises(_RpcError):
validate_proposed_file(_sv.TOOL_EGRESS_BLOCK, "routes: nope\n") validate_proposed_file(_sv.TOOL_EGRESS_BLOCK, "routes: nope\n")
def test_egress_routes_yaml_rejects_log_full(self):
with self.assertRaises(_RpcError) as cm:
validate_proposed_file(
_sv.TOOL_EGRESS_ALLOW,
"log: 2\nroutes:\n - host: example.com\n",
)
self.assertEqual(ERR_INVALID_PARAMS, cm.exception.code)
self.assertIn("must not change egress logging", cm.exception.message)
# --- JSON-RPC parsing ------------------------------------------------------ # --- JSON-RPC parsing ------------------------------------------------------