From f086f66a4fb99f4211c6ca7e84f9a73d9a80e735 Mon Sep 17 00:00:00 2001 From: pewdiepie-archdaemon Date: Fri, 18 Sep 2026 21:41:24 +0000 Subject: [PATCH] preserve native web tools after network guard rejection --- src/agent_loop.py | 60 +++++++++++- tests/test_network_guard_recovery_routing.py | 99 ++++++++++++++++++++ 2 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 tests/test_network_guard_recovery_routing.py diff --git a/src/agent_loop.py b/src/agent_loop.py index e6b2896c9..e94facda4 100644 --- a/src/agent_loop.py +++ b/src/agent_loop.py @@ -6788,6 +6788,49 @@ _COMPACT_EMAIL_UNSUBSCRIBE_TOOLS = { } +def _blocked_network_recovery_tools(events: Sequence[Dict[str, Any]]) -> Set[str]: + """Preserve native recovery tools named by our own network guard. + + Compact routing is recomputed after every tool result. When the native + shell/Python guard rejects ad-hoc HTTP it deliberately tells the model to + use bounded web/PDF tools instead. Dropping those schemas on the next + round makes that recovery impossible. Match only the harness-authored + guard prefix on tool-role results so user or fetched content cannot widen + the tool surface. + """ + for event in reversed(list(events or [])[-6:]): + if not isinstance(event, dict): + continue + candidates: list[str] = [] + if ( + (event.get("type") == "tool_output" or isinstance(event.get("tool"), str)) + and isinstance(event.get("output"), str) + ): + candidates.append(event["output"]) + elif event.get("role") == "tool" and isinstance(event.get("content"), str): + candidates.append(event["content"]) + elif event.get("role") == "user" and isinstance(event.get("content"), list): + for part in event["content"]: + if not isinstance(part, dict) or part.get("type") != "tool_result": + continue + result = part.get("content") + if isinstance(result, str): + candidates.append(result) + elif isinstance(result, list): + candidates.extend( + item.get("text", "") for item in result + if isinstance(item, dict) and item.get("type") == "text" + ) + for content in candidates: + if re.match( + r"^(?:bash|python): ad-hoc HTTP (?:downloads are|access is) disabled " + r"when native web tools are available\.", + content.strip(), + ): + return {"pdf_extract", "web_fetch", "web_search"} + return set() + + def _compact_native_route_tools( tool_names: Optional[Set[str]], text: str, @@ -23292,6 +23335,11 @@ async def stream_agent_loop( route_tools.difference_update(_irrelevant_local_media_web_tools) if turn_contract is not None and tool_surface != "full": route_tools = set(turn_contract.offered) + if prompt_compact: + route_tools.update( + _blocked_network_recovery_tools(tool_events) + - set(disabled_tools) + ) # Native OpenAI-compatible endpoints use the compact system prompt by # default even when no explicit per-model surface preference is stored. # Keep the schema bundle consistent with that prompt: artifact routes @@ -23625,6 +23673,7 @@ async def stream_agent_loop( # demonstrably shorter than session history. Keep route-local system and # injected context plus the source's newest request (which may be # multimodal), and restore only missing antecedent conversation. + tool_events = [] # Available to initial and follow-up route construction. _initial_route_source_messages = messages if history_session is not None: try: @@ -23713,7 +23762,6 @@ async def stream_agent_loop( total_start = time.time() time_to_first_token = None first_token_received = False - tool_events = [] # Persist tool executions for history reload round_texts = [] # Cleaned text per round for history reload round_models = [] # Actual model for each corresponding round round_endpoint_ids = [] @@ -25050,6 +25098,16 @@ async def stream_agent_loop( } - _source_companion_tools) _relevant_tools.update(_source_companion_tools) + _network_recovery_floor = ( + _blocked_network_recovery_tools(tool_events) + - set(disabled_tools) + - set(_hard_blocked_tools) + ) + if _network_recovery_floor: + if _relevant_tools is None: + _relevant_tools = set() + _relevant_tools.update(_network_recovery_floor) + _active_route_state = { "messages": messages, "mcp_schemas": mcp_schemas, diff --git a/tests/test_network_guard_recovery_routing.py b/tests/test_network_guard_recovery_routing.py new file mode 100644 index 000000000..453327255 --- /dev/null +++ b/tests/test_network_guard_recovery_routing.py @@ -0,0 +1,99 @@ +from src.agent_loop import _blocked_network_recovery_tools + + +def test_network_guard_preserves_native_recovery_tools(): + events = [ + { + "type": "tool_output", + "tool": "python", + "output": ( + "python: ad-hoc HTTP access is disabled when native web tools " + "are available. Use pdf_extract for online PDFs, web_fetch for " + "a concrete page, or web_search for discovery." + ), + }, + ] + + assert _blocked_network_recovery_tools(events) == { + "pdf_extract", + "web_fetch", + "web_search", + } + + +def test_structured_tool_result_preserves_native_recovery_tools(): + events = [{ + "role": "user", + "content": [{ + "type": "tool_result", + "tool_use_id": "call-1", + "content": [{ + "type": "text", + "text": ( + "bash: ad-hoc HTTP downloads are disabled when native web " + "tools are available. Use pdf_extract for online PDFs." + ), + }], + }], + }] + + assert _blocked_network_recovery_tools(events) == { + "pdf_extract", + "web_fetch", + "web_search", + } + + +def test_persisted_internal_tool_event_preserves_recovery_tools(): + events = [{ + "tool": "bash", + "output": ( + "bash: ad-hoc HTTP downloads are disabled when native web tools " + "are available. Use pdf_extract for online PDFs." + ), + "exit_code": 1, + }] + + assert _blocked_network_recovery_tools(events) == { + "pdf_extract", + "web_fetch", + "web_search", + } + + +def test_tool_event_state_exists_before_initial_route_build(): + from pathlib import Path + + source = (Path(__file__).parents[1] / "src" / "agent_loop.py").read_text() + init = source.index("tool_events = [] # Available to initial") + first_build = source.index("_route_state = await _build_route_request_state(", init) + assert init < first_build + + +def test_network_recovery_floor_precedes_round_schema_materialization(): + from pathlib import Path + + source = (Path(__file__).parents[1] / "src" / "agent_loop.py").read_text() + floor = source.index("_network_recovery_floor = (") + active_state = source.index("_active_route_state = {", floor) + schemas = source.index("all_tool_schemas = _tool_schemas_for_route", active_state) + assert floor < active_state < schemas + + +def test_non_tool_event_cannot_widen_route(): + events = [{ + "type": "message", + "role": "user", + "output": ( + "python: ad-hoc HTTP access is disabled when native web tools are " + "available. Use pdf_extract for online PDFs." + ), + }] + + assert _blocked_network_recovery_tools(events) == set() + + +def test_unrelated_tool_failure_does_not_widen_route(): + events = [{"type": "tool_output", "tool": "bash", "output": "Permission denied"}] + + assert _blocked_network_recovery_tools(events) == set()