fix(web): warn when a configured search/extract backend is discarded - #128
Merged
juniperbevensee merged 2 commits intoJul 30, 2026
Merged
Conversation
A `web.*_backend` value that is not available was silently dropped and replaced by auto-detection, at no log level. `_is_backend_available()` returns False for any name it does not recognise, so a one-character typo —`brave` instead of the registered `brave-free`— made `_get_capability_backend()` quietly return a completely different provider. Observed on a 7-agent fleet: `web.search_backend: brave` meant every web_search was served by firecrawl instead. It ran that way indefinitely. The config file said one thing, the runtime did another, and nothing at any log level disagreed — the only visible symptom was "web search returns empty", which is indistinguishable from a provider outage. Diagnosing it required reading the dispatcher source. Two layers now report, each once per process: - `_get_capability_backend` warns when an explicitly configured backend is discarded, and distinguishes the two operator mistakes that were previously identical: an unrecognised name (a typo — lists valid values) vs a known backend whose credential is unset. `_KNOWN_BACKENDS` exists only to make that distinction; a test pins it against `_is_backend_available` so it cannot drift and start calling a real backend a typo. - the `web_search_tool` dispatcher warns when the resolved backend is not a registered search provider. Both are deduped: these paths run on every web_search/web_extract dispatch and every `hermes tools` repaint, so an un-deduped warning would itself become the spam problem. Silence is preserved for the cases where nothing was lost — no configured value, an available backend, or a fallback that lands on the same provider. Behavior is unchanged; only logging is added. Tests: 10 new. Verified non-vacuous by reverting the source — the 5 behavior-pinning tests fail without the fix, while the 5 silence-asserting tests correctly still pass. 263 web tests pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This replaces the first cut of this change wholesale. Two independent audits each found it emitting FALSE diagnostics, which for a PR whose thesis is "a wrong message turns a typo into an unfalsifiable bug report" is disqualifying on its own terms. Round 1 rejected the config-layer classifier: - `_KNOWN_BACKENDS` was a verbatim duplicate of `_LEGACY_WEB_BACKENDS`, 132 lines above it in the same file, and was used with the OPPOSITE meaning to that constant's docstring. Deleted; classification now uses `_LEGACY_WEB_BACKENDS or _registered_web_provider(...)`, the same test `_resolve_backend` uses, so a registered third-party provider can never be reported as a typo with a valid-values list that excludes it. - The dispatch-site guard `if backend:` was dead code — `_get_backend()` never returns empty — so on a fresh install with no config at all it announced "Configured search backend 'brave-free' is not a registered provider", contradicting the tool's own error 15 lines later. - `web.backend` — the ONLY key `hermes tools` writes — was not covered at all. Now is, via a shared `_warn_discarded_backend` that takes the key it read. Round 2 rejected the replacement dispatch-layer warner, and it is now REMOVED rather than repaired. It hardcoded `web.%s_backend` while its value could come from `web.backend`, so the default case named a key absent from the operator's config. It asserted "the credential is set", inferred from configured == resolved — a fact `_resolve_backend` never establishes, since that function returns any legacy-or-registered NAME without probing availability. And it was near-dead: every bundled provider supports search, and the extract dispatcher already returns a precise typed error for a genuine capability mismatch a few lines below. A warning that misstates the key, invents a credential fact, and duplicates a better error is worse than silence. Also from round 2: - `_valid_backend_names` now filters by capability. An `extract_backend` typo was answered with a list including brave-free/ddgs/searxng/xai, none of which can extract — swapping one broken config for another, and contradicting the extract dispatcher's own "firecrawl, tavily, exa, or parallel". - The dedup reset moved to `tests/tools/conftest.py` so it guards the whole directory; the duplicate in-file fixture is gone. - Dropped a tombstone test that could only fail if someone re-added a deleted symbol name. Behavior is unchanged — logging only. Verified by AST comparison in round 1 that `_resolve_backend`'s body is identical to the old `_get_backend`. Tests: 12, verified by mutation — reverting each of six behaviours individually fails a test. 265 web tests pass, ruff clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
juniperbevensee
force-pushed
the
dev/juniperbevensee/web-backend-silent-fallback
branch
from
July 30, 2026 01:28
312aa13 to
12c1fc9
Compare
juniperbevensee
deleted the
dev/juniperbevensee/web-backend-silent-fallback
branch
July 30, 2026 01:48
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The failure
A fleet had
web.search_backend: braveset. The registered provider name isbrave-free, so the value was discarded and a different provider (firecrawl) served every search on all seven agents, indefinitely, with nothing logged at any level. The config file and the runtime disagreed and nothing said so — the only symptom was "web search returns empty", indistinguishable from a provider outage. Diagnosing it required reading the dispatcher source.This PR was rewritten twice after audits
Both audits found the change emitting false diagnostics — disqualifying for a PR arguing that a wrong message turns a typo into an unfalsifiable bug report. Recording what was wrong, since the failures are more instructive than the fix:
Round 1 — the classifier lied.
_KNOWN_BACKENDSwas a verbatim duplicate of_LEGACY_WEB_BACKENDS132 lines above it in the same file, used with the opposite meaning to that constant's own docstring. A registered third-party provider missing its key was therefore reported as a typo, with a "valid values" list excluding the user's own provider.if backend:was dead code —_get_backend()never returns empty — so a fresh install with no config announced "Configured search backend 'brave-free' is not a registered provider", contradicting the tool's own error 15 lines later.web.backend— the only keyhermes toolswrites — wasn't covered at all.Round 2 — the replacement dispatch warner lied differently. It hardcoded
web.%s_backendwhile its value could come fromweb.backend, naming a key absent from the operator's config. It asserted "the credential is set", inferred fromconfigured == resolved— a fact_resolve_backendnever establishes, since it returns any legacy-or-registered name without probing availability.What it does now
One report site: the config layer, in
_warn_discarded_backend, which knows which key it read. Classification uses_LEGACY_WEB_BACKENDS or _registered_web_provider(...)— the same test_resolve_backenduses — so a registered provider is never called a typo.versus a real backend missing its credential:
The dispatch-layer warner is removed, not repaired. Beyond being wrong, it was near-dead: every bundled provider supports search, and the extract dispatcher already returns a precise typed error for a genuine capability mismatch a few lines below. A warning that misstates the key, invents a credential fact, and duplicates a better error is worse than silence. A test asserts it stays gone.
_valid_backend_namesfilters by capability. Anextract_backendtypo was previously answered with a list includingbrave-free/ddgs/searxng/xai— none of which can extract — swapping one broken config for another and contradicting the extract dispatcher's own "firecrawl, tavily, exa, or parallel".Deduped per
(key, value): these paths run on every dispatch and everyhermes toolsrepaint, so an un-deduped warning becomes the spam it exists to surface. Silence is preserved wherever nothing was lost — no configured value, an available backend, or a fallback resolving to the same provider.Behavior
Logging only. Confirmed by AST comparison that
_resolve_backend's body is identical to the previous_get_backend; the new wrapper returnsresolvedunconditionally with the warn intry/except.Testing
12 tests, mutation-verified rather than trusted green — reverting each of six behaviours individually fails a test (silence the warner → 5 failures; misclassify registered providers, warn-when-unconfigured, drop
web.backendcoverage, capability-blind list, break dedup → 1 each). 265 web tests pass, ruff clean.The dedup reset lives in
tests/tools/conftest.pyso it guards the whole directory: the set is process-global, andpytest tests/tools/shares one interpreter (cross-file isolation only holds underscripts/run_tests_parallel.py, which CI uses).Upstream
This code is upstream (
NousResearch/hermes-agent) — the fallback block iskshitijk4poor's "refactor(web): dispatch all three tools through web_search_registry", present verbatim onupstream/mainattools/web_tools.py:685-691. Upstream issue #71929 already documents this bug, with the identicalbravevsbrave-freeexample and the words "silently fails"; its proposed fix is a config-form dropdown, which does nothing for agents whoseconfig.yamlis machine-written. So this is a different layer of fix to an already-reported problem, not a novel finding. No upstream PR is opened here.🤖 Generated with Claude Code