fix(cli): auth.json-only login with a benched credential is explained, not sent to the wizard

Two gaps from review of #113720's fix:

1. A profile logged in via auth.json (active_provider: nous) with no
   model.provider in config.yaml resolves as "auto". The ladder's OAuth rung
   swallowed the AuthError for "auto" and fell through to the keyless OpenRouter
   fallback, so the startup probe returned (False, None) and the first-run
   wizard ran anyway. The ladder now catches the AuthError in _ladder_rungs,
   still falls through for "auto", but stamps the swallowed error on a keyless
   fallback as `auth_error`; _probe_runtime_credentials returns it so the notice
   names the real failure.

2. The gate itself lived only in cli.py::_tui_print_startup and was untested at
   the seam (reverting cli.py left the suite green). It is now one mixin method,
   _maybe_offer_first_run_setup (tty check → probe → explain → offer), called
   from _tui_print_startup, and both tests drive that method with stdin.isatty
   patched True and _offer_first_run_setup asserting it is not called.

Tests: the benched-credential test now covers the gate and the cooldown headline
wording; the blank-install control is folded into the new auth.json-only test.
This commit is contained in:
teknium1
2026-09-18 04:03:12 -07:00
committed by Teknium
parent 90a6c48037
commit e63da95318
4 changed files with 105 additions and 47 deletions

View File

@@ -394,8 +394,9 @@ class CLIAgentSetupMixin:
return self._probe_runtime_credentials()[0]
def _probe_runtime_credentials(self) -> tuple:
"""``(ready, error)``: *error* is the exception that stopped resolution, ``None`` when a
provider resolved (usable or merely keyless). Never prints or mutates CLI state."""
"""``(ready, error)``: *error* is the exception that stopped resolution — raised, or
swallowed by the "auto" ladder and stamped on a keyless fallback — ``None`` when a provider
resolved (usable or merely keyless). Never prints or mutates CLI state."""
from hermes_cli.runtime_provider import resolve_runtime_provider
try:
runtime = resolve_runtime_provider(
@@ -409,7 +410,16 @@ class CLIAgentSetupMixin:
base_url = runtime.get("base_url")
if callable(api_key) or (isinstance(api_key, str) and api_key):
return bool(base_url), None
return _keyless_custom_base(base_url), None
return _keyless_custom_base(base_url), runtime.get("auth_error")
def _maybe_offer_first_run_setup(self) -> None:
"""Interactive startup gate: a blank install goes to the provider wizard; a configured
profile whose credential is benched or signed out gets the reason instead (#113720)."""
if not sys.stdin.isatty():
return
ready, error = self._probe_runtime_credentials()
if not ready and not self._explain_unusable_credentials(error):
self._offer_first_run_setup()
def _explain_unusable_credentials(self, error) -> bool:
"""A configured profile whose credential is benched, quarantined or signed out is not a