fix(auth) fix a few cases where refresh tokens were not rotated.

This commit is contained in:
Robin Fernandes 2026-05-17 22:29:40 +10:00 committed by Teknium
parent 20bffa5b37
commit 569bc94b59
6 changed files with 166 additions and 109 deletions

View file

@ -1,13 +1,13 @@
"""Nous Portal upstream adapter.
Reads the user's Nous OAuth state from ``~/.hermes/auth.json``, refreshes
the access token and resolves the ``agent_key`` compatibility credential
when needed, then exposes the upstream base URL plus bearer for the proxy
server to forward to.
Reads the user's Nous OAuth state from ``~/.hermes/auth.json`` through the
shared runtime resolver, refreshes the access token and resolves the
``agent_key`` compatibility credential when needed, then exposes the upstream
base URL plus bearer for the proxy server to forward to.
The ``agent_key`` field may hold either a NAS invoke JWT or the legacy
opaque session key. The refresh helper handles both see
:func:`hermes_cli.auth.refresh_nous_oauth_from_state`.
:func:`hermes_cli.auth.resolve_nous_runtime_credentials`.
"""
from __future__ import annotations
@ -22,12 +22,13 @@ from hermes_cli.auth import (
NOUS_INFERENCE_AUTH_MODE_AUTO,
NOUS_INFERENCE_AUTH_MODE_LEGACY,
_load_auth_store,
_auth_store_lock,
_is_terminal_nous_refresh_error,
_quarantine_nous_oauth_state,
_quarantine_nous_pool_entries,
_save_auth_store,
_write_shared_nous_state,
refresh_nous_oauth_from_state,
resolve_nous_runtime_credentials,
)
from hermes_cli.proxy.adapters.base import UpstreamAdapter, UpstreamCredential
@ -50,9 +51,8 @@ class NousPortalAdapter(UpstreamAdapter):
"""Proxy upstream for the Nous Portal inference API."""
def __init__(self) -> None:
# Lock guards _load → refresh → _save against parallel proxy requests
# racing to refresh expired tokens. Refresh itself is HTTP, so we
# hold the lock across the network call (brief; OAuth refresh is fast).
# Serialize proxy requests in this process; cross-process token refresh
# and persistence are handled by resolve_nous_runtime_credentials().
self._lock = threading.Lock()
@property
@ -107,8 +107,7 @@ class NousPortalAdapter(UpstreamAdapter):
)
try:
refreshed = refresh_nous_oauth_from_state(
state,
refreshed = resolve_nous_runtime_credentials(
inference_auth_mode=inference_auth_mode,
)
except AuthError as exc:
@ -131,22 +130,20 @@ class NousPortalAdapter(UpstreamAdapter):
f"Failed to refresh Nous Portal credentials: {exc}"
) from exc
self._save_state(refreshed)
agent_key = refreshed.get("agent_key")
agent_key = refreshed.get("api_key")
if not agent_key:
raise RuntimeError(
"Nous Portal refresh did not return a usable agent_key. "
"Try `hermes login nous` to re-authenticate."
)
base_url = refreshed.get("inference_base_url") or DEFAULT_NOUS_INFERENCE_URL
base_url = refreshed.get("base_url") or DEFAULT_NOUS_INFERENCE_URL
base_url = base_url.rstrip("/")
return UpstreamCredential(
bearer=agent_key,
base_url=base_url,
expires_at=refreshed.get("agent_key_expires_at"),
expires_at=refreshed.get("expires_at"),
)
# ------------------------------------------------------------------
@ -156,7 +153,8 @@ class NousPortalAdapter(UpstreamAdapter):
def _read_state(self) -> Optional[Dict[str, Any]]:
try:
store = _load_auth_store()
with _auth_store_lock():
store = _load_auth_store()
except Exception as exc:
logger.warning("proxy: failed to load auth store: %s", exc)
return None
@ -174,21 +172,20 @@ class NousPortalAdapter(UpstreamAdapter):
quarantine_reason: Optional[str] = None,
) -> None:
try:
store = _load_auth_store()
if quarantine_error is not None and quarantine_reason:
_quarantine_nous_pool_entries(
store,
quarantine_error,
reason=quarantine_reason,
)
providers = store.setdefault("providers", {})
providers["nous"] = state
_save_auth_store(store)
with _auth_store_lock():
store = _load_auth_store()
if quarantine_error is not None and quarantine_reason:
_quarantine_nous_pool_entries(
store,
quarantine_error,
reason=quarantine_reason,
)
providers = store.setdefault("providers", {})
providers["nous"] = state
_save_auth_store(store)
_write_shared_nous_state(state)
except Exception as exc:
# Best effort — we still return the fresh credential. The next
# request just won't see cached state, which means another refresh.
logger.warning("proxy: failed to persist refreshed Nous state: %s", exc)
logger.warning("proxy: failed to persist Nous quarantine state: %s", exc)
__all__ = ["NousPortalAdapter"]