NousResearch/hermes-agent

Per-provider CA cert settings ignored in metadata probes

Cluster 66544 · agent. Data as of

Nothing open on GitHub. Fix merged in #56681. Check it covers your case.

The short answer

0open issues
0open PRs
5items grouped
Known?
Yes. 5 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #56681. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this resolved, deciding PR #86491. That is separate from the fix event.
Still open
Nothing open on GitHub.

These issues and PRs address a bug where provider-scoped SSL/CA certificate configuration was not being passed through to custom /models and pricing endpoint probes. The work involves threading per-provider TLS settings into those requests.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #56681 fix(agent): honor custom CA certs for custom_providers HTTPS endpoints (main + aux)

    merged to main

    files: agent/agent_init.py, agent/agent_runtime_helpers.py, agent/auxiliary_client.py, agent/process_bootstrap.py, agent/ssl_verify.py, hermes_cli/config.py, run_agent.py, tests/agent/test_auxiliary_client_ssl_verify.py, tests/agent/test_ssl_verify.py, tests/hermes_cli/test_custom_provider_tls.py, tests/run_agent/test_create_openai_client_ssl_verify.py

    Show the diff
    • agent/agent_init.py
      @@ -974,6 +974,21 @@ def _moa_reference_relay(event: str, **kwargs: Any) -> None:
               # this mutation is reflected in the client built just below.
               agent._apply_user_default_headers()
       
      +        try:
      +            from hermes_cli.config import (
      +                apply_custom_provider_tls_to_client_kwargs,
      +                get_compatible_custom_providers,
      +                load_config,
      +            )
      +
      +            apply_custom_provider_tls_to_client_kwargs(
      +                client_kwargs,
      +                str(client_kwargs.get("base_url") or agent.base_url or ""),
      +                get_compatible_custom_providers(load_config()),
      +            )
      +        except Exception:
      +            logger.debug("custom-provider TLS resolution skipped", exc_info=True)
      +
               agent.api_key = [redacted:secret-assignment]"api_key", "")
               agent.base_url = client_kwargs.get("base_url", agent.base_url)
               try:
    • agent/agent_runtime_helpers.py
      @@ -1513,6 +1513,7 @@ def anthropic_prompt_cache_policy(
       
       def create_openai_client(agent, client_kwargs: dict, *, reason: str, shared: bool) -> Any:
           from agent.auxiliary_client import _validate_base_url, _validate_proxy_env_urls
      +    from agent.ssl_verify import resolve_httpx_verify
           # Treat client_kwargs as read-only. Callers pass agent._client_kwargs (or shallow
           # copies of it) in; any in-place mutation leaks back into the stored dict and is
           # reused on subsequent requests. #10933 hit this by injecting an httpx.Client
      @@ -1522,6 +1523,9 @@ def create_openai_client(agent, client_kwargs: dict, *, reason: str, shared: boo
           # copy locks the contract so future transport/keepalive work can't reintroduce
           # the same class of bug.
           client_kwargs = dict(client_kwargs)
      +    ssl_ca_cert = client_kwargs.pop("ssl_ca_cert", None)
      +    ssl_verify_cfg = client_kwargs.pop("ssl_verify", None)
      +    httpx_verify = resolve_httpx_verify(ca_bundle=ssl_ca_cert, ssl_verify=ssl_verify_cfg)
           _validate_proxy_env_urls()
           _validate_base_url(client_kwargs.get("base_url"))
           if agent.provider == "copilot-acp" or str(client_kwargs.get("base_url", "")).startswith("acp://copilot"):
      @@ -1545,7 +1549,9 @@ def create_openai_client(agent, client_kwargs: dict, *, reason: str, shared: boo
                       if k in {"api_key", "base_url", "default_headers", "timeout", "http_client"}
                   }
                   if "http_client" not in safe_kwargs:
      -                keepalive_http = agent._build_keepalive_http_client(base_url)
      +                keepalive_http = agent._build_keepalive_http_client(
      +                    base_url, verify=httpx_verify,
      +                )
                       if keepalive_http is not None:
                           safe_kwargs["http_client"] = keepalive_http
                   client = GeminiNativeClient(**safe_kwargs)
      @@ -1574,7 +1580,9 @@ def create_openai_client(agent, client_kwargs: dict, *, reason: str, shared: boo
           # Tests in ``tests/run_agent/test_create_openai_client_reuse.py`` and
           # ``tests/run_agent/test_sequential_chats_live.py`` pin this invariant.
           if "http_client" not in client_kwargs:
      -        keepalive_http = agent._build_keepalive_http_client(client_kwargs.get("base_url", ""))
      +        keepalive_http = agent._build_keepalive_http_client(
      +            client_kwargs.get("base_url", ""), verify=httpx_verify,
      +        )
               if keepalive_http is not None:
                   client_kwargs["http_client"] = keepalive_http
           # Delegate all rate-limit / 5xx retry to hermes's outer conversation loop,
      @@ -1778,6 +1786,24 @@ def switch_model(agent, new_model, new_provider, api_key='', base_url='', api_mo
                       "api_key": effective_key,
                       "base_url": effective_base,
                   }
      +            try:
      +                from hermes_cli.config import (
      +                    apply_custom_provider_tls_to_client_kwargs,
      +                    get_compatible_custom_providers,
      +                    load_config_readonly,
      +                )
      +
      +                # Read custom_providers from live config (not the init-time
      +                # snapshot on ``agent._custom_providers``) so ssl_ca_cert /
      +                # ssl_verify edits are honored when switching mid-session,
      +                # matching the context-length reload below (#15779).
      +                apply_custom_provider_tls_to_client_kwargs(
      +                    agent._client_kwargs,
      +                    str(effective_base or ""),
      +                    get_compatible_custom_providers(load_config_readonly()),
      +                )
      +            except Exception:
      +                logger.debug("custom-provider TLS resolution skipped on switch_model", exc_info=True)
                   _sm_timeout = get_provider_request_timeout(agent.provider, agent.model)
                   if _sm_timeout is not None:
                       agent._client_kwargs["timeout"] = _sm_timeout
    • agent/auxiliary_client.py
      @@ -128,13 +128,45 @@ def __repr__(self):
       _LOGGED_UNSUPPORTED_OAUTH_KEYS: set = set()
       
       
      +def _resolve_aux_verify(base_url: Optional[str]) -> Any:
      +    """Resolve httpx ``verify`` for an auxiliary-client base_url.
      +
      +    Mirrors the main client's TLS resolution so auxiliary calls (compression,
      +    vision, web_extract, title generation, etc.) honor per-provider
      +    ``ssl_ca_cert`` / ``ssl_verify`` config and the ``HERMES_CA_BUNDLE`` /
      +    ``SSL_CERT_FILE`` env conventions. Best-effort: any failure falls back to
      +    the httpx/certifi default (``True``).
      +    """
      +    try:
      +        from agent.ssl_verify import resolve_httpx_verify
      +        from hermes_cli.config import (
      +            get_custom_provider_tls_settings,
      +            load_config_readonly,
      +        )
      +
      +        tls = get_custom_provider_tls_settings(
      +            str(base_url or ""), config=load_config_readonly()
      +        )
      +        return resolve_httpx_verify(
      +            ca_bundle=tls.get("ssl_ca_cert"),
      +            ssl_verify=tls.get("ssl_verify"),
      +            base_url=str(base_url or ""),
      +        )
      +    except Exception:
      +        return True
      +
      +
       def _openai_http_client_kwargs(
           base_url: Optional[str],
           *,
           async_mode: bool = False,
       ) -> Dict[str, Any]:
           """Inject keepalive httpx client with env-only proxy (not macOS system proxy)."""
      -    client = build_keepalive_http_client(str(base_url or ""), async_mode=async_mode)
      +    client = build_keepalive_http_client(
      +        str(base_url or ""),
      +        async_mode=async_mode,
      +        verify=_resolve_aux_verify(base_url),
      +    )
           if client is None:
               return {}
           return {"http_client": client}
    • agent/process_bootstrap.py
      @@ -146,6 +146,7 @@ def build_keepalive_http_client(
           base_url: str = "",
           *,
           async_mode: bool = False,
      +    verify: Any = True,
       ) -> Optional[Any]:
           """Build an httpx client for OpenAI SDK calls with env-only proxy policy.
       
      @@ -154,6 +155,13 @@ def build_keepalive_http_client(
           ``trust_env`` path, so macOS system proxy settings from
           ``urllib.request.getproxies()`` (which omit the ExceptionsList) are not
           applied. Mirrors ``AIAgent._build_keepalive_http_client``.
      +
      +    ``verify`` is forwarded to httpx so auxiliary-client calls (compression,
      +    vision, web_extract, title generation, etc.) honor the same per-provider
      +    ``ssl_ca_cert`` / ``ssl_verify`` and ``HERMES_CA_BUNDLE`` settings the main
      +    client uses. It is passed on the ``HTTPTransport`` (which owns the SSL
      +    context when a custom transport is supplied) and, for the copilot branch
      +    that has no custom transport, on the client itself.
           """
           try:
               import httpx
      @@ -161,7 +169,7 @@ def build_keepalive_http_client(
       
               if "api.githubcopilot.com" in str(base_url or "").lower():
                   client_cls = httpx.AsyncClient if async_mode else httpx.Client
      -            return client_cls()
      +            return client_cls(verify=verify)
       
               sock_opts = [(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)]
               if hasattr(socket, "TCP_KEEPIDLE"):
      @@ -174,8 +182,10 @@ def build_keepalive_http_client(
               proxy = _get_proxy_for_base_url(base_url)
               transport_cls = httpx.AsyncHTTPTransport if async_mode else httpx.HTTPTransport
               client_cls = httpx.AsyncClient if async_mode else httpx.Client
      +        # verify lives on the transport: httpx ignores the client-level
      +        # ``verify`` when a custom ``transport=`` is supplied.
               return client_cls(
      -            transport=transport_cls(socket_options=sock_opts),
      +            transport=transport_cls(socket_options=sock_opts, verify=verify),
                   proxy=proxy,
               )
           except Exception:
    • agent/ssl_verify.py
      @@ -0,0 +1,63 @@
      +"""TLS verify resolution for httpx/OpenAI provider clients."""
      +
      +from __future__ import annotations
      +
      +import logging
      +import os
      +import ssl
      +from pathlib import Path
      +from typing import Any, Optional
      +
      +logger = logging.getLogger(__name__)
      +
      +
      +def _coerce_insecure(ssl_verify: Any) -> bool:
      +    if ssl_verify is False:
      +        return True
      +    if isinstance(ssl_verify, str) and ssl_verify.strip().lower() in {"false", "0", "no", "off"}:
      +        return True
      +    return False
      +
      +
      +def resolve_httpx_verify(
      +    *,
      +    ca_bundle: Optional[str] = None,
      +    ssl_verify: Any = None,
      +    base_url: str = "",
      +) -> bool | ssl.SSLContext:
      +    """Resolve httpx ``verify`` for provider HTTP clients.
      +
      +    Priority:
      +    1. ``ssl_verify: false`` — disable verification (local dev only)
      +    2. explicit ``ca_bundle`` (per-provider ``ssl_ca_cert`` config field)
      +    3. ``HERMES_CA_BUNDLE``, ``SSL_CERT_FILE``, ``REQUESTS_CA_BUNDLE``,
      +       ``CURL_CA_BUNDLE`` env vars
      +    4. ``True`` (httpx/certifi default)
      +
      +    ``base_url`` is used only for the insecure-mode warning message.
      +    """
      +    if _coerce_insecure(ssl_verify):
      +        logger.warning(
      +            "TLS certificate verification DISABLED (ssl_verify: false) for %s — "
      +            "this is intended for local development only and is unsafe on any "
      +            "network you do not fully control.",
      +            base_url or "a custom provider endpoint",
      +        )
      +        return False
      +
      +    effective_ca = (
      +        (ca_bundle or "").strip()
      +        or os.getenv("HERMES_CA_BUNDLE", "").strip()
      +        or os.getenv("SSL_CERT_FILE", "").strip()
      +        or os.getenv("REQUESTS_CA_BUNDLE", "").strip()
      +        or os.getenv("CURL_CA_BUNDLE", "").strip()
      +    )
      +    if effective_ca:
      +        ca_path = str(Path(effective_ca).expanduser())
      +        if os.path.isfile(ca_path):
      +            return ssl.create_default_context(cafile=ca_path)
      +        logger.warning(
      +            "CA bundle path does not exist: %s — falling back to default certificates",
      +            effective_ca,
      +        )
      +    return True
    • hermes_cli/config.py
      @@ -4478,7 +4478,7 @@ def _normalize_custom_provider_entry(
               "api_mode", "transport", "model", "default_model", "models",
               "context_length", "rate_limit_delay",
               "request_timeout_seconds", "stale_timeout_seconds",
      -        "discover_models", "extra_body",
      +        "discover_models", "extra_body", "ssl_ca_cert", "ssl_verify",
           }
           for camel, snake in _CAMEL_ALIASES.items():
               if camel in entry and snake not in entry:
      @@ -4585,6 +4585,16 @@ def _normalize_custom_provider_entry(
           if isinstance(extra_body, dict):
               normalized["extra_body"] = dict(extra_body)
       
      +    ssl_ca_cert = entry.get("ssl_ca_cert")
      +    if isinstance(ssl_ca_cert, str) and ssl_ca_cert.strip():
      +        normalized["ssl_ca_cert"] = ssl_ca_cert.strip()
      +
      +    ssl_verify = entry.get("ssl_verify")
      +    if isinstance(ssl_verify, bool):
      +        normalized["ssl_verify"] = ssl_verify
      +    elif isinstance(ssl_verify, str) and ssl_verify.strip():
      +        normalized["ssl_verify"] = ssl_verify.strip()
      +
           return normalized
       
       
      @@ -4612,6 +4622,8 @@ def _custom_provider_entry_to_provider_config(
               "rate_limit_delay",
               "discover_models",
               "extra_body",
      +        "ssl_ca_cert",
      +        "ssl_verify",
           ):
               if field in normalized:
                   provider_entry[field] = normalized[field]
      @@ -4688,6 +4700,71 @@ def _append_if_new(entry: Optional[Dict[str, Any]]) -> None:
           return compatible
       
       
      +def _coerce_ssl_verify(value: Any) -> Optional[bool]:
      +    if value is None:
      +        return None
      +    if isinstance(value, bool):
      +        return value
      +    if isinstance(value, str):
      +        lowered = value.strip().lower()
      +        if lowered in {"false", "0", "no", "off"}:
      +            return False
      +        if lowered in {"true", "1", "yes", "on"}:
      +            return True
      +    return None
      +
      +
      +def get_custom_provider_tls_settings(
      +    base_url: str,
      +    custom_providers: Optional[List[Dict[str, Any]]] = None,
      +    config: Optional[Dict[str, Any]] = None,
      +) -> Dict[str, Any]:
      +    """Return TLS settings from a matching ``custom_providers`` / ``providers`` entry."""
      +    if custom_providers is None:
      +        try:
      +            custom_providers = get_compatible_custom_providers(config)
      +        except Exception:
      +            custom_providers = []
      +    if not base_url or not isinstance(custom_providers, list):
      +        return {}
      +
      +    # Case-insensitive compare: elsewhere custom_providers are keyed on a
      +    # lowercased base_url (see get_compatible_custom_providers dedup), and
      +    # scheme/host are case-insensitive anyway — so a config entry written as
      +    # https://Ollama.Example.com/v1 must still match a lowercased runtime
      +    # base_url. Exact match after rstrip('/') + lower() (no prefix/substring).
      +    target_url = (base_url or "").rstrip("/").lower()
      +    for entry in custom_providers:
      +        if not isinstance(entry, dict):
      +            continue
      +        entry_url = (entry.get("base_url") or "").rstrip("/").lower()
      +        if not entry_url or entry_url != target_url:
      +            continue
      +        out: Dict[str, Any] = {}
      +        ca = entry.get("ssl_ca_cert")
      +        if isinstance(ca, str) and ca.strip():
      +            out["ssl_ca_cert"] = ca.strip()
      +        verify = _coerce_ssl_verify(entry.get("ssl_verify"))
      +        if verify is not None:
      +            out["ssl_verify"] = verify
      +        return out
      +    return {}
      +
      +
      +def apply_custom_provider_tls_to_client_kwargs(
      +    client_kwargs: Dict[str, Any],
      +    base_url: str,
      +    custom_providers: Optional[List[Dict[str, Any]]] = None,
      +    config: Optional[Dict[str, Any]] = None,
      +) -> None:
      +    """Attach per-provider TLS knobs to OpenAI client kwargs when matched."""
      +    tls = get_custom_provider_tls_settings(base_url, custom_providers, config)
      +    if tls.get("ssl_ca_cert"):
      +        client_kwargs["ssl_ca_cert"] = tls["ssl_ca_cert"]
      +    if "ssl_verify" in tls:
      +        client_kwargs["ssl_verify"] = tls["ssl_verify"]
      +
      +
       def get_custom_provider_context_length(
           model: str,
           base_url: str,
      @@ -4813,6 +4890,7 @@ def check_config_version() -> Tuple[int, int]:
       _VALID_CUSTOM_PROVIDER_FIELDS = {
           "name", "base_url", "api_key", "api_mode", "model", "models",
           "context_length", "rate_limit_delay", "extra_body",
      +    "ssl_ca_cert", "ssl_verify",
           # key_env is read at runtime by runtime_provider.py and auxiliary_client.py
           # — include it here so the set accurately describes the supported schema.
           "key_env",
    • run_agent.py
      @@ -3884,13 +3884,13 @@ def _is_openai_client_closed(client: Any) -> bool:
               return False
       
           @staticmethod
      -    def _build_keepalive_http_client(base_url: str = "") -> Any:
      +    def _build_keepalive_http_client(base_url: str = "", *, verify: Any = True) -> Any:
               try:
                   import httpx as _httpx
                   import socket as _socket
       
                   if "api.githubcopilot.com" in str(base_url or "").lower():
      -                return _httpx.Client()
      +                return _httpx.Client(verify=verify)
       
                   _sock_opts = [(_socket.SOL_SOCKET, _socket.SO_KEEPALIVE, 1)]
                   if hasattr(_socket, "TCP_KEEPIDLE"):
      @@ -3904,8 +3904,10 @@ def _build_keepalive_http_client(base_url: str = "") -> Any:
                   # Explicitly read proxy settings while still honoring NO_PROXY for
                   # loopback / local endpoints such as a locally hosted sub2api.
                   _proxy = _get_proxy_for_base_url(base_url)
      +            # verify lives on the transport: httpx ignores the client-level
      +            # ``verify`` when a custom ``transport=`` is supplied.
                   return _httpx.Client(
      -                transport=_httpx.HTTPTransport(socket_options=_sock_opts),
      +                transport=_httpx.HTTPTransport(socket_options=_sock_opts, verify=verify),
                       proxy=_proxy,
                   )
               except Exception:
    • tests/agent/test_auxiliary_client_ssl_verify.py
      @@ -0,0 +1,79 @@
      +"""Regression: auxiliary-client keepalive httpx client must honor custom CA bundles.
      +
      +The main OpenAI client resolves per-provider ``ssl_ca_cert`` / ``ssl_verify`` and
      +``HERMES_CA_BUNDLE`` via ``agent.ssl_verify.resolve_httpx_verify``. Auxiliary calls
      +(compression, vision, web_extract, title generation, session_search) build their own
      +keepalive client through ``agent.process_bootstrap.build_keepalive_http_client`` and must
      +apply the same TLS settings — otherwise an HTTPS custom_providers endpoint signed by a
      +private CA works for chat but fails ``APIConnectionError`` on every auxiliary task.
      +"""
      +
      +import ssl
      +
      +import certifi
      +import httpx
      +import pytest
      +
      +from agent.process_bootstrap import build_keepalive_http_client
      +
      +_CA_ENV_VARS = ("HERMES_CA_BUNDLE", "SSL_CERT_FILE", "REQUESTS_CA_BUNDLE", "HTTPS_PROXY")
      +
      +
      [redacted:email]
      +def clean_tls_env(monkeypatch):
      +    for var in _CA_ENV_VARS:
      +        monkeypatch.delenv(var, raising=False)
      +
      +
      +def test_build_keepalive_http_client_forwards_verify_context(clean_tls_env):
      +    ctx = ssl.create_default_context(cafile=certifi.where())
      +    client = build_keepalive_http_client("https://ollama.example.com/v1", verify=ctx)
      +    assert isinstance(client, httpx.Client)
      +    assert client._transport._pool._ssl_context is ctx
      +
      +
      +def test_build_keepalive_http_client_verify_false_disables_hostname_check(clean_tls_env):
      +    client = build_keepalive_http_client("https://ollama.example.com/v1", verify=False)
      +    assert isinstance(client, httpx.Client)
      +    assert client._transport._pool._ssl_context.check_hostname is False
      +
      +
      +def test_build_keepalive_http_client_default_verify_true(clean_tls_env):
      +    client = build_keepalive_http_client("https://ollama.example.com/v1")
      +    assert isinstance(client, httpx.Client)
      +
      +
      +def test_resolve_aux_verify_uses_per_provider_ssl_ca_cert(clean_tls_env, monkeypatch):
      +    """_resolve_aux_verify should mirror the main-client resolution for a matched base_url."""
      +    import hermes_cli.config as cfg
      +    from agent import auxiliary_client
      +
      +    # get_custom_provider_tls_settings is imported inside the function from
      +    # hermes_cli.config, so patch it at the source module.
      +    monkeypatch.setattr(
      +        cfg,
      +        "get_custom_provider_tls_settings",
      +        lambda *a, **k: {"ssl_ca_cert": certifi.where()},
      +    )
      +    verify = auxiliary_client._resolve_aux_verify("https://ollama.example.com/v1")
      +    assert isinstance(verify, ssl.SSLContext)
      +
      +
      +def test_resolve_aux_verify_ssl_verify_false(clean_tls_env, monkeypatch):
      +    import hermes_cli.config as cfg
      +    from agent import auxiliary_client
      +
      +    monkeypatch.setattr(
      +        cfg,
      +        "get_custom_provider_tls_settings",
      +        lambda *a, **k: {"ssl_verify": False},
      +    )
      +    assert auxiliary_client._resolve_aux_verify("https://ollama.example.com/v1") is False
      +
      +
      +def test_resolve_aux_verify_no_match_defaults_true(clean_tls_env, monkeypatch):
      +    import hermes_cli.config as cfg
      +    from agent import auxiliary_client
      +
      +    monkeypatch.setattr(cfg, "get_custom_provider_tls_settings", lambda *a, **k: {})
      +    assert auxiliary_client._resolve_aux_verify("https://openrouter.ai/api/v1") is True
    • tests/agent/test_ssl_verify.py
      @@ -0,0 +1,40 @@
      +"""Tests for agent.ssl_verify.resolve_httpx_verify."""
      +
      +import ssl
      +
      +import certifi
      +import pytest
      +
      +from agent.ssl_verify import resolve_httpx_verify
      +
      +_CA_ENV_VARS = ("HERMES_CA_BUNDLE", "SSL_CERT_FILE", "REQUESTS_CA_BUNDLE")
      +
      +
      [redacted:email]
      +def clean_ca_env(monkeypatch):
      +    for var in _CA_ENV_VARS:
      +        monkeypatch.delenv(var, raising=False)
      +
      +
      +def test_ssl_verify_false_disables_verification(clean_ca_env):
      +    assert resolve_httpx_verify(ssl_verify=False) is False
      +
      +
      +def test_hermes_ca_bundle_returns_ssl_context(clean_ca_env, monkeypatch):
      +    monkeypatch.setenv("HERMES_CA_BUNDLE", certifi.where())
      +    result = resolve_httpx_verify()
      +    assert isinstance(result, ssl.SSLContext)
      +
      +
      +def test_explicit_ca_bundle_param(clean_ca_env):
      +    result = resolve_httpx_verify(ca_bundle=certifi.where())
      +    assert isinstance(result, ssl.SSLContext)
      +
      +
      +def test_missing_ca_bundle_falls_back_to_true(clean_ca_env, monkeypatch):
      +    monkeypatch.setenv("HERMES_CA_BUNDLE", "/nonexistent/root-ca.pem")
      +    assert resolve_httpx_verify() is True
      +
      +
      +def test_default_without_env_is_true(clean_ca_env):
      +    assert resolve_httpx_verify() is True
    • tests/hermes_cli/test_custom_provider_tls.py
      @@ -0,0 +1,72 @@
      +"""Tests for per-provider TLS settings in custom_providers config."""
      +
      +from hermes_cli.config import (
      +    apply_custom_provider_tls_to_client_kwargs,
      +    get_custom_provider_tls_settings,
      +)
      +
      +
      +def test_get_custom_provider_tls_settings_matches_base_url():
      +    providers = [
      +        {
      +            "name": "Ollama",
      +            "base_url": "https://ollama.example.com/v1",
      +            "ssl_ca_cert": "/etc/ssl/mkcert-root.pem",
      +        }
      +    ]
      +    tls = get_custom_provider_tls_settings(
      +        "https://ollama.example.com/v1/",
      +        custom_providers=providers,
      +    )
      +    assert tls == {"ssl_ca_cert": "/etc/ssl/mkcert-root.pem"}
      +
      +
      +def test_apply_custom_provider_tls_to_client_kwargs():
      +    client_kwargs = {"api_key": "x", "base_url": "https://ollama.example.com/v1"}
      +    providers = [
      +        {
      +            "name": "Ollama",
      +            "base_url": "https://ollama.example.com/v1",
      +            "ssl_ca_cert": "/etc/ssl/mkcert-root.pem",
      +            "ssl_verify": True,
      +        }
      +    ]
      +    apply_custom_provider_tls_to_client_kwargs(
      +        client_kwargs,
      +        "https://ollama.example.com/v1",
      +        custom_providers=providers,
      +    )
      +    assert client_kwargs["ssl_ca_cert"] == "/etc/ssl/mkcert-root.pem"
      +    assert client_kwargs["ssl_verify"] is True
      +
      +
      +def test_get_custom_provider_tls_settings_matches_case_insensitively():
      +    """A config base_url with mixed case must still match a lowercased runtime base_url."""
      +    providers = [
      +        {
      +            "name": "Ollama",
      +            "base_url": "https://Ollama.Example.com/v1",
      +            "ssl_ca_cert": "/etc/ssl/mkcert-root.pem",
      +        }
      +    ]
      +    tls = get_custom_provider_tls_settings(
      +        "https://ollama.example.com/v1",
      +        custom_providers=providers,
      +    )
      +    assert tls == {"ssl_ca_cert": "/etc/ssl/mkcert-root.pem"}
      +
      +
      +def test_get_custom_provider_tls_settings_no_substring_bypass():
      +    """A base_url that is only a prefix of an entry must NOT match."""
      +    providers = [
      +        {
      +            "name": "Ollama",
      +            "base_url": "https://ollama.example.com/v1",
      +            "ssl_verify": False,
      +        }
      +    ]
      +    # A different host that shares a prefix must not pick up ssl_verify:false.
      +    assert get_custom_provider_tls_settings(
      +        "https://ollama.example.com.attacker.test/v1",
      +        custom_providers=providers,
      +    ) == {}
    • tests/run_agent/test_create_openai_client_ssl_verify.py
      @@ -0,0 +1,46 @@
      +"""Regression: keepalive httpx client must honor custom CA bundles for HTTPS providers."""
      +
      +import ssl
      +
      +import certifi
      +import httpx
      +import pytest
      +
      +from agent.ssl_verify import resolve_httpx_verify
      +from run_agent import AIAgent
      +
      +_CA_ENV_VARS = ("HERMES_CA_BUNDLE", "SSL_CERT_FILE", "REQUESTS_CA_BUNDLE", "HTTPS_PROXY")
      +
      +
      [redacted:email]
      +def clean_tls_env(monkeypatch):
      +    for var in _CA_ENV_VARS:
      +        monkeypatch.delenv(var, raising=False)
      +
      +
      +def test_build_keepalive_http_client_uses_hermes_ca_bundle(clean_tls_env, monkeypatch):
      +    monkeypatch.setenv("HERMES_CA_BUNDLE", certifi.where())
      +    verify = resolve_httpx_verify()
      +    client = AIAgent._build_keepalive_http_client(
      +        "https://ollama.example.com/v1", verify=verify,
      +    )
      +    assert isinstance(client, httpx.Client)
      +    assert isinstance(client._transport._pool._ssl_context, ssl.SSLContext)
      +
      +
      +def test_build_keepalive_http_client_honors_per_provider_ssl_ca_cert(clean_tls_env):
      +    verify = resolve_httpx_verify(ca_bundle=certifi.where())
      +    client = AIAgent._build_keepalive_http_client(
      +        "https://ollama.example.com/v1", verify=verify,
      +    )
      +    assert isinstance(client, httpx.Client)
      +    assert isinstance(client._transport._pool._ssl_context, ssl.SSLContext)
      +
      +
      +def test_build_keepalive_http_client_ssl_verify_false(clean_tls_env):
      +    verify = resolve_httpx_verify(ssl_verify=False)
      +    client = AIAgent._build_keepalive_http_client(
      +        "https://ollama.example.com/v1", verify=verify,
      +    )
      +    assert isinstance(client, httpx.Client)
      +    assert client._transport._pool._ssl_context.check_hostname is False
  • #86491 fix(providers): honor per-provider TLS on custom /models and pricing probes

    merged to main

    files: agent/model_metadata.py, hermes_cli/models.py, hermes_cli/urllib_security.py, tests/agent/test_custom_provider_ca_probes.py

2 PRs closed without merging

From the close comments: merged via another change 1 · superseded by another PR 1

  • #66561 fix(providers): honor per-provider TLS on custom /models and pricing probes · merged via another change → 44d5806, #86491, #56681, #66544
  • #66564 fix(ssl): thread provider ssl_ca_cert into /models and pricing probes (#66544) · superseded by another PR → #66561
Closed issues · 1
  • Closed#66544Custom-provider metadata probes ignore provider-scoped CA settings
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.40: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
agent
Merged PRs
#56681, #86491
Closed issues
1
Attempts
2 PRs closed without merging
Fix link checked
14 Aug 2026

For your agent

The same facts, typed.