From 3085dc1b3679c63af0d8bede1948b2afa5a9eca8 Mon Sep 17 00:00:00 2001 From: alvin-cmd <179569513+alvin-cmd@users.noreply.github.com> Date: Wed, 15 Jul 2026 02:11:51 +0200 Subject: [PATCH] fix(research): respect disabled web access --- routes/chat_routes.py | 12 ++++++---- src/tool_policy.py | 21 ++++++++++++++++++ tests/test_chat_route_tool_policy.py | 33 +++++++++++++++++++++++----- 3 files changed, 56 insertions(+), 10 deletions(-) diff --git a/routes/chat_routes.py b/routes/chat_routes.py index b8d9934b4c..5fab05b267 100644 --- a/routes/chat_routes.py +++ b/routes/chat_routes.py @@ -45,7 +45,7 @@ from src.tool_policy import ( WEB_TOOL_NAMES, build_effective_tool_policy, - is_web_search_explicitly_denied, + disabled_web_tools_for_turn, web_search_enabled_for_turn, ) @@ -780,6 +780,11 @@ async def chat_stream(request: Request) -> StreamingResponse: ) _research_flags = {"do": do_research} # Mutable container for generator scope + _disabled_web_tools = disabled_web_tools_for_turn( + allow_web_search, + use_web, + do_research, + ) # Query active document — prefer explicit ID from frontend, fall back to session lookup active_doc = None @@ -880,8 +885,7 @@ async def chat_stream(request: Request) -> StreamingResponse: if allow_bash is not None and str(allow_bash).lower() != "true": disabled_tools.add("bash") _explicit_web_intent = bool(_tool_intent and _tool_intent.category == "web") - if is_web_search_explicitly_denied(allow_web_search) or not _search_enabled: - disabled_tools.update(WEB_TOOL_NAMES) + disabled_tools.update(_disabled_web_tools) if _explicit_web_intent: # A direct lookup/search request should not drift into personal # tools or shell fallbacks. It can only use web_search/web_fetch @@ -898,7 +902,7 @@ async def chat_stream(request: Request) -> StreamingResponse: if _search_enabled: disabled_tools.difference_update(WEB_TOOL_NAMES) else: - disabled_tools.update(WEB_TOOL_NAMES) + disabled_tools.update(_disabled_web_tools) elif _search_enabled: disabled_tools.difference_update(WEB_TOOL_NAMES) diff --git a/src/tool_policy.py b/src/tool_policy.py index f0582235a3..01f60e158f 100644 --- a/src/tool_policy.py +++ b/src/tool_policy.py @@ -50,6 +50,27 @@ def web_search_enabled_for_turn(allow_web_search: object, use_web: object = None return tool_toggle_enabled(allow_web_search) or tool_toggle_enabled(use_web) +def disabled_web_tools_for_turn( + allow_web_search: object, + use_web: object = None, + use_research: object = None, +) -> Set[str]: + """Return network-backed tools denied by the request's web controls. + + Deep research performs live retrieval too, so an agent must not be able to + use ``trigger_research`` as a fallback when web access is off. An explicit + research request remains allowed while the quick-search tools stay off. + """ + + if web_search_enabled_for_turn(allow_web_search, use_web): + return set() + + disabled = set(WEB_TOOL_NAMES) + if not tool_toggle_enabled(use_research): + disabled.add("trigger_research") + return disabled + + _COMMON_TOOL_NAMES = { "api_call", "app_api", diff --git a/tests/test_chat_route_tool_policy.py b/tests/test_chat_route_tool_policy.py index ffc5cc1a2c..5ac4e131a0 100644 --- a/tests/test_chat_route_tool_policy.py +++ b/tests/test_chat_route_tool_policy.py @@ -16,7 +16,7 @@ from src.action_intents import classify_tool_intent from src.tool_policy import ( WEB_TOOL_NAMES, - is_web_search_explicitly_denied, + disabled_web_tools_for_turn, web_search_enabled_for_turn, ) @@ -94,8 +94,11 @@ def test_disabled_tools_respects_missing_vs_explicit_toggles(): assert "web_search_enabled_for_turn(allow_web_search, use_web)" in source, ( "web tools must be gated through the explicit per-turn web setting" ) - assert "disabled_tools.update(WEB_TOOL_NAMES)" in source, ( - "disabled_tools must add web_search/web_fetch when web is not explicitly enabled" + assert "disabled_web_tools_for_turn(" in source, ( + "web access must be gated through the shared per-turn denylist helper" + ) + assert "disabled_tools.update(_disabled_web_tools)" in source, ( + "disabled_tools must add all denied network-backed tools" ) assert "_forced_tools = set(WEB_TOOL_NAMES)" in source, ( "web tools should only be forced visible from the explicit web setting" @@ -109,6 +112,7 @@ def _build_disabled_tools( allow_bash=None, allow_web_search=None, use_web=None, + use_research=None, can_use_bash=True, can_use_browser=True, explicit_web_intent=False, @@ -124,8 +128,12 @@ def _build_disabled_tools( if allow_bash is not None and str(allow_bash).lower() != "true": disabled_tools.add("bash") search_enabled = web_search_enabled_for_turn(allow_web_search, use_web) - if is_web_search_explicitly_denied(allow_web_search) or not search_enabled: - disabled_tools.update(WEB_TOOL_NAMES) + disabled_web_tools = disabled_web_tools_for_turn( + allow_web_search, + use_web, + use_research, + ) + disabled_tools.update(disabled_web_tools) if explicit_web_intent: disabled_tools.update({ "bash", "python", @@ -139,7 +147,7 @@ def _build_disabled_tools( if search_enabled: disabled_tools.difference_update(WEB_TOOL_NAMES) else: - disabled_tools.update(WEB_TOOL_NAMES) + disabled_tools.update(disabled_web_tools) elif search_enabled: disabled_tools.difference_update(WEB_TOOL_NAMES) @@ -178,6 +186,18 @@ def test_json_body_allow_web_search_false_disables_web(): disabled = _build_disabled_tools(allow_web_search="false") assert "web_search" in disabled assert "web_fetch" in disabled + assert "trigger_research" in disabled + + +def test_explicit_research_toggle_allows_research_with_web_search_off(): + disabled = _build_disabled_tools( + allow_web_search="false", + use_research="true", + ) + + assert "web_search" in disabled + assert "web_fetch" in disabled + assert "trigger_research" not in disabled def test_chat_mode_use_web_true_enables_web(): @@ -245,6 +265,7 @@ def test_web_search_disabled_by_default_without_explicit_turn_setting(): disabled = _build_disabled_tools(allow_web_search=None) assert "web_search" in disabled assert "web_fetch" in disabled + assert "trigger_research" in disabled def test_non_privileged_user_without_explicit_flag_still_disabled():