From dea5acb2609c01beb6b1724055c8cd9778e3f23b Mon Sep 17 00:00:00 2001 From: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com> Date: Mon, 12 Oct 2026 08:56:49 -0400 Subject: [PATCH 1/2] fix(search): require explicit anonymous provider opt-in --- agent/tools/web_search/web_search.py | 4 ++-- tests/test_web_search_explicit_anonymous.py | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 tests/test_web_search_explicit_anonymous.py diff --git a/agent/tools/web_search/web_search.py b/agent/tools/web_search/web_search.py index 2fe0c71c..bc6a73d1 100644 --- a/agent/tools/web_search/web_search.py +++ b/agent/tools/web_search/web_search.py @@ -116,12 +116,12 @@ def _get_api_key(provider: str) -> str: def _anysearch_anonymous_enabled() -> bool: """Check if AnySearch anonymous mode is enabled via config.""" - return bool(_tools_web_search_conf().get("anysearch_anonymous")) + return _tools_web_search_conf().get("anysearch_anonymous") is True def _keenable_anonymous_enabled() -> bool: """Check if Keenable anonymous (keyless) mode is enabled via config.""" - return bool(_tools_web_search_conf().get("keenable_anonymous")) + return _tools_web_search_conf().get("keenable_anonymous") is True def _get_searxng_url() -> str: diff --git a/tests/test_web_search_explicit_anonymous.py b/tests/test_web_search_explicit_anonymous.py new file mode 100644 index 00000000..32682171 --- /dev/null +++ b/tests/test_web_search_explicit_anonymous.py @@ -0,0 +1,17 @@ +import pytest +from agent.tools.web_search import web_search as ws + + +@pytest.mark.parametrize('value', ['false', 'off', '0', [], {'invalid': True}, 1]) +def test_anonymous_search_requires_boolean_opt_in(monkeypatch, value): + monkeypatch.setattr(ws, '_get_api_key', lambda provider: '') + monkeypatch.setattr(ws, '_get_searxng_url', lambda: '') + monkeypatch.setattr(ws, 'conf', lambda: {'tools': {'web_search': {'anysearch_anonymous': value, 'keenable_anonymous': value}}}) + assert ws.configured_providers() == [] + + +def test_explicit_opt_in_and_api_key_configuration_remain_supported(monkeypatch): + monkeypatch.setattr(ws, '_get_api_key', lambda provider: 'fake-key' if provider == 'tavily' else '') + monkeypatch.setattr(ws, '_get_searxng_url', lambda: '') + monkeypatch.setattr(ws, 'conf', lambda: {'tools': {'web_search': {'anysearch_anonymous': True, 'keenable_anonymous': True}}}) + assert ws.configured_providers() == ['anysearch', 'tavily', 'keenable'] From 9d4cfeb2011964952b8057d0dd63f422f526c646 Mon Sep 17 00:00:00 2001 From: zhayujie Date: Fri, 16 Oct 2026 16:02:34 +0800 Subject: [PATCH 2/2] fix: share the anonymous search opt-in parsing between the tool and the console Co-authored-by: cowagent --- agent/tools/web_search/web_search.py | 17 +++++++++++++++-- channel/web/api/models.py | 6 ++++-- tests/test_web_search_explicit_anonymous.py | 15 ++++++++++++++- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/agent/tools/web_search/web_search.py b/agent/tools/web_search/web_search.py index bc6a73d1..06097704 100644 --- a/agent/tools/web_search/web_search.py +++ b/agent/tools/web_search/web_search.py @@ -114,14 +114,27 @@ def _get_api_key(provider: str) -> str: return "" +def anonymous_opt_in(value) -> bool: + """Whether a config value explicitly enables keyless search. Anything not + clearly "on" (e.g. the string "false") stays off, since it sends queries + to a third party.""" + if isinstance(value, bool): + return value + if isinstance(value, int): + return value == 1 + if isinstance(value, str): + return value.strip().lower() in ("true", "1", "yes", "on") + return False + + def _anysearch_anonymous_enabled() -> bool: """Check if AnySearch anonymous mode is enabled via config.""" - return _tools_web_search_conf().get("anysearch_anonymous") is True + return anonymous_opt_in(_tools_web_search_conf().get("anysearch_anonymous")) def _keenable_anonymous_enabled() -> bool: """Check if Keenable anonymous (keyless) mode is enabled via config.""" - return _tools_web_search_conf().get("keenable_anonymous") is True + return anonymous_opt_in(_tools_web_search_conf().get("keenable_anonymous")) def _get_searxng_url() -> str: diff --git a/channel/web/api/models.py b/channel/web/api/models.py index b4d47a52..a5d9b0dd 100644 --- a/channel/web/api/models.py +++ b/channel/web/api/models.py @@ -1489,8 +1489,10 @@ class ModelsHandler: if not isinstance(ws_cfg, dict): ws_cfg = {} - anonymous_on = bool(ws_cfg.get("anysearch_anonymous")) - keenable_anonymous_on = bool(ws_cfg.get("keenable_anonymous")) + from agent.tools.web_search.web_search import anonymous_opt_in + + anonymous_on = anonymous_opt_in(ws_cfg.get("anysearch_anonymous")) + keenable_anonymous_on = anonymous_opt_in(ws_cfg.get("keenable_anonymous")) searxng_url = (ws_cfg.get("searxng_url") or "").strip() if isinstance(ws_cfg, dict) else "" providers = [] configured_ids = [] diff --git a/tests/test_web_search_explicit_anonymous.py b/tests/test_web_search_explicit_anonymous.py index 32682171..6083eafe 100644 --- a/tests/test_web_search_explicit_anonymous.py +++ b/tests/test_web_search_explicit_anonymous.py @@ -2,7 +2,7 @@ import pytest from agent.tools.web_search import web_search as ws -@pytest.mark.parametrize('value', ['false', 'off', '0', [], {'invalid': True}, 1]) +@pytest.mark.parametrize('value', ['false', 'off', '0', '', None, 0, [], {'invalid': True}]) def test_anonymous_search_requires_boolean_opt_in(monkeypatch, value): monkeypatch.setattr(ws, '_get_api_key', lambda provider: '') monkeypatch.setattr(ws, '_get_searxng_url', lambda: '') @@ -15,3 +15,16 @@ def test_explicit_opt_in_and_api_key_configuration_remain_supported(monkeypatch) monkeypatch.setattr(ws, '_get_searxng_url', lambda: '') monkeypatch.setattr(ws, 'conf', lambda: {'tools': {'web_search': {'anysearch_anonymous': True, 'keenable_anonymous': True}}}) assert ws.configured_providers() == ['anysearch', 'tavily', 'keenable'] + + +@pytest.mark.parametrize('value', [True, 'true', 'True', 'yes', 'on', '1', 1]) +def test_explicit_on_values_enable_anonymous_search(value): + assert ws.anonymous_opt_in(value) is True + + +def test_console_status_matches_the_tool(monkeypatch): + from channel.web.api.models import ModelsHandler + cfg = {'tools': {'web_search': {'anysearch_anonymous': 'false', 'keenable_anonymous': 'false'}}} + providers = {p['id']: p for p in ModelsHandler._search_capability(cfg)['providers']} + assert not providers['anysearch'].get('configured') + assert not providers['keenable'].get('configured')