Skip to content

fix: restrict provider URL detection to exact hostname matches (salvage #13229) - #13302

Merged
teknium1 merged 3 commits into
mainfrom
hermes/hermes-4a3f7f64
Apr 21, 2026
Merged

fix: restrict provider URL detection to exact hostname matches (salvage #13229)#13302
teknium1 merged 3 commits into
mainfrom
hermes/hermes-4a3f7f64

Conversation

@teknium1

@teknium1 teknium1 commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

Salvages #13229 (@Aslaaen) and sweeps the same substring → hostname class across the entire codebase, not just OpenAI/xAI/Anthropic.

Summary

Every live-routing site that used "provider.host" in url to detect a known provider now parses the hostname. Substring matching treats attacker- or proxy-controlled paths/hosts like https://evil/openrouter.ai/v1 or https://api.openai.com.example/v1 as native endpoints, leading to wrong api_mode, wrong auth keys, wrong provider-specific headers (codex Cloudflare tokens, copilot tokens), or wrong billing routes.

Commits (in order)

  1. 6a78719 (@Aslaaen, original) — _detect_api_mode_for_url, AIAgent xAI branch, _is_direct_openai_url, is_xai_responses.
  2. 88e8dc0 — finishes the OpenAI/xAI/Anthropic class. Covers the remaining 9 call sites for those three providers and consolidates the hostname helper into utils.base_url_hostname (single source of truth).
  3. 00913c9 — adds utils.base_url_host_matches(base_url, domain) and sweeps every remaining provider-host substring check in live-routing code. 26 call sites across 14 files.

Providers now hardened

OpenAI, xAI, Anthropic (original PR + commit 2), OpenRouter, GitHub Copilot, Kimi, Qwen (portal), ChatGPT/Codex, Bedrock, GitHub Models, Vercel AI Gateway, Nous Research, Z.AI, Moonshot, Arcee, MiniMax.

What was vulnerable before

Site Vulnerability
hermes_cli/runtime_provider._is_openrouter_url https://evil/openrouter.ai/v1 would get OPENROUTER_API_KEY attached (issue #420, #560 class)
cli.py API-key selector Same as above
run_agent.py header cascade × 2 Codex Cloudflare token, Copilot token, Kimi User-Agent could attach to wrong endpoint
agent/auxiliary_client.py header/client cascades × 7 Same header-leak class for auxiliary client
agent/usage_pricing.resolve_billing_route Cost estimates routed to wrong provider's pricing
trajectory_compressor._detect_provider 8-provider cascade could pick wrong provider for compression
hermes_cli/providers.determine_api_mode Bedrock / anthropic detection
tools/delegate_tool Subagent Codex routing
hermes_cli/doctor /models probe Kimi header
cli.py + gateway/run.py /model switch hints Cache-enabled hint for OpenRouter + Claude

New helpers in utils

  • base_url_hostname(url) — returns the lowercased hostname, empty if absent. Handles bare hosts, full URLs, trailing dots, ports, whitespace.
  • base_url_host_matches(url, domain) — safe counterpart to domain in url. True on exact hostname and subdomain matches; False on path segments, host suffixes, host prefixes, empty inputs.

Bedrock detection tightened from "bedrock-runtime" in url to hostname starts with bedrock-runtime. AND host is under amazonaws.com. ChatGPT/Codex detection tightened from "chatgpt.com/backend-api/codex" in url to hostname is chatgpt.com AND path contains /backend-api/codex.

Tests

  • tests/test_base_url_hostname.py — 18 tests covering both helpers: path-contains-host, host-suffix, host-prefix (fake-openrouter.ai), trailing dot, port, case, empty inputs, subdomain-matches.
  • tests/hermes_cli/test_determine_api_mode_hostname.py — 6 tests for OpenAI/Anthropic determine_api_mode plus third-party /anthropic path-suffix convention.
  • Original @Aslaaen tests unchanged — still pass.

Validation

Result
Hostname regression tests (new) 44 pass
runtime_provider, minimax, bedrock, gemini, auxiliary 312 pass
codex_cloudflare, usage_pricing, compressor, fallback_model 47 pass
openai_client_lifecycle, provider_parity, cli_provider_resolution 132 pass
delegate, credential_pool, context_compressor 165 pass
Full gateway suite (3494 total) 3494 pass — 7 pre-existing flakes unrelated to this PR (telegram approval buttons, test_signal / internal_event_bypass_pairing xdist ordering flakes present on plain main)
E2E call-site behavior (live imports, 26 assertions, 6 modules) OK
py_compile across 14 touched source files OK

Pre-existing failures confirmed on main (unrelated): test_compatible_custom_providers_prefers_api, TestGeminiModelCatalog::test_provider_models_exist, TestHuggingFaceModels::test_model_metadata_has_context_lengths.

Closes #13229.

@github-actions

Copy link
Copy Markdown
Contributor

🚨 CRITICAL Supply Chain Risk Detected

This PR contains a pattern that has been used in real supply chain attacks. A maintainer must review the flagged code carefully before merging.

🚨 CRITICAL: Install-hook file added or modified

These files can execute code during package installation or interpreter startup.

Files:

hermes_cli/setup.py

Scanner only fires on high-signal indicators: .pth files, base64+exec/eval combos, subprocess with encoded commands, or install-hook files. Low-signal warnings were removed intentionally — if you're seeing this comment, the finding is worth inspecting.

Aslaaen and others added 3 commits April 20, 2026 22:13
…ites

Aslaaen's fix in the original PR covered _detect_api_mode_for_url and the
two openai/xai sites in run_agent.py. This finishes the sweep: the same
substring-match false-positive class (e.g. https://api.openai.com.evil/v1,
https://proxy/api.openai.com/v1, https://api.anthropic.com.example/v1)
existed in eight more call sites, and the hostname helper was duplicated
in two modules.

- utils: add shared base_url_hostname() (single source of truth).
- hermes_cli/runtime_provider, run_agent: drop local duplicates, import
  from utils. Reuse the cached AIAgent._base_url_hostname attribute
  everywhere it's already populated.
- agent/auxiliary_client: switch codex-wrap auto-detect, max_completion_tokens
  gate (auxiliary_max_tokens_param), and custom-endpoint max_tokens kwarg
  selection to hostname equality.
- run_agent: native-anthropic check in the Claude-style model branch
  and in the AIAgent init provider-auto-detect branch.
- agent/model_metadata: Anthropic /v1/models context-length lookup.
- hermes_cli/providers.determine_api_mode: anthropic / openai URL
  heuristics for custom/unknown providers (the /anthropic path-suffix
  convention for third-party gateways is preserved).
- tools/delegate_tool: anthropic detection for delegated subagent
  runtimes.
- hermes_cli/setup, hermes_cli/tools_config: setup-wizard vision-endpoint
  native-OpenAI detection (paired with deduping the repeated check into
  a single is_native_openai boolean per branch).

Tests:
- tests/test_base_url_hostname.py covers the helper directly
  (path-containing-host, host-suffix, trailing dot, port, case).
- tests/hermes_cli/test_determine_api_mode_hostname.py adds the same
  regression class for determine_api_mode, plus a test that the
  /anthropic third-party gateway convention still wins.

Also: add asslaenn5@gmail.com → Aslaaen to scripts/release.py AUTHOR_MAP.
Completes the hostname-hardening sweep — every substring check against a
provider host in live-routing code is now hostname-based. This closes the
same false-positive class for OpenRouter, GitHub Copilot, Kimi, Qwen,
ChatGPT/Codex, Bedrock, GitHub Models, Vercel AI Gateway, Nous, Z.AI,
Moonshot, Arcee, and MiniMax that the original PR closed for OpenAI, xAI,
and Anthropic.

New helper:
- utils.base_url_host_matches(base_url, domain) — safe counterpart to
  'domain in base_url'. Accepts hostname equality and subdomain matches;
  rejects path segments, host suffixes, and prefix collisions.

Call sites converted (real-code only; tests, optional-skills, red-teaming
scripts untouched):

run_agent.py (10 sites):
- AIAgent.__init__ Bedrock branch, ChatGPT/Codex branch (also path check)
- header cascade for openrouter / copilot / kimi / qwen / chatgpt
- interleaved-thinking trigger (openrouter + claude)
- _is_openrouter_url(), _is_qwen_portal()
- is_native_anthropic check
- github-models-vs-copilot detection (3 sites)
- reasoning-capable route gate (nousresearch, vercel, github)
- codex-backend detection in API kwargs build
- fallback api_mode Bedrock detection

agent/auxiliary_client.py (7 sites):
- extra-headers cascades in 4 distinct client-construction paths
  (resolve custom, resolve auto, OpenRouter-fallback-to-custom,
  _async_client_from_sync, resolve_provider_client explicit-custom,
  resolve_auto_with_codex)
- _is_openrouter_client() base_url sniff

agent/usage_pricing.py:
- resolve_billing_route openrouter branch

agent/model_metadata.py:
- _is_openrouter_base_url(), Bedrock context-length lookup

hermes_cli/providers.py:
- determine_api_mode Bedrock heuristic

hermes_cli/runtime_provider.py:
- _is_openrouter_url flag for API-key preference (issues #420, #560)

hermes_cli/doctor.py:
- Kimi User-Agent header for /models probes

tools/delegate_tool.py:
- subagent Codex endpoint detection

trajectory_compressor.py:
- _detect_provider() cascade (8 providers: openrouter, nous, codex, zai,
  kimi-coding, arcee, minimax-cn, minimax)

cli.py, gateway/run.py:
- /model-switch cache-enabled hint (openrouter + claude)

Bedrock detection tightened from 'bedrock-runtime in url' to
'hostname starts with bedrock-runtime. AND host is under amazonaws.com'.
ChatGPT/Codex detection tightened from 'chatgpt.com/backend-api/codex in
url' to 'hostname is chatgpt.com AND path contains /backend-api/codex'.

Tests:
- tests/test_base_url_hostname.py extended with a base_url_host_matches
  suite (exact match, subdomain, path-segment rejection, host-suffix
  rejection, host-prefix rejection, empty-input, case-insensitivity,
  trailing dot).

Validation: 651 targeted tests pass (runtime_provider, minimax, bedrock,
gemini, auxiliary, codex_cloudflare, usage_pricing, compressor_fallback,
fallback_model, openai_client_lifecycle, provider_parity, cli_provider_resolution,
delegate, credential_pool, context_compressor, plus the 4 hostname test
modules). 26-assertion E2E call-site verification across 6 modules passes.
@teknium1
teknium1 force-pushed the hermes/hermes-4a3f7f64 branch from 00913c9 to 5c0e695 Compare April 21, 2026 05:14
@teknium1
teknium1 merged commit dbb7e00 into main Apr 21, 2026
5 of 7 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

🚨 CRITICAL Supply Chain Risk Detected

This PR contains a pattern that has been used in real supply chain attacks. A maintainer must review the flagged code carefully before merging.

🚨 CRITICAL: Install-hook file added or modified

These files can execute code during package installation or interpreter startup.

Files:

hermes_cli/setup.py

Scanner only fires on high-signal indicators: .pth files, base64+exec/eval combos, subprocess with encoded commands, or install-hook files. Low-signal warnings were removed intentionally — if you're seeing this comment, the finding is worth inspecting.

@teknium1
teknium1 deleted the hermes/hermes-4a3f7f64 branch April 21, 2026 05:14
r266-tech added a commit to r266-tech/hermes-agent that referenced this pull request Jun 7, 2026
…known-provider branch

NousResearch#13302 swept the substring->hostname URL check across the codebase but missed
determine_api_mode()'s known-provider branch (get_provider(provider) is not
None), which still does `"api.openai.com" in url_lower`. The sibling
unknown-provider branch right below already uses
`base_url_hostname(...) == "api.openai.com"`.

A known provider behind a path-style proxy (".../api.openai.com/v1") or a
lookalike host ("api.openai.com.evil/v1") is therefore given the wrong api_mode
(codex_responses / anthropic_messages instead of the transport default) -> hard
400/404 on the wire after a /model switch. Reachable from switch_model()
(agent_runtime_helpers.py) and model_switch.py with a real provider.

Match on the parsed hostname, mirroring the sibling branch's checks
(base_url_hostname already imported -> zero new deps); base_url_hostname
normalises scheme-less hosts so those still route correctly. The /anthropic
path-suffix convention is preserved. Adds TestKnownProviderHostHardening
(known-provider branch incl. scheme-less + path-style + lookalike cases).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants