diff --git a/bot_bottle/backend/smolmachines/launch.py b/bot_bottle/backend/smolmachines/launch.py index a1d0925..d1a8eda 100644 --- a/bot_bottle/backend/smolmachines/launch.py +++ b/bot_bottle/backend/smolmachines/launch.py @@ -217,11 +217,15 @@ def _discover_urls( agent_supervise_url = f"http://{loopback_ip}:{supervise_host_port}/" existing_no_proxy = plan.guest_env.get("NO_PROXY", "localhost,127.0.0.1") + no_proxy = f"{existing_no_proxy},{loopback_ip}" guest_env = { **plan.guest_env, "HTTPS_PROXY": agent_proxy_url, "HTTP_PROXY": agent_proxy_url, - "NO_PROXY": f"{existing_no_proxy},{loopback_ip}", + "https_proxy": agent_proxy_url, + "http_proxy": agent_proxy_url, + "NO_PROXY": no_proxy, + "no_proxy": no_proxy, } if agent_git_gate_host: guest_env["GIT_GATE_URL"] = f"http://{agent_git_gate_host}" diff --git a/bot_bottle/egress_addon.py b/bot_bottle/egress_addon.py index fd51fe9..c0df317 100644 --- a/bot_bottle/egress_addon.py +++ b/bot_bottle/egress_addon.py @@ -5,7 +5,6 @@ egress container.""" from __future__ import annotations -import dataclasses import json import os import signal @@ -27,6 +26,7 @@ from egress_addon_core import ( # type: ignore[import-not-found] # pylint: dis load_config, match_route, outbound_scan_headers, + route_to_yaml_dict, scan_inbound, scan_outbound, ) @@ -82,7 +82,7 @@ class EgressAddon: def _serve_introspection(self, flow: http.HTTPFlow, path: str) -> None: if path == "/allowlist": payload = json.dumps( - {"routes": [dataclasses.asdict(r) for r in self.config.routes]}, + {"routes": [route_to_yaml_dict(r) for r in self.config.routes]}, indent=2, ).encode("utf-8") flow.response = http.Response.make( diff --git a/bot_bottle/egress_addon_core.py b/bot_bottle/egress_addon_core.py index 65f86c7..595baeb 100644 --- a/bot_bottle/egress_addon_core.py +++ b/bot_bottle/egress_addon_core.py @@ -359,6 +359,56 @@ def _parse_one(idx: int, raw: object) -> Route: ) +def _path_match_to_dict(pm: PathMatch) -> dict[str, object]: + d: dict[str, object] = {"value": pm.value} + if pm.type != "prefix": + d["type"] = pm.type + return d + + +def _header_match_to_dict(hm: HeaderMatch) -> dict[str, object]: + d: dict[str, object] = {"name": hm.name, "value": hm.value} + if hm.type != "exact": + d["type"] = hm.type + return d + + +def _match_entry_to_dict(me: MatchEntry) -> dict[str, object]: + d: dict[str, object] = {} + if me.paths: + d["paths"] = [_path_match_to_dict(p) for p in me.paths] + if me.methods: + d["methods"] = list(me.methods) + if me.headers: + d["headers"] = [_header_match_to_dict(h) for h in me.headers] + return d + + +def route_to_yaml_dict(r: Route) -> dict[str, object]: + """Serialize a Route to YAML-schema-compatible dict. + + Uses the same field names the YAML parser accepts, so the output + can be round-tripped directly into an `allow` or `egress-block` + proposal without translation. Fields that are empty/default are + omitted so the agent doesn't copy irrelevant keys.""" + d: dict[str, object] = {"host": r.host} + if r.auth_scheme: + d["auth_scheme"] = r.auth_scheme + d["token_env"] = r.token_env + if r.matches: + d["matches"] = [_match_entry_to_dict(m) for m in r.matches] + if r.git_fetch: + d["git"] = {"fetch": True} + dlp: dict[str, object] = {} + if r.outbound_detectors is not None: + dlp["outbound_detectors"] = list(r.outbound_detectors) + if r.inbound_detectors is not None: + dlp["inbound_detectors"] = list(r.inbound_detectors) + if dlp: + d["dlp"] = dlp + return d + + def load_routes(text: str) -> tuple[Route, ...]: """Parse YAML text → routes.""" try: @@ -698,6 +748,7 @@ def scan_inbound( __all__ = [ "LOG_BLOCKS", + "route_to_yaml_dict", "LOG_FULL", "LOG_OFF", "Config", diff --git a/bot_bottle/supervise_server.py b/bot_bottle/supervise_server.py index d50f642..6530561 100644 --- a/bot_bottle/supervise_server.py +++ b/bot_bottle/supervise_server.py @@ -172,7 +172,24 @@ TOOL_DEFINITIONS: list[dict[str, object]] = [ "properties": { "routes_yaml": { "type": "string", - "description": "Full proposed /etc/egress/routes.yaml content.", + "description": ( + "Full proposed /etc/egress/routes.yaml content. " + "Each route entry accepts these keys:\n" + " host: (required)\n" + " auth_scheme: Bearer|token (must pair with token_env)\n" + " token_env: (must pair with auth_scheme)\n" + " matches: (optional list of match entries)\n" + " - paths: [{type: prefix|exact|regex, value: /...}]\n" + " methods: [GET, POST, ...]\n" + " headers: [{name: X-Hdr, value: val, type: exact|regex}]\n" + " git: (optional; omit to block git clone/fetch)\n" + " fetch: true\n" + " dlp: (optional DLP scanner overrides)\n" + " outbound_detectors: [token_patterns, known_secrets]\n" + " inbound_detectors: [naive_injection_detection]\n" + "Omit any key that should use its default. " + "`list-egress-routes` returns routes in this same format." + ), }, "justification": { "type": "string", @@ -196,7 +213,24 @@ TOOL_DEFINITIONS: list[dict[str, object]] = [ "properties": { "routes_yaml": { "type": "string", - "description": "Full proposed /etc/egress/routes.yaml content.", + "description": ( + "Full proposed /etc/egress/routes.yaml content. " + "Each route entry accepts these keys:\n" + " host: (required)\n" + " auth_scheme: Bearer|token (must pair with token_env)\n" + " token_env: (must pair with auth_scheme)\n" + " matches: (optional list of match entries)\n" + " - paths: [{type: prefix|exact|regex, value: /...}]\n" + " methods: [GET, POST, ...]\n" + " headers: [{name: X-Hdr, value: val, type: exact|regex}]\n" + " git: (optional; omit to block git clone/fetch)\n" + " fetch: true\n" + " dlp: (optional DLP scanner overrides)\n" + " outbound_detectors: [token_patterns, known_secrets]\n" + " inbound_detectors: [naive_injection_detection]\n" + "Omit any key that should use its default. " + "`list-egress-routes` returns routes in this same format." + ), }, "justification": { "type": "string",