From 30b0958a69cc36e825942cc2ff16a77e2d92968c Mon Sep 17 00:00:00 2001 From: Kyzcreig <9063726+Kyzcreig@users.noreply.github.com> Date: Mon, 24 Aug 2026 14:16:13 -0700 Subject: [PATCH] fix(auth): defer hermes_cli.config import until after PROVIDER_REGISTRY exists MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit hermes_cli.auth imports hermes_cli.config at module top; config runs provider-plugin discovery at import time (module-level provider imports + _inject_profile_env_vars), so plugins can execute against a partially initialized auth module during TUI startup — anything they try to register/inspect on auth silently loses to fallback metadata. Move the config import below ProviderConfig/PROVIDER_REGISTRY and document the ordering contract. Regression test spawns a clean interpreter with a probe plugin and asserts its registration lands (red before this fix, green after). --- hermes_cli/auth.py | 9 ++- .../test_auth_registry_import_order.py | 70 +++++++++++++++++++ 2 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 tests/providers/test_auth_registry_import_order.py diff --git a/hermes_cli/auth.py b/hermes_cli/auth.py index 8a8c2bfce9..286f7ff0ea 100644 --- a/hermes_cli/auth.py +++ b/hermes_cli/auth.py @@ -28,8 +28,6 @@ from pathlib import Path from typing import Any, Callable, Dict, FrozenSet, Iterable, List, Optional, Tuple from urllib.parse import urlparse -from hermes_cli.config import ( - get_hermes_home, get_config_path, read_raw_config, require_readable_config_before_write) from hermes_constants import OPENROUTER_BASE_URL, hermes_home_key, secure_parent_dir from agent.credential_persistence import sanitize_borrowed_credential_payload from utils import atomic_json_write, atomic_yaml_write, env_float, file_signature, is_truthy_value # noqa: F401 (env_float: agent.credential_pool reads auth_mod.env_float) @@ -249,6 +247,13 @@ PROVIDER_REGISTRY: Dict[str, ProviderConfig] = { p.id: p for p in (r if isinstance(r, ProviderConfig) else _api_key_provider(*r) for r in _REGISTRY_ROWS) } +# ``hermes_cli.config`` discovers model-provider plugins while importing, and a plugin may read this +# module's registry during that discovery. Keep the import below ProviderConfig / PROVIDER_REGISTRY so +# a plugin never observes a partially initialized auth module (CONTRACT: during discovery a plugin may +# rely only on ``ProviderConfig`` and ``PROVIDER_REGISTRY`` from here — nothing defined below). +from hermes_cli.config import ( # noqa: E402 + get_hermes_home, get_config_path, read_raw_config, require_readable_config_before_write) + # Providers handled outside the registry: copilot/kimi/zai have bespoke token refresh here; # openrouter/custom are aggregator/user-supplied and runtime_provider relies on # ``openrouter not in PROVIDER_REGISTRY``. diff --git a/tests/providers/test_auth_registry_import_order.py b/tests/providers/test_auth_registry_import_order.py new file mode 100644 index 0000000000..4747efc1fb --- /dev/null +++ b/tests/providers/test_auth_registry_import_order.py @@ -0,0 +1,70 @@ +"""Regression coverage for provider auth registration during TUI imports.""" + +from __future__ import annotations + +import os +import subprocess +import sys +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[2] + + +def test_tui_import_exposes_auth_registry_to_provider_plugins(tmp_path): + """Provider discovery must not see a partially initialized auth module.""" + hermes_home = tmp_path / ".hermes" + plugin_dir = hermes_home / "plugins" / "model-providers" / "import-order-probe" + plugin_dir.mkdir(parents=True) + (plugin_dir / "__init__.py").write_text( + "from providers import register_provider\n" + "from providers.base import ProviderProfile\n" + "\n" + "profile = ProviderProfile(\n" + " name='import-order-probe',\n" + " display_name='Profile fallback',\n" + " env_vars=('IMPORT_ORDER_PROBE_KEY',),\n" + " base_url='https://profile.example/v1',\n" + " auth_type='api_key',\n" + ")\n" + "register_provider(profile)\n" + "\n" + "from hermes_cli.auth import PROVIDER_REGISTRY, ProviderConfig\n" + "PROVIDER_REGISTRY['import-order-probe'] = ProviderConfig(\n" + " id='import-order-probe',\n" + " name='Plugin injection',\n" + " auth_type='api_key',\n" + " inference_base_url='https://plugin.example/v1',\n" + " api_key_env_vars=('IMPORT_ORDER_PROBE_KEY',),\n" + ")\n", + encoding="utf-8", + ) + + env = os.environ.copy() + env["HERMES_HOME"] = str(hermes_home) + env.pop("HERMES_PROFILE", None) + env["PYTHONPATH"] = os.pathsep.join([ + str(REPO_ROOT), + env.get("PYTHONPATH", ""), + ]).rstrip(os.pathsep) + probe = subprocess.run( + [ + sys.executable, + "-c", + "from tools.environments.local import _HERMES_PROVIDER_ENV_BLOCKLIST; " + "from hermes_cli.auth import PROVIDER_REGISTRY; " + "cfg = PROVIDER_REGISTRY['import-order-probe']; " + "assert cfg.name == 'Plugin injection', cfg; " + "assert cfg.inference_base_url == 'https://plugin.example/v1', cfg; " + "assert 'IMPORT_ORDER_PROBE_KEY' in _HERMES_PROVIDER_ENV_BLOCKLIST", + ], + cwd=REPO_ROOT, + env=env, + stdin=subprocess.DEVNULL, + capture_output=True, + text=True, + timeout=30, + ) + + assert probe.returncode == 0, probe.stdout + probe.stderr + assert "partially initialized module 'hermes_cli.auth'" not in probe.stderr