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:
@@ -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``.
|
||||
|
||||
70
tests/providers/test_auth_registry_import_order.py
Normal file
70
tests/providers/test_auth_registry_import_order.py
Normal 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
|
||||
Reference in New Issue
Block a user