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