diff --git a/tests/tools/test_web_tools_config.py b/tests/tools/test_web_tools_config.py index 104236292a..f765d2ba60 100644 --- a/tests/tools/test_web_tools_config.py +++ b/tests/tools/test_web_tools_config.py @@ -916,3 +916,35 @@ class TestSiblingProvidersEnvResolution: from agent.web_search_provider import get_provider_env assert get_provider_env("WSP_TEST_UNSET_KEY") == "" + + +def test_xai_only_gate_agrees_with_dispatcher_when_web_xai_plugin_loaded(monkeypatch, tmp_path): + """With the bundled web-xai plugin registered (the default), the registry resolves xai + as the single eligible search provider while _get_backend never autodetects it. The + gate must follow the dispatcher: keyless off -> no servable backend -> tools stay off + (#116175 review follow-up). Module-level on purpose: TestCheckWebApiKey neutralizes the + registry path with get_active_*_provider -> None.""" + from agent import web_search_registry as registry + from plugins.web.xai.provider import XAIWebSearchProvider + + monkeypatch.setenv("HERMES_HOME", str(tmp_path)) + monkeypatch.setenv("XAI_API_KEY", "xai-test-key") + for k in ("PERPLEXITY_API_KEY", "SEARXNG_URL", "BRAVE_SEARCH_API_KEY", "TAVILY_API_KEY", "EXA_API_KEY"): + monkeypatch.delenv(k, raising=False) + with registry._lock: + saved = dict(registry._providers) + registry._providers.clear() + registry.register_provider(XAIWebSearchProvider()) + try: + with patch("tools.web_tools._load_web_config", return_value={}), \ + patch("tools.web_tools._ensure_web_plugins_loaded", lambda: None), \ + patch("tools.web_tools.check_firecrawl_api_key", return_value=False), \ + patch("agent.web_search_registry._keyless_tier_enabled", return_value=False): + from tools.web_tools import _get_backend, check_web_api_key + assert registry.get_active_search_provider().name == "xai" + assert _get_backend() == "firecrawl" # legacy sentinel: nothing servable + assert check_web_api_key() is False + finally: + with registry._lock: + registry._providers.clear() + registry._providers.update(saved) diff --git a/tools/web_tools.py b/tools/web_tools.py index 0406d46a30..22b00afa2a 100644 --- a/tools/web_tools.py +++ b/tools/web_tools.py @@ -131,8 +131,13 @@ def _get_backend() -> str: if provider.name not in _LEGACY_WEB_BACKENDS and _probe(provider, "is_available"): return provider.name - # Keyless free tier — strictly last so it never pre-empts a keyed backend. Discovery must run - # first: reachable from contexts that haven't loaded plugins (subprocess runs, delegate children). + return _keyless_backend() or "firecrawl" # default (backward compat) + + +def _keyless_backend() -> Optional[str]: + """Keyless free-tier backend name, or None. Strictly the last autodetect rung so it never + pre-empts a keyed backend. Discovery must run first: reachable from contexts that haven't + loaded plugins (subprocess runs, delegate children).""" try: _ensure_web_plugins_loaded() from agent.web_search_registry import _keyless_preference, _keyless_tier_enabled @@ -143,8 +148,7 @@ def _get_backend() -> str: return name except Exception as exc: # noqa: BLE001 — registry optional; never fatal logger.debug("keyless fallback walk failed: %s", exc) - - return "firecrawl" # default (backward compat) + return None def _get_search_backend() -> str: @@ -448,9 +452,17 @@ def check_web_api_key() -> bool: try: _ensure_web_plugins_loaded() from agent.web_search_registry import get_active_search_provider, get_active_extract_provider - return _provider_is_ready(get_active_search_provider()) or _provider_is_ready( - get_active_extract_provider() - ) + for provider in (get_active_search_provider(), get_active_extract_provider()): + if provider is not None and getattr(provider, "name", None) in _WEB_CHECK_SKIP: + # The registry's single-eligible / legacy walk picked a built-in that _get_backend + # never autodetects (the explicit-config case was handled above): the dispatcher + # would route to the keyless tier instead, so gate on exactly that. + if _keyless_backend() is not None: + return True + continue + if _provider_is_ready(provider): + return True + return False except Exception as exc: # noqa: BLE001 — registry optional; never fatal logger.debug("web provider registry availability check failed: %s", exc) return False