mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-06 07:51:53 +00:00
fix(auth) fix a few cases where refresh tokens were not rotated.
This commit is contained in:
parent
20bffa5b37
commit
569bc94b59
6 changed files with 166 additions and 109 deletions
|
|
@ -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"]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue