"""Preventive SSL CA certificate checks — catch broken CA bundle paths before OpenAI/httpx turns them into an opaque ``FileNotFoundError``.""" from __future__ import annotations import logging import os import ssl from pathlib import Path from agent.errors import SSLConfigurationError logger = logging.getLogger(__name__) _CA_BUNDLE_ENV_VARS = ("HERMES_CA_BUNDLE", "SSL_CERT_FILE", "REQUESTS_CA_BUNDLE", "CURL_CA_BUNDLE") _SKIP_VALUES = {"1", "true", "yes", "on"} _REPAIR_HINT = ( "Repair: run `hermes doctor --fix` (auto-reinstalls certifi), or " "manually: python -m pip install --force-reinstall certifi openai httpx\n" "If you configured a custom corporate CA bundle, fix or unset the " "broken CA bundle environment variable." ) def _skip_ssl_guard_enabled() -> bool: return os.getenv("HERMES_SKIP_SSL_GUARD", "").strip().lower() in _SKIP_VALUES def _ssl_err(message: str) -> SSLConfigurationError: """Create a consistent, user-actionable SSL configuration error.""" return SSLConfigurationError(f"{message}\n{_REPAIR_HINT}") def _validate_bundle_path(label: str, value: str, *, require_substantial: bool = False) -> None: path = Path(value).expanduser() if not path.exists(): raise _ssl_err(f"{label} points to a missing CA bundle: {value}") if not path.is_file(): raise _ssl_err(f"{label} does not point to a CA bundle file: {value}") if require_substantial and path.stat().st_size < 1024: raise _ssl_err(f"{label} at {value} appears corrupted (too small)") try: ctx = ssl.create_default_context(cafile=str(path)) except Exception as exc: raise _ssl_err(f"{label} CA bundle at {value} cannot be loaded: {exc}") from exc try: loaded_certs = ctx.get_ca_certs() except NotImplementedError: # truststore-backed SSLContext (Windows OS trust store) lacks get_ca_certs(); loading above already validated it. return if not loaded_certs: raise _ssl_err(f"{label} CA bundle at {value} did not load any certificates") def verify_ca_bundle() -> None: """Verify configured and bundled CA certificates are present and loadable. Raises SSLConfigurationError when an explicit CA-bundle env var points at a bad path or certifi's bundled ``cacert.pem`` is missing/corrupt. """ if _skip_ssl_guard_enabled(): logger.debug("SSL CA bundle guard skipped via HERMES_SKIP_SSL_GUARD") return for env_var in _CA_BUNDLE_ENV_VARS: value = os.getenv(env_var) if value: _validate_bundle_path(env_var, value) try: import certifi except Exception as exc: raise _ssl_err(f"certifi is not importable: {exc}") from exc _validate_bundle_path("certifi", str(certifi.where()), require_substantial=True) def verify_ca_bundle_with_fallback() -> None: """Backward-compatible name for older call sites; a broken certifi bundle fails later anyway, so enforce the same check.""" verify_ca_bundle()