fix: dashboard shows Nous Portal as 'not connected' despite active auth (#9261)

The dashboard device-code flow (_nous_poller in web_server.py) saved
credentials to the credential pool only, while get_nous_auth_status()
only checked the auth store (auth.json). This caused the Keys tab to
show 'not connected' even when the backend was fully authenticated.

Two fixes:
1. get_nous_auth_status() now checks the credential pool first (like
   get_codex_auth_status() already does), then falls back to the auth
   store.
2. _nous_poller now also persists to the auth store after saving to
   the credential pool, matching the CLI flow (_login_nous).

Adds 3 tests covering pool-only, auth-store-fallback, and empty-state
scenarios.
This commit is contained in:
Teknium 2026-04-13 16:32:11 -07:00 committed by GitHub
parent 8d023e43ed
commit 32cea0c08d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 120 additions and 1 deletions

View file

@ -2253,7 +2253,40 @@ def resolve_nous_runtime_credentials(
# =============================================================================
def get_nous_auth_status() -> Dict[str, Any]:
"""Status snapshot for `hermes status` output."""
"""Status snapshot for `hermes status` output.
Checks the credential pool first (where the dashboard device-code flow
and ``hermes auth`` store credentials), then falls back to the legacy
auth-store provider state.
"""
# Check credential pool first — the dashboard device-code flow saves
# here but may not have written to the auth store yet.
try:
from agent.credential_pool import load_pool
pool = load_pool("nous")
if pool and pool.has_credentials():
entry = pool.select()
if entry is not None:
access_token = (
getattr(entry, "access_token", None)
or getattr(entry, "runtime_api_key", "")
)
if access_token:
return {
"logged_in": True,
"portal_base_url": getattr(entry, "portal_base_url", None)
or getattr(entry, "base_url", None),
"inference_base_url": getattr(entry, "inference_base_url", None)
or getattr(entry, "base_url", None),
"access_token": access_token,
"access_expires_at": getattr(entry, "expires_at", None),
"agent_key_expires_at": getattr(entry, "agent_key_expires_at", None),
"has_refresh_token": bool(getattr(entry, "refresh_token", None)),
}
except Exception:
pass
# Fall back to auth-store provider state
state = get_provider_auth_state("nous")
if not state:
return {

View file

@ -1216,6 +1216,22 @@ def _nous_poller(session_id: str) -> None:
"base_url": full_state.get("inference_base_url"),
})
pool.add_entry(entry)
# Also persist to auth store so get_nous_auth_status() sees it
# (matches what _login_nous in auth.py does for the CLI flow).
try:
from hermes_cli.auth import (
_load_auth_store, _save_provider_state, _save_auth_store,
_auth_store_lock,
)
with _auth_store_lock():
auth_store = _load_auth_store()
_save_provider_state(auth_store, "nous", full_state)
_save_auth_store(auth_store)
except Exception as store_exc:
_log.warning(
"oauth/device: credential pool saved but auth store write failed "
"(session=%s): %s", session_id, store_exc,
)
with _oauth_sessions_lock:
sess["status"] = "approved"
_log.info("oauth/device: nous login completed (session=%s)", session_id)