Skip to content

fix(agent): honor HERMES_TLS_MAX_VERSION to cap provider TLS handshakes - #44392

Open
AIalliAI wants to merge 1 commit into
NousResearch:mainfrom
AIalliAI:fix/44365-tls-max-version
Open

fix(agent): honor HERMES_TLS_MAX_VERSION to cap provider TLS handshakes#44392
AIalliAI wants to merge 1 commit into
NousResearch:mainfrom
AIalliAI:fix/44365-tls-max-version

Conversation

@AIalliAI

@AIalliAI AIalliAI commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Summary

#44365 reports intermittent DeepSeek connection failures on Windows desktop: the bundled Python/OpenSSL dies with [SSL: UNEXPECTED_EOF_WHILE_READING] ~15s into every TLS 1.3 handshake against api.deepseek.com, while a TLS 1.2-only handshake succeeds in 0.33s (and curl/PowerShell work because they use the OS TLS stack, not OpenSSL). This is a known class of CDN-edge/middlebox behavior — the server (or something in the path) accepts TLS 1.2 ClientHellos but kills TLS 1.3 ones.

This adds the escape hatch the issue asks for, as a config.yaml setting:

network:
  tls_max_version: "1.2"

Implementation

  • hermes_cli/config.py: new network.tls_max_version key (default "" = OpenSSL default), sibling of network.force_ipv4 in the existing "connectivity workarounds" section. Per the AGENTS.md contribution rubric, the user-facing surface is config.yaml — the env var below is an internal bridge.
  • hermes_constants.py: apply_tls_max_version() bridges the config value onto the internal HERMES_TLS_MAX_VERSION env var (same pattern as gateway.strictHERMES_MEDIA_DELIVERY_STRICT). The env-var hop is needed because agent/process_bootstrap.py has no config access at HTTP-client build time, and spawned agent subprocesses must inherit the cap. An explicitly exported env var wins over config.yaml, so one-off shell overrides keep working.
  • Bridged at both entrypoints that already apply network.force_ipv4: the hermes_cli/main.py early raw-yaml block (covers CLI, desktop dashboard spawns, TUI gateway — no extra config.yaml read) and the gateway/run.py bootstrap.
  • agent/process_bootstrap.py: _get_tls_ssl_context() parses the value (accepts 1.2/1.3, optional tls/tlsv prefix, case-insensitive; yaml floats fine) and builds an ssl.SSLContext with maximum_version capped. It honors the CLI's existing CA-bundle override convention (HERMES_CA_BUNDLE > REQUESTS_CA_BUNDLE > SSL_CERT_FILE, same precedence as _resolve_requests_verify in agent/model_metadata.py). Unset/invalid values fall back to httpx defaults with a logged warning; 1.0/1.1 are deliberately rejected — the knob exists to dodge broken TLS 1.3 paths, not to enable deprecated protocols.
  • run_agent.py _build_keepalive_http_client: passes the context to both the keepalive HTTPTransport (httpx ignores client-level verify when an explicit transport is passed) and the httpx.Client (so the proxy mount built internally from proxy= inherits the same cap — otherwise proxied users would silently keep the broken default).

Tests

tests/run_agent/test_keepalive_tls_max_version.py (12 tests): parsing (unset/blank, prefixes, invalid + 1.0/1.1 rejection), integration pins that the capped context lands on the transport pool and on the HTTPProxy mount when HTTPS_PROXY is set, that the default path keeps MAXIMUM_SUPPORTED, and the config bridge (sets the env var, never overrides an explicitly exported one, no-op on empty, handles yaml-float 1.2 and whitespace). Adjacent suites (test_create_openai_client_proxy_env, test_openai_client_lifecycle, test_ipv4_preference, hermes_cli/test_config.py) all pass.

Fixes #44365

@alt-glitch alt-glitch added type/bug Something isn't working comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint area/config Config system, migrations, profiles provider/deepseek DeepSeek API P2 Medium — degraded but workaround exists labels Jun 11, 2026
@liuhao1024

Copy link
Copy Markdown
Contributor

✅ Code Review — Clean

Reviewed the full diff (3 files: process_bootstrap.py, run_agent.py, test_keepalive_tls_max_version.py).

What I verified:

  • _get_tls_ssl_context() correctly rejects TLS 1.0/1.1 (only 1.2/1.3 accepted), handles prefix normalization (tls/tlsv/bare), and falls back gracefully on invalid input
  • CA bundle resolution follows the correct precedence: HERMES_CA_BUNDLE > REQUESTS_CA_BUNDLE > SSL_CERT_FILE
  • The SSL context is applied to both the HTTPTransport pool and the httpx.Client verify kwarg — this is necessary because httpx ignores client-level verify when an explicit transport is passed (well-documented in the PR body)
  • Proxy mount inherits the capped context (tested in test_proxy_mount_inherits_capped_context) — a capped direct transport next to an uncapped proxy mount would silently reintroduce the bug for proxied users
  • 7 tests cover: unset env, TLS 1.2 cap, prefix normalization, invalid values, keepalive transport, proxy mount, and default-TLS sanity

No issues found. LGTM.

@AIalliAI

Copy link
Copy Markdown
Contributor Author

Reworked in 1cb8df1 per the AGENTS.md contribution rubric ("behavioral settings go in config.yaml, not new HERMES_* env vars"): the user-facing knob is now network.tls_max_version in config.yaml (sibling of network.force_ipv4), bridged onto the internal HERMES_TLS_MAX_VERSION env var at startup via hermes_constants.apply_tls_max_version() — same pattern as gateway.strictHERMES_MEDIA_DELIVERY_STRICT. The mechanism @liuhao1024 reviewed is unchanged (_get_tls_ssl_context parsing, transport + proxy-mount application); an explicitly exported env var still wins for one-off shell overrides. 5 new tests cover the bridge (12 total).

@AIalliAI

Copy link
Copy Markdown
Contributor Author

Requesting maintainer review — this is ready to land from my side. Just merge-synced with current main (the only conflict was a trivial AUTHOR_MAP keep-both in scripts/release.py); the PR's touched test files pass locally on the merged head. Standalone fork CI is pending first-run approval here; the rollup branch in #44061 carrying this session's batch is fully green on upstream CI.

@AIalliAI

AIalliAI commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

STALE-NEEDS-REBASE: This PR is ~27 days old and has merge conflicts with main. The fix may still be relevant, but it needs a rebase and human decision on whether to revive. Not closing -- leaving for triage.

Some CDN edges and middleboxes accept TLS 1.2 handshakes but kill TLS 1.3 ClientHellos, surfacing as [SSL: UNEXPECTED_EOF_WHILE_READING] ~15s into every request while curl (OS TLS stack) works fine (NousResearch#44365, DeepSeek's edge).

- add _get_tls_ssl_context + _apply_tls_max_version in agent/process_bootstrap.py, composing the cap onto the httpx verify value both keepalive builders already carry so per-provider CA material survives

- apply the cap in AIAgent._build_keepalive_http_client (primary clients) and the module-level build_keepalive_http_client (auxiliary clients), covering direct transports, no-proxy mounts, and proxy mounts

- expose network.tls_max_version in config.yaml (DEFAULT_CONFIG, _config_version 33->34), bridged to HERMES_TLS_MAX_VERSION at startup via hermes_constants.apply_tls_max_version from gateway/run.py and hermes_cli/main.py; an explicitly exported env var still wins

Fixes NousResearch#44365
@AIalliAI
AIalliAI force-pushed the fix/44365-tls-max-version branch from c0474be to 009bd23 Compare July 11, 2026 03:15

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for carrying the config.yaml bridge and both client-builder paths. The TLS-cap premise remains present on current main: neither keepalive builder reads a cap, while both receive the resolved verify value.

Problems

  • agent/process_bootstrap.py:229 builds a separate TLS context before inspecting an existing verify context. If that independent CA lookup fails (:201-208), _apply_tls_max_version() returns the supplied provider context uncapped. Current provider setup creates that context before entering the builder (agent/agent_runtime_helpers.py:1689-1691). Parse the cap independently and cap an existing context first.
  • hermes_cli/config.py:3316 bumps _config_version for a defaulted field, contrary to AGENTS.md:584-590; this addition deep-merges without migration.
  • website/docs/user-guide/configuration.md:1998-2007 has no documentation for the new public setting.

Suggested changes

  • Add a regression for an explicit valid provider CA context plus an invalid unrelated CA environment value.
  • Preserve the existing default verification/trust-store semantics when replacing verify=True.
  • Remove the schema-version bump and document the setting.

This is an automated hermes-sweeper review.


Returns ``verify`` unchanged when the cap is unset or invalid.
"""
capped = _get_tls_ssl_context()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This creates and validates a separate CA context before inspecting verify. With a valid per-provider SSLContext but an invalid unrelated HERMES_CA_BUNDLE/REQUESTS_CA_BUNDLE value, _get_tls_ssl_context() returns None and the valid supplied context is returned uncapped. Parse the version separately and cap an existing SSLContext before any fresh CA-context creation.

Comment thread hermes_cli/config.py

# Config schema version - bump this when adding new required fields
"_config_version": 33,
"_config_version": 34,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please leave _config_version unchanged: this is a defaulted key in an existing section, so the normal deep merge supplies it without a migration. AGENTS.md explicitly reserves version bumps for active config transformations.

@teknium1 teknium1 added sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users labels Jul 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/config Config system, migrations, profiles comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P2 Medium — degraded but workaround exists provider/deepseek DeepSeek API sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Windows desktop: DeepSeek intermittent connection failure with Python/OpenSSL TLS 1.3

4 participants