fix(auth): defer hermes_cli.config import until after PROVIDER_REGISTRY exists

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).
This commit is contained in:
Kyzcreig
2026-08-24 14:16:13 -07:00
committed by Teknium
parent 04f80dba0d
commit 30b0958a69
2 changed files with 77 additions and 2 deletions

View File

@@ -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``.

View File

@@ -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