_shared_context imported truststore._ssl_constants unconditionally on the
ssl_ca_cert branch. truststore is a >=3.14 dependency, so a 3.11-3.13
bridge install with a provider ssl_ca_cert crashed client construction
with ModuleNotFoundError. Only reach for truststore's saved original
class when ssl.SSLContext has actually been replaced; otherwise the
stdlib class is right there and the private import is never needed.
Also pin the test env: the "missing bundle falls back to True" contract
inherits the host's SSL_CERT_FILE (NixOS shells export it), which flips
the return to the shared platform context — a host dependency, not an
order dependency. Add the SSL_CERT_FILE contract as its own test.
Keep downloads bound to their remote representation and publish through
atomic destination-local staging. Serialize shared partial ownership.
Keep explicit CA trust scoped to provider probes. Preserve checkpoint
history and edited files, validate all profile inputs before dependency
publication, and separate data removal from installed runtime ownership.
Exclude machine-specific PM state from portable transfers. Keep plugin
files and nested skill tools intact. Preserve native test isolation.
Focused native Windows receipts cover the individual repairs and their
integration. This commit does not claim a full-suite or release build.
The upstream/main merge re-added HERMES_CA_BUNDLE/SSL_CERT_FILE/
REQUESTS_CA_BUNDLE/CURL_CA_BUNDLE fallbacks to resolve_httpx_verify —
the exact env-ladder the truststore port (2f20ceb6f7) removed. Explicit
per-provider ssl_ca_cert is the only thing above the platform store.
test_env_ca_bundle_vars_no_longer_steer_trust caught it red.
Cherry-pick of 03b45db777 from ethie/bundles-local-models: TLS trust was
five hand-rolled ladders (agent/ssl_verify, hermes_cli/auth,
agent/model_metadata, hermes_cli/urllib_security, gateway/run's SSL_CERT_FILE
mutation) all ultimately pointing at certifi's frozen list. Trust now comes
from the platform verifier via truststore: CryptoAPI on Windows,
Security.framework on macOS, OpenSSL's store on Linux; install_truststore()
patches ssl.SSLContext process-wide. agent/ssl_verify.py is the one
authority; agent/ssl_guard.py deleted.
Also closes the session-flagged coverage gap: hermes_cli/main.py (CLI
entrypoint) and tui_gateway/entry.py now call install_truststore() so
subcommands/help that never construct an AIAgent still get OS-store trust.
A fan-out of 30 delegated children built 183 httpx.HTTPTransport objects
(each with its own httpcore pool + parsed SSL context): 3 per agent x
(primary + aux clients). A profiled session with ~130 children held 107 TLS
sockets to one provider. Peak RSS for the 30-child bench drops 286 -> 195 MB;
live HTTPTransports 183 -> 2, ConnectionPools 183 -> 7.
What is shared: the sync `HTTPTransport` (pool + SSL context) per
(scheme, verify, proxy, happy-eyeballs) identity, in a bounded module dict.
What is NOT shared: the per-agent `httpx.Client` wrapper. Each client mounts
a `_SharedTransport` view whose `close()` marks only that view closed and
never touches the pool, so the #10933 contract (close client A, build client
B, B works) holds unchanged — the pinning tests in
test_create_openai_client_reuse.py / test_sequential_chats_live.py pass as-is.
Safety for cross-thread aborts: `_SharedTransport.handle_request` stamps its
id into `request.extensions`; `_iter_pool_sockets` now only shuts down a
shared pool's in-flight requests carrying the calling client's stamp and
never its idle connections, so interrupting child A cannot sever child B's
stream (#29507 / #72975 walker semantics preserved for unshared pools).
Also:
- `resolve_httpx_verify` caches one SSLContext per CA-bundle path. With
SSL_CERT_FILE/HERMES_CA_BUNDLE set, every agent used to parse the bundle
again and — because the share key is context identity — get a private pool.
- The client no longer builds a third, unused default transport; its
default transport is the https view.
- Mounted transports now actually receive pool limits (Client-level
`limits=` never reached them, so mounts ran on httpx defaults with a 5 s
keepalive_expiry). The shared pool uses 50 keepalive / 1000 max so one
pool covers a whole concurrent fan-out.
- `close_shared_transports()` really closes the pools (tests / shutdown).
Async clients (`async_mode=True`) stay unshared: an httpcore async pool is
bound to the event loop that first uses it. Proxy-backed clients keep
httpx's per-client proxy transport.
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.