fix(web-tools): ensure plugins loaded before backend selection for plugin-registered providers - #67309
fix(web-tools): ensure plugins loaded before backend selection for plugin-registered providers#67309mblauser wants to merge 3 commits into
Conversation
…ugin-registered providers
teknium1
left a comment
There was a problem hiding this comment.
Thanks — the premise is confirmed on current main. web_extract_tool() resolves _get_extract_backend() at tools/web_tools.py:858, while plugin discovery occurs only afterward at tools/web_tools.py:867; configured non-legacy providers therefore consult an empty registry through _is_backend_available() (tools/web_tools.py:304-308, 323-327). The proposed calls at the shared gates match the issue, and _ensure_plugins_discovered() is idempotent (hermes_cli/plugins.py:1286-1287, 2330-2337).
Problems
- The diff has no regression test. The closest custom-provider coverage pre-registers its fake provider in
tests/tools/test_web_tools_config.py:753-759, so it does not verify discovery occurs before backend selection.
Suggested changes
- Add a cold-registry regression test that has
_ensure_web_plugins_loaded()register an available fake configured provider, then asserts_get_extract_backend()selects it.
Automated hermes-sweeper review.
|
I've addressed the review feedback by adding the requested cold-registry regression test. Whenever convenient, would someone please approve the workflow run so the CI can execute and verify the changes? No rush; happy to wait for your review timeline. |
|
This PR was flagged at P3 "Low - cosmetic, nice to have" but the underlying blocking bug is more than cosmetic. It prevents custom web_extract plugins such as hermes-plugin-crawl4ai for a self-hosted crawl4ai from loading. This simple fix solves that. |
…suite The test added in 029b5bf did not exercise the fix. It called register_provider() directly after _reset_for_tests(), so the registry was pre-populated by the test rather than by discovery, and it never engaged _ensure_web_plugins_loaded() at all. It passed with both fix lines removed, which made it a coverage illusion rather than a regression guard. Replace it with tests/tools/test_plugin_discovery_ordering.py: - test_discovery_precedes_registry_read, parametrized over _is_backend_available, _get_backend and _get_extract_backend. Spies both registry helpers (_registered_web_provider and _registered_web_provider_available, since the two chokepoints use different ones) plus discovery, and asserts discovery precedes the registry read. Enforces the ordering invariant rather than this specific fix, so a future registry consumer that skips discovery also fails. - Two cold-registry selection tests covering the capability-override path and the plain web.backend path, pinning each of the two fix lines independently. - A negative test so discovery running does not make an unregistered backend name pass. - A re-entrancy test covering a plugin whose import-time code calls back into _is_backend_available() during its own registration. Skips when the fix is absent. - One integration-marked test running real discovery, so the mocked tests are not resting on an unverified premise. Verified against the tree with and without the fix: the ordering and selection tests fail without it and pass with it, and reverting either fix line individually fails only the tests covering that line. Also ran the full set of tests/tools/ files that import or exercise tools.web_tools (16 files, 386 tests) with the fix applied: all pass, no unrelated regressions. No production code changes in this commit.
|
Thanks for pointing out the errors in the test methodology. The test pre-populated the registry itself rather than letting discovery do it, so it never exercised the ordering this PR changes. I have replaced it with a suite that I verified fails without the fix. Why the fix sits at the two shared gatesSome context on how this got to where it is, because the shape of the fix is the main thing worth reviewing and it only makes sense against what came before it. #34563 introduced #67110 found that exact gap in The place where I would push for merging this PR is the wider scope that it fixes. Moving the call earlier in
Those are two distinct registry reads through two distinct helpers, which is easy to miss (I missed it myself while writing the tests, see below). Fixing the extract caller leaves the search path and the availability gate still resolving against a possibly empty registry. If we fix this too narrowly, we would be back here for the third time with a third narrow patch. Putting the call at I am not attached to this particular placement if you see a reason it is wrong. If there is a hot path where even the guard check matters, or if you would rather this live one level up, I am happy to move it. The property I care about is that no registry consumer can be added later without discovery having run first, and the ordering test below is written to enforce that property rather than this specific implementation of it. The replacement testsFive classes, eight tests, one marked
What I checked, and one thing this caughtRan the suite against the tree with the fix applied and with it reverted:
I also ran every test in That last point is also how I found a bug in my own test. My first ordering spy only wrapped |
|
Thanks to #58320 for flagging the flawed test methodology in the earlier commit on this PR. That's been corrected: see the latest commit and comment above. The fix itself is ready and just waiting on review and merge. Once that happens, it closes out the plugin discovery ordering piece of the wider web-plugin issues. #58320's specific ask around clearer failure messaging is a separate, still-open concern and deserves its own follow-up. |
SummaryTwenty-four PRs address or reference three linked causes: cold-registry discovery ordering, hardcoded provider-selection/tool gates, and non-actionable web-tool failures. #34563 and #57779 are merged reference implementations for dispatcher discovery and registry-aware provider selection, while #67309, #67110, #71791, and #72646 cover the remaining selection-before-discovery gap and #58359 separately covers failure guidance. Related pull requests
Duplicates#27584, #27700, #28202, and #69144 overlap on discovery-before-dispatch, with #34563 as the merged reference. #31829, #31887, #32465, #33516, #33902, #36094, #38375, #52057, #56201, and #56761 overlap with the provider-gate class implemented by #57779; #67110, #67309, #71791, and the web-selection portion of #72646 overlap on discovery-before-selection. Suggested consolidationKeep #67309 open with a salvage path: retain the shared-gate fix and its corrected cold-registry tests, then obtain an independent run/review against current main. Close #67110 as a narrower duplicate of #67309; despite the keep_open review on #71791, its diff duplicates that ordering fix while adding a second discovery wrapper and omitting check_web_api_key() coverage, so close it as a duplicate of #67309; keep #72646 open only to split and preserve its distinct display fix while addressing its check_web_api_key() review, and keep #58359 open only after replacing the unsupported cache/browser guidance, adding focused tests, and removing the unrelated Himalaya edit. Complex graphflowchart LR
classDef open fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
classDef merged fill:#dcfce7,stroke:#15803d,color:#14532d
classDef closed fill:#e5e7eb,stroke:#6b7280,color:#1f2937
classDef unverified fill:#f3f4f6,stroke:#9ca3af,color:#374151
classDef best stroke-width:3px,stroke:#b45309
classDef target stroke-width:3px,stroke:#4338ca
I27683(["issue #27683 (closed)"])
subgraph Dup67309 ["PRs duplicating each other"]
P67309["PR #67309 (open)"]
P71791["PR #71791 (open)"]
end
P67309 -.->|partial| I27683
class I27683 closed
class P67309 open
class P71791 open
class P67309 target
click I27683 "https://github.com/NousResearch/hermes-agent/issues/27683"
click P67309 "https://github.com/NousResearch/hermes-agent/pull/67309"
click P71791 "https://github.com/NousResearch/hermes-agent/pull/71791"
Graph: solid arrow = fixes / best fix, dashed arrow = partial or unverified (see edge label); boxed group = PRs duplicating each other; amber border = best fix; indigo border = target; gray node = closed (state tag in the node label). Cross-PR triage: Reviewed 24 pull requests and 8 issues in this complex. Each diff was read against this issue; Assessment working set: 147 kB of PR diffs, 118 kB of issue/PR text, 64 kB of discussion (79 comments), 96 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch. |
What does this PR do?
Plugin-registered web providers (any provider contributed via the plugin system rather than bundled into core) are correctly discovered and correctly registered, but they are unreachable from backend selection in
tools/web_tools.py, because backend selection can run before plugin discovery has populated the registry. The user-facing symptom is a silent fallback to the sharedweb.backend, which for extract-only plugin providers means landing on a search-only backend (e.g.searxng) and returning a "search-only backend" error forweb_extract.The specific fail-case: the hermes-plugin-crawl4ai (https://github.com/mblauser/hermes-plugin-crawl4ai) plugin, set as
web.extract_backend: crawl4ai, which every diagnostic said was working (installs cleanly, loads cleanly, registers itself, reportssupports_extract() = Trueandis_available() = True), yet every extraction fell through to searxng with "SearXNG is a search-only backend and cannot extract URL content."This is worth fixing because the plugin pattern is the project's intended extension path for web providers (per AGENTS.md: "capability lives at the edges"). A plugin that loads and registers correctly but is silently unreachable from selection is a confusing failure mode. Every diagnostic the user has says "working", and the tool still errors out.
The fix moves
_ensure_web_plugins_loaded()into the two chokepoint functions (_is_backend_available()and_get_backend()) that all backend-resolution paths pass through, rather than placing it in one specific caller. This covers the full bug class (all sibling call paths) not just the symptom hit.Related Issue
No existing issue tracks this. A related PR (#67110) addresses the narrower symptom for extract backends only. This PR supersedes that approach by fixing at the shared chokepoint, covering all backend resolution paths including
_get_backend(),check_web_api_key, and any future callers.Type of Change
Changes Made
tools/web_tools.py: added_ensure_web_plugins_loaded()at the top of_is_backend_available()(line ~324) and_get_backend()(line ~230). Removed the call previously added at_get_capability_backend()from the initial attempt (net +2 lines, -3 lines).How to Test
Prerequisites: a plugin-registered web provider (e.g. crawl4ai) configured as
web.extract_backendwith a non-pluginweb.backend(e.g. searxng).Run backend selection check:
Expected:
crawl4ai(the plugin-registered provider), notsearxng.Run availability check:
Expected:
True.Run end-to-end extraction:
Expected: no error, title returned.
Verify regression (search still works via the configured
web.backend):Expected:
success: Truewith at least 1 result.Root cause (for reviewers)
The order in which things load is the problem:
Line 10 (
_ensure_web_plugins_loaded()) is already called insideweb_search_tool()andweb_extract_tool(), but both sit after the backend-selection call. The existing placement makes dispatch work once a backend is chosen, but not the choice itself. That is the gap this fix corrects.For backends in
_LEGACY_WEB_BACKENDS(the bundled providers),_is_backend_available()uses a cheap hardcoded env-var probe, so they never hit this. For anything else (i.e. any plugin-registered provider) it delegates to the registry, which is empty at that point. The fix moves plugin discovery before registry reads, so the order becomes correct.Why this approach over a narrower fix
An earlier sibling PR (#67110) adds
_ensure_web_plugins_loaded()only to the extract-backend resolution path. This PR goes further by fixing at the two chokepoints the file already documents as the shared resolution paths:_is_backend_available(): its docstring calls it "the single chokepoint through which_get_backend,_get_capability_backend, andcheck_web_api_keyall resolve availability"_get_backend(): the shared fallback that any caller reaches when no per-capability override is set or when an override fails availabilityThis covers every documented caller, not just extract, and satisfies the AGENTS.md rubric: fixes must "fix the whole bug class (sibling call paths included) not just the one site the reporter hit."
Why this is low risk
_ensure_web_plugins_loaded()is idempotent and caches after the first call; subsequent calls are a boolean check. The xai probe comment at line 349 notes_is_backend_available()runs on everyhermes toolsrepaint, so the cache matters and it holds. No change to selection semantics, fallback priority, or bundled-provider behavior. Backends in_LEGACY_WEB_BACKENDSare resolved via hardcoded env-var probes and never touch the registry.Verification
End-to-end against a live crawl4ai instance with
web.extract_backend: crawl4aiandweb.backend: searxng:_get_extract_backend()returnssearxng(wrong)crawl4ai(correct)_is_backend_available('crawl4ai')FalseTrueweb_extract_tool('https://example.com')web_search_tool('test', limit=2)via searxngChecklist
Code
fix(web-tools): ensure plugins loaded before backend selection for plugin-registered providers)pytest tests/ -qand all tests pass (untested; relied on manual end-to-end verification against a live instance)Documentation & Housekeeping
docs/, docstrings) -- this PR body documents the fix reasoning. No README or docstring changes needed.Files changed
tools/web_tools.py: +2 lines, −3 lines (revert of first attempt)