hermes - 💡(How to fix) Fix [Bug]: Custom web search backends silently fall through to DDGS due to hardcoded allowlist

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

Error Message

Collecting debug report... Traceback (most recent call last): File "/Users/paulie/.hermes/hermes-agent/venv/bin/hermes", line 10, in <module> sys.exit(main()) ^^^^^^ File "/Users/paulie/.hermes/hermes-agent/hermes_cli/main.py", line 14612, in main args.func(args) File "/Users/paulie/.hermes/hermes-agent/hermes_cli/main.py", line 6310, in cmd_debug run_debug(args) File "/Users/paulie/.hermes/hermes-agent/hermes_cli/debug.py", line 725, in run_debug run_debug_share(args) File "/Users/paulie/.hermes/hermes-agent/hermes_cli/debug.py", line 599, in run_debug_share dump_text = _capture_dump() ^^^^^^^^^^^^^^^ File "/Users/paulie/.hermes/hermes-agent/hermes_cli/debug.py", line 523, in _capture_dump run_dump(_FakeArgs()) File "/Users/paulie/.hermes/hermes-agent/hermes_cli/dump.py", line 329, in run_dump lines.append(f" mcp_servers: {_count_mcp_servers(config)}") ^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/paulie/.hermes/hermes-agent/hermes_cli/dump.py", line 104, in _count_mcp_servers servers = mcp.get("servers", {}) ^^^^^^^ AttributeError: 'NoneType' object has no attribute 'get'

Root Cause

Root Cause Analysis (optional)

Fix Action

Fix / Workaround

web_extract() and web_search() should dispatch to the provider registered in agent/web_search_registry.py, not only to names in a hardcoded allowlist. A new backend plugin should work with zero code changes to tools/web_tools.py.

The dispatch gate at _get_backend() (tools/web_tools.py, line 143) has a hardcoded set: {"parallel", "firecrawl", "tavily", "exa", "searxng", "brave-free", "ddgs", "xai"} When web.backend is set to a name NOT in this set, the condition falls through to a legacy env-var candidate walk that picks DDGS as the no-key default. The web_search_registry correctly knows about the custom provider, but _get_backend() never asks it.

Option B from the analysis: in _get_backend(), when configured is not in the hardcoded set but is non-empty, fall through to the registry: if configured in {…}: return configured

  • if configured:
  •    from agent.web_search_registry import get_provider
  •    if get_provider(configured):
  •        return configured

This makes the allowlist a fast-path (no import overhead for bundled backends) rather than a gate. Any plugin that correctly registers via register_web_search_provider() will be dispatched to without code changes.

Code Example

Collecting debug report...
Traceback (most recent call last):
  File "/Users/paulie/.hermes/hermes-agent/venv/bin/hermes", line 10, in <module>
    sys.exit(main())
             ^^^^^^
  File "/Users/paulie/.hermes/hermes-agent/hermes_cli/main.py", line 14612, in main
    args.func(args)
  File "/Users/paulie/.hermes/hermes-agent/hermes_cli/main.py", line 6310, in cmd_debug
    run_debug(args)
  File "/Users/paulie/.hermes/hermes-agent/hermes_cli/debug.py", line 725, in run_debug
    run_debug_share(args)
  File "/Users/paulie/.hermes/hermes-agent/hermes_cli/debug.py", line 599, in run_debug_share
    dump_text = _capture_dump()
                ^^^^^^^^^^^^^^^
  File "/Users/paulie/.hermes/hermes-agent/hermes_cli/debug.py", line 523, in _capture_dump
    run_dump(_FakeArgs())
  File "/Users/paulie/.hermes/hermes-agent/hermes_cli/dump.py", line 329, in run_dump
    lines.append(f"  mcp_servers:        {_count_mcp_servers(config)}")
                                          ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/paulie/.hermes/hermes-agent/hermes_cli/dump.py", line 104, in _count_mcp_servers
    servers = mcp.get("servers", {})
              ^^^^^^^
AttributeError: 'NoneType' object has no attribute 'get'

---
RAW_BUFFERClick to expand / collapse

Bug Description

When a WebSearchProvider plugin registers a custom backend name (e.g. "pkl-extract") and web.backend is set to that name in config.yaml, web_extract() and web_search() silently route to DDGS instead. The error message the user sees says "DDGS is a search-only backend" — misleading them to debug config/plugin loading when the real cause is a code-level allowlist gap.

Steps to Reproduce

  1. Write a WebSearchProvider subclass that registers as "my-custom-backend" with supports_search=True, supports_extract=True
  2. Place it as a bundled plugin at plugins/web/my_plugin/ with kind: backend in plugin.yaml
  3. Set web.backend: my-custom-backend in ~/.hermes/config.yaml
  4. Confirm registration: log shows "Plugin 'web-my_plugin' registered web provider: my-custom-backend"
  5. Call web_extract("https://example.com")
  6. Actual: Returns {"success": false, "error": "DuckDuckGo (ddgs) is a search-only backend and cannot extract URL content."}
  7. Expected: Routes through my-custom-backend provider, returns markdown content

Expected Behavior

web_extract() and web_search() should dispatch to the provider registered in agent/web_search_registry.py, not only to names in a hardcoded allowlist. A new backend plugin should work with zero code changes to tools/web_tools.py.

Actual Behavior

The dispatch gate at _get_backend() (tools/web_tools.py, line 143) has a hardcoded set: {"parallel", "firecrawl", "tavily", "exa", "searxng", "brave-free", "ddgs", "xai"} When web.backend is set to a name NOT in this set, the condition falls through to a legacy env-var candidate walk that picks DDGS as the no-key default. The web_search_registry correctly knows about the custom provider, but _get_backend() never asks it.

Affected Component

Tools (terminal, file ops, web, code execution, etc.)

Messaging Platform (if gateway-related)

N/A (CLI only)

Debug Report

Collecting debug report...
Traceback (most recent call last):
  File "/Users/paulie/.hermes/hermes-agent/venv/bin/hermes", line 10, in <module>
    sys.exit(main())
             ^^^^^^
  File "/Users/paulie/.hermes/hermes-agent/hermes_cli/main.py", line 14612, in main
    args.func(args)
  File "/Users/paulie/.hermes/hermes-agent/hermes_cli/main.py", line 6310, in cmd_debug
    run_debug(args)
  File "/Users/paulie/.hermes/hermes-agent/hermes_cli/debug.py", line 725, in run_debug
    run_debug_share(args)
  File "/Users/paulie/.hermes/hermes-agent/hermes_cli/debug.py", line 599, in run_debug_share
    dump_text = _capture_dump()
                ^^^^^^^^^^^^^^^
  File "/Users/paulie/.hermes/hermes-agent/hermes_cli/debug.py", line 523, in _capture_dump
    run_dump(_FakeArgs())
  File "/Users/paulie/.hermes/hermes-agent/hermes_cli/dump.py", line 329, in run_dump
    lines.append(f"  mcp_servers:        {_count_mcp_servers(config)}")
                                          ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/paulie/.hermes/hermes-agent/hermes_cli/dump.py", line 104, in _count_mcp_servers
    servers = mcp.get("servers", {})
              ^^^^^^^
AttributeError: 'NoneType' object has no attribute 'get'

Operating System

macOS 15.5 (arm64)

Python Version

3.11.15

Hermes Version

v0.15.1 (2026.5.29)

Additional Logs / Traceback (optional)

Root Cause Analysis (optional)

─ python

tools/web_tools.py, line 142-143:

configured = (_load_web_config().get("backend") or "").lower().strip() if configured in {"parallel", "firecrawl", "tavily", "exa", "searxng", "brave-free", "ddgs", "xai"}: return configured The hardcoded set at line 143 acts as a gate. Any backend name not in this list falls through to the legacy env-var fallback (line ~153), which picks DDGS. The fix that avoids per-backend code changes: when "configured" is a non-empty string not in the known set, check agent.web_search_registry.get_provider(configured). If the provider exists, return "configured" — the allowlist becomes a fast-path for well-known backends instead of an exclusive gate. Reference: agent/web_search_registry.py has get_active_search_provider() and get_active_extract_provider() that already resolve by config name.

Proposed Fix (optional)

Option B from the analysis: in _get_backend(), when configured is not in the hardcoded set but is non-empty, fall through to the registry: if configured in {…}: return configured

  • if configured:
  •    from agent.web_search_registry import get_provider
  •    if get_provider(configured):
  •        return configured

This makes the allowlist a fast-path (no import overhead for bundled backends) rather than a gate. Any plugin that correctly registers via register_web_search_provider() will be dispatched to without code changes.

Are you willing to submit a PR for this?

  • I'd like to fix this myself and submit a PR

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

hermes - 💡(How to fix) Fix [Bug]: Custom web search backends silently fall through to DDGS due to hardcoded allowlist