refactor(hermes_cli): auth_nous/codex — unify refresh_nous_oauth_pure into from_state, codex login POST helper
This commit is contained in:
@@ -4,9 +4,8 @@ Tokens live in ~/.hermes/auth.json, NOT ~/.codex/: Hermes keeps its own Codex OA
|
||||
separate from the Codex CLI / VS Code extension so one app's refresh-token rotation cannot
|
||||
invalidate the other's session.
|
||||
|
||||
Split out of ``hermes_cli/auth.py``; every name is re-exported there so ``hermes_cli.auth.<name>``
|
||||
keeps resolving (and monkeypatching). Origin-internal helpers are imported lazily inside each
|
||||
function (no import cycle; patches on ``hermes_cli.auth.<helper>`` still intercept).
|
||||
Split out of ``hermes_cli/auth.py`` and re-exported there; origin helpers are imported lazily
|
||||
inside each function so ``hermes_cli.auth.<name>`` patches still intercept (and no import cycle).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -220,6 +219,15 @@ def _refresh_payload_access_token(
|
||||
return payload, access
|
||||
|
||||
|
||||
def _codex_login_post(url: str, *, failure: Tuple[str, str], **kwargs: Any) -> "httpx.Response":
|
||||
"""One 15s POST for the device-login flow; transport errors become ``_codex_err(*failure)``."""
|
||||
try:
|
||||
with _codex_http_client(timeout=httpx.Timeout(15.0)) as client:
|
||||
return client.post(url, **kwargs)
|
||||
except Exception as exc:
|
||||
raise _codex_err(f"{failure[0]}: {exc}", failure[1])
|
||||
|
||||
|
||||
def _codex_http_client(**kwargs: Any) -> "httpx.Client":
|
||||
"""Build an ``httpx.Client`` for Codex OAuth/probe endpoints with Happy-Eyeballs racing.
|
||||
|
||||
@@ -708,13 +716,10 @@ def _codex_request_device_code(issuer: str, client_id: str) -> Dict[str, Any]:
|
||||
resp = None
|
||||
max_attempts = 4
|
||||
for attempt in range(1, max_attempts + 1):
|
||||
try:
|
||||
with _codex_http_client(timeout=httpx.Timeout(15.0)) as client:
|
||||
resp = client.post(
|
||||
f"{issuer}/api/accounts/deviceauth/usercode", json={"client_id": client_id},
|
||||
headers={"Content-Type": "application/json"})
|
||||
except Exception as exc:
|
||||
raise _codex_err(f"Failed to request device code: {exc}", "device_code_request_failed")
|
||||
resp = _codex_login_post(
|
||||
f"{issuer}/api/accounts/deviceauth/usercode", json={"client_id": client_id},
|
||||
headers={"Content-Type": "application/json"},
|
||||
failure=("Failed to request device code", "device_code_request_failed"))
|
||||
if resp.status_code != 429:
|
||||
break
|
||||
if attempt < max_attempts:
|
||||
@@ -777,17 +782,14 @@ def _codex_exchange_authorization_code(
|
||||
raise _codex_err(
|
||||
"Device auth response missing authorization_code or code_verifier.",
|
||||
"device_code_incomplete_exchange")
|
||||
try:
|
||||
with _codex_http_client(timeout=httpx.Timeout(15.0)) as client:
|
||||
token_resp = client.post(
|
||||
CODEX_OAUTH_TOKEN_URL,
|
||||
data={
|
||||
"grant_type": "authorization_code", "code": authorization_code,
|
||||
"redirect_uri": f"{issuer}/deviceauth/callback", "client_id": client_id,
|
||||
"code_verifier": code_verifier},
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"})
|
||||
except Exception as exc:
|
||||
raise _codex_err(f"Token exchange failed: {exc}", "token_exchange_failed")
|
||||
token_resp = _codex_login_post(
|
||||
CODEX_OAUTH_TOKEN_URL,
|
||||
data={
|
||||
"grant_type": "authorization_code", "code": authorization_code,
|
||||
"redirect_uri": f"{issuer}/deviceauth/callback", "client_id": client_id,
|
||||
"code_verifier": code_verifier},
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
failure=("Token exchange failed", "token_exchange_failed"))
|
||||
if token_resp.status_code == 429:
|
||||
raise _codex_login_rate_limited_error(token_resp, during=" during token exchange")
|
||||
if token_resp.status_code != 200:
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
"""Shared device-code / browser / TLS helpers for interactive OAuth logins.
|
||||
|
||||
Split out of ``hermes_cli/auth.py``; every name is re-exported there so ``hermes_cli.auth.<name>``
|
||||
keeps resolving (and monkeypatching). Origin-internal helpers are imported lazily inside each
|
||||
function (no import cycle; patches on ``hermes_cli.auth.<helper>`` still intercept).
|
||||
Split out of ``hermes_cli/auth.py`` and re-exported there; origin helpers are imported lazily
|
||||
inside each function so ``hermes_cli.auth.<name>`` patches still intercept (and no import cycle).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
"""Nous Portal OAuth: device-code login, refresh, shared-store mirroring, JWT selection, status.
|
||||
|
||||
Split out of ``hermes_cli/auth.py``; every name is re-exported there so ``hermes_cli.auth.<name>``
|
||||
keeps resolving (and monkeypatching). Origin-internal helpers are imported lazily inside each
|
||||
function (no import cycle; patches on ``hermes_cli.auth.<helper>`` still intercept).
|
||||
Split out of ``hermes_cli/auth.py`` and re-exported there; origin helpers are imported lazily
|
||||
inside each function so ``hermes_cli.auth.<name>`` patches still intercept (and no import cycle).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -732,17 +731,37 @@ def refresh_nous_oauth_pure(
|
||||
``on_state_update`` fires after a successful access-token refresh so callers owning persistent
|
||||
state can save the rotated refresh token before later validation can fail.
|
||||
"""
|
||||
return refresh_nous_oauth_from_state(
|
||||
{
|
||||
"access_token": access_token, "refresh_token": refresh_token, "client_id": client_id,
|
||||
"portal_base_url": portal_base_url, "inference_base_url": inference_base_url,
|
||||
"token_type": token_type, "scope": scope, "obtained_at": obtained_at,
|
||||
"expires_at": expires_at, "agent_key": agent_key,
|
||||
"agent_key_expires_at": agent_key_expires_at,
|
||||
"tls": {"insecure": insecure, "ca_bundle": ca_bundle}},
|
||||
timeout_seconds=timeout_seconds, force_refresh=force_refresh,
|
||||
on_state_update=on_state_update)
|
||||
|
||||
|
||||
def refresh_nous_oauth_from_state(
|
||||
src: Dict[str, Any], *, timeout_seconds: float = 15.0, force_refresh: bool = False,
|
||||
on_state_update: Optional[Callable[[Dict[str, Any], str], None]] = None) -> Dict[str, Any]:
|
||||
"""Refresh Nous OAuth from a state dict (defaults filled in) without mutating auth.json."""
|
||||
from hermes_cli.auth import (
|
||||
_assert_nous_inference_jwt_usable, _refresh_access_token, _resolve_verify,
|
||||
_select_nous_invoke_jwt)
|
||||
tls = src.get("tls") or {}
|
||||
insecure, ca_bundle = tls.get("insecure"), tls.get("ca_bundle")
|
||||
state: Dict[str, Any] = {
|
||||
"access_token": access_token, "refresh_token": refresh_token,
|
||||
"client_id": client_id or DEFAULT_NOUS_CLIENT_ID,
|
||||
"portal_base_url": (portal_base_url or DEFAULT_NOUS_PORTAL_URL).rstrip("/"),
|
||||
"inference_base_url": (inference_base_url or DEFAULT_NOUS_INFERENCE_URL).rstrip("/"),
|
||||
"token_type": token_type or "Bearer", "scope": scope or DEFAULT_NOUS_SCOPE,
|
||||
"obtained_at": obtained_at, "expires_at": expires_at, "agent_key": agent_key,
|
||||
"agent_key_expires_at": agent_key_expires_at,
|
||||
"access_token": src.get("access_token", ""), "refresh_token": src.get("refresh_token", ""),
|
||||
"client_id": src.get("client_id") or DEFAULT_NOUS_CLIENT_ID,
|
||||
"portal_base_url": (src.get("portal_base_url") or DEFAULT_NOUS_PORTAL_URL).rstrip("/"),
|
||||
"inference_base_url": (
|
||||
src.get("inference_base_url") or DEFAULT_NOUS_INFERENCE_URL).rstrip("/"),
|
||||
"token_type": src.get("token_type") or "Bearer",
|
||||
"scope": src.get("scope") or DEFAULT_NOUS_SCOPE,
|
||||
"obtained_at": src.get("obtained_at"), "expires_at": src.get("expires_at"),
|
||||
"agent_key": src.get("agent_key"), "agent_key_expires_at": src.get("agent_key_expires_at"),
|
||||
"tls": {"insecure": bool(insecure), "ca_bundle": ca_bundle}}
|
||||
verify = _resolve_verify(insecure=insecure, ca_bundle=ca_bundle, auth_state=state)
|
||||
with _nous_http_client(timeout_seconds or 15.0, verify) as client:
|
||||
@@ -767,23 +786,6 @@ def refresh_nous_oauth_pure(
|
||||
return state
|
||||
|
||||
|
||||
def refresh_nous_oauth_from_state(
|
||||
state: Dict[str, Any], *, timeout_seconds: float = 15.0, force_refresh: bool = False,
|
||||
on_state_update: Optional[Callable[[Dict[str, Any], str], None]] = None) -> Dict[str, Any]:
|
||||
"""Refresh Nous OAuth from a state dict. Thin wrapper around refresh_nous_oauth_pure."""
|
||||
tls = state.get("tls") or {}
|
||||
return refresh_nous_oauth_pure(
|
||||
state.get("access_token", ""), state.get("refresh_token", ""),
|
||||
state.get("client_id", "hermes-cli"), state.get("portal_base_url", DEFAULT_NOUS_PORTAL_URL),
|
||||
state.get("inference_base_url", DEFAULT_NOUS_INFERENCE_URL),
|
||||
token_type=state.get("token_type", "Bearer"), scope=state.get("scope", DEFAULT_NOUS_SCOPE),
|
||||
obtained_at=state.get("obtained_at"), expires_at=state.get("expires_at"),
|
||||
agent_key=state.get("agent_key"), agent_key_expires_at=state.get("agent_key_expires_at"),
|
||||
timeout_seconds=timeout_seconds, insecure=tls.get("insecure"),
|
||||
ca_bundle=tls.get("ca_bundle"),
|
||||
force_refresh=force_refresh, on_state_update=on_state_update)
|
||||
|
||||
|
||||
def persist_nous_credentials(creds: Dict[str, Any], *, label: Optional[str] = None):
|
||||
"""Persist Nous OAuth credentials as the singleton provider state.
|
||||
|
||||
@@ -1082,24 +1084,23 @@ def _snapshot_nous_pool_status() -> Dict[str, Any]:
|
||||
return (agent_exp, access_exp, -int(getattr(entry, "priority", 0) or 0))
|
||||
|
||||
entry = max(entries, key=_entry_sort_key)
|
||||
if not getattr(entry, "runtime_api_key", None):
|
||||
attr = lambda name, default=None: getattr(entry, name, default) # noqa: E731
|
||||
if not attr("runtime_api_key"):
|
||||
return _empty_nous_auth_status()
|
||||
access_token = getattr(entry, "access_token", None)
|
||||
auth_type = str(getattr(entry, "auth_type", "") or "").strip().lower()
|
||||
refresh_token = getattr(entry, "refresh_token", None)
|
||||
access_token, refresh_token = attr("access_token"), attr("refresh_token")
|
||||
auth_type = str(attr("auth_type", "") or "").strip().lower()
|
||||
is_portal_oauth = bool(access_token) and (
|
||||
auth_type.startswith("oauth") or bool(refresh_token))
|
||||
label = getattr(entry, "label", "unknown")
|
||||
portal_status_url = (
|
||||
(getattr(entry, "portal_base_url", None) or DEFAULT_NOUS_PORTAL_URL) if is_portal_oauth
|
||||
else None)
|
||||
label = attr("label", "unknown")
|
||||
return {
|
||||
"logged_in": is_portal_oauth, "portal_base_url": portal_status_url,
|
||||
"inference_base_url": getattr(entry, "inference_base_url", None)
|
||||
or getattr(entry, "runtime_base_url", None) or getattr(entry, "base_url", None),
|
||||
"logged_in": is_portal_oauth,
|
||||
"portal_base_url": (
|
||||
(attr("portal_base_url") or DEFAULT_NOUS_PORTAL_URL) if is_portal_oauth else None),
|
||||
"inference_base_url": (
|
||||
attr("inference_base_url") or attr("runtime_base_url") or attr("base_url")),
|
||||
"access_token": access_token if is_portal_oauth else None,
|
||||
"access_expires_at": getattr(entry, "expires_at", None),
|
||||
"agent_key_expires_at": getattr(entry, "agent_key_expires_at", None),
|
||||
"access_expires_at": attr("expires_at"),
|
||||
"agent_key_expires_at": attr("agent_key_expires_at"),
|
||||
"has_refresh_token": bool(refresh_token), "inference_credential_present": True,
|
||||
"credential_source": f"pool:{label}", "source": f"pool:{label}"}
|
||||
except Exception:
|
||||
@@ -1417,11 +1418,9 @@ def _pick_nous_model_after_login(
|
||||
raise _nous_err("No runtime API key available to fetch models", "invalid_token")
|
||||
|
||||
from hermes_cli.models import (
|
||||
get_curated_nous_model_ids, get_pricing_for_provider,
|
||||
check_nous_free_tier, partition_nous_models_by_tier,
|
||||
nous_policy_allowed_ids, restrict_to_nous_policy,
|
||||
union_with_portal_free_recommendations,
|
||||
union_with_portal_paid_recommendations)
|
||||
get_curated_nous_model_ids, get_pricing_for_provider, check_nous_free_tier,
|
||||
partition_nous_models_by_tier, nous_policy_allowed_ids, restrict_to_nous_policy,
|
||||
union_with_portal_free_recommendations, union_with_portal_paid_recommendations)
|
||||
model_ids = get_curated_nous_model_ids()
|
||||
_portal = auth_state.get("portal_base_url", "")
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
"""Single-use OAuth grant hygiene: strip cloned grants from profiles, heal forked grants.
|
||||
|
||||
Split out of ``hermes_cli/auth.py``; every name is re-exported there so ``hermes_cli.auth.<name>``
|
||||
keeps resolving (and monkeypatching). Origin-internal helpers are imported lazily inside each
|
||||
function (no import cycle; patches on ``hermes_cli.auth.<helper>`` still intercept).
|
||||
Split out of ``hermes_cli/auth.py`` and re-exported there; origin helpers are imported lazily
|
||||
inside each function so ``hermes_cli.auth.<name>`` patches still intercept (and no import cycle).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
Reference in New Issue
Block a user