The salvaged fix wired per-provider ssl_ca_cert / ssl_verify (and HERMES_CA_BUNDLE) into the MAIN OpenAI client. This follow-up: - Auxiliary client parity: process_bootstrap.build_keepalive_http_client accepts and forwards verify; auxiliary_client._resolve_aux_verify mirrors the main-client TLS resolution (via load_config_readonly, the read-only fast path) so compression/vision/web_extract/title-gen/session_search honor the same per-provider CA. Without this, chat worked against a private-CA endpoint but every auxiliary call still failed APIConnectionError. - switch_model now reads custom_providers from live config (load_config_readonly) instead of the init-time agent._custom_providers snapshot, so ssl_ca_cert / ssl_verify edits are honored on mid-session model switch — matching the context-length reload (#15779). - Drop the dead client-level verify= where a custom httpx transport is used (httpx ignores it there); verify lives on the transport. Fix docstrings. Applies to both run_agent._build_keepalive_http_client and process_bootstrap. - resolve_httpx_verify: add CURL_CA_BUNDLE to the env chain (consistency with agent/ssl_guard._CA_BUNDLE_ENV_VARS) and emit a loud logger.warning naming the endpoint whenever ssl_verify:false disables verification. - get_custom_provider_tls_settings: case-insensitive base_url match (config dedup already lowercases; scheme/host are case-insensitive) so a mixed-case entry doesn't silently drop its CA. Exact match preserved — no prefix bypass. - Demote best-effort except Exception: pass in agent_init/switch_model to logger.debug(exc_info=True). - Tests for aux verify forwarding, _resolve_aux_verify, case-insensitive match, and prefix-bypass rejection.
63 lines
2 KiB
Python
63 lines
2 KiB
Python
"""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
|