mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-06 07:51:53 +00:00
refactor(auth): collapse Nous inference fallback controls
This commit is contained in:
parent
89a3d038cf
commit
0bac7dd05b
13 changed files with 1071 additions and 240 deletions
|
|
@ -19,11 +19,12 @@ The fix:
|
|||
|
||||
These tests pin the corrected behavior.
|
||||
"""
|
||||
import asyncio
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
import httpx
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from hermes_cli.web_server import _SESSION_TOKEN, app
|
||||
|
|
@ -32,6 +33,32 @@ client = TestClient(app)
|
|||
HEADERS = {"X-Hermes-Session-Token": _SESSION_TOKEN}
|
||||
|
||||
|
||||
def _fake_nous_device_data():
|
||||
return {
|
||||
"device_code": "device-code",
|
||||
"user_code": "NOUS-1234",
|
||||
"verification_uri": "https://portal.nousresearch.com/device",
|
||||
"verification_uri_complete": (
|
||||
"https://portal.nousresearch.com/device?user_code=NOUS-1234"
|
||||
),
|
||||
"expires_in": 600,
|
||||
"interval": 5,
|
||||
}
|
||||
|
||||
|
||||
def _invoke_scope_refusal():
|
||||
request = httpx.Request("POST", "https://portal.nousresearch.com/oauth/device/code")
|
||||
response = httpx.Response(
|
||||
400,
|
||||
json={
|
||||
"error": "invalid_scope",
|
||||
"error_description": "unsupported scope inference:invoke",
|
||||
},
|
||||
request=request,
|
||||
)
|
||||
return httpx.HTTPStatusError("invalid scope", request=request, response=response)
|
||||
|
||||
|
||||
def test_minimax_login_does_not_launch_anthropic_flow():
|
||||
"""Click 'Login' on MiniMax → MUST NOT return claude.ai auth_url."""
|
||||
fake_user_code_resp = {
|
||||
|
|
@ -48,6 +75,9 @@ def test_minimax_login_does_not_launch_anthropic_flow():
|
|||
), patch(
|
||||
"hermes_cli.auth._minimax_pkce_pair",
|
||||
return_value=("verifier-stub", "challenge-stub", "stub-state"),
|
||||
), patch(
|
||||
"hermes_cli.web_server._minimax_poller",
|
||||
return_value=None,
|
||||
):
|
||||
resp = client.post(
|
||||
"/api/providers/oauth/minimax-oauth/start",
|
||||
|
|
@ -69,6 +99,113 @@ def test_minimax_login_does_not_launch_anthropic_flow():
|
|||
assert body["expires_in"] == 600
|
||||
|
||||
|
||||
def test_nous_dashboard_device_flow_honors_legacy_scope_override(monkeypatch):
|
||||
from hermes_cli import auth as auth_mod
|
||||
from hermes_cli import web_server as ws
|
||||
|
||||
requested_scopes = []
|
||||
|
||||
def fake_request_device_code(**kwargs):
|
||||
requested_scopes.append(kwargs["scope"])
|
||||
return _fake_nous_device_data()
|
||||
|
||||
monkeypatch.setenv(auth_mod.NOUS_LEGACY_SESSION_KEYS_ENV, "true")
|
||||
monkeypatch.setattr(auth_mod, "_request_device_code", fake_request_device_code)
|
||||
monkeypatch.setattr(ws, "_nous_poller", lambda sid: None)
|
||||
|
||||
result = asyncio.run(ws._start_device_code_flow("nous"))
|
||||
try:
|
||||
assert requested_scopes == [auth_mod.NOUS_LEGACY_AGENT_KEY_SCOPE]
|
||||
assert result["flow"] == "device_code"
|
||||
assert result["user_code"] == "NOUS-1234"
|
||||
assert (
|
||||
ws._oauth_sessions[result["session_id"]]["scope"]
|
||||
== auth_mod.NOUS_LEGACY_AGENT_KEY_SCOPE
|
||||
)
|
||||
finally:
|
||||
ws._oauth_sessions.pop(result["session_id"], None)
|
||||
|
||||
|
||||
def test_nous_dashboard_device_flow_retries_legacy_scope_on_invoke_refusal(monkeypatch):
|
||||
from hermes_cli import auth as auth_mod
|
||||
from hermes_cli import web_server as ws
|
||||
|
||||
requested_scopes = []
|
||||
|
||||
def fake_request_device_code(**kwargs):
|
||||
requested_scopes.append(kwargs["scope"])
|
||||
if len(requested_scopes) == 1:
|
||||
raise _invoke_scope_refusal()
|
||||
return _fake_nous_device_data()
|
||||
|
||||
monkeypatch.delenv(auth_mod.NOUS_LEGACY_SESSION_KEYS_ENV, raising=False)
|
||||
monkeypatch.setattr(auth_mod, "_request_device_code", fake_request_device_code)
|
||||
monkeypatch.setattr(ws, "_nous_poller", lambda sid: None)
|
||||
|
||||
result = asyncio.run(ws._start_device_code_flow("nous"))
|
||||
try:
|
||||
assert requested_scopes == [
|
||||
auth_mod.DEFAULT_NOUS_SCOPE,
|
||||
auth_mod.NOUS_LEGACY_AGENT_KEY_SCOPE,
|
||||
]
|
||||
assert (
|
||||
ws._oauth_sessions[result["session_id"]]["scope"]
|
||||
== auth_mod.NOUS_LEGACY_AGENT_KEY_SCOPE
|
||||
)
|
||||
finally:
|
||||
ws._oauth_sessions.pop(result["session_id"], None)
|
||||
|
||||
|
||||
def test_nous_dashboard_poller_preserves_effective_scope_when_token_omits_scope(monkeypatch):
|
||||
from hermes_cli import auth as auth_mod
|
||||
from hermes_cli import web_server as ws
|
||||
|
||||
session_id = "nous-effective-scope-test"
|
||||
ws._oauth_sessions[session_id] = {
|
||||
"session_id": session_id,
|
||||
"provider": "nous",
|
||||
"flow": "device_code",
|
||||
"created_at": time.time(),
|
||||
"status": "pending",
|
||||
"error_message": None,
|
||||
"portal_base_url": "https://portal.nousresearch.com",
|
||||
"client_id": "hermes-cli",
|
||||
"device_code": "device-code",
|
||||
"interval": 5,
|
||||
"expires_at": time.time() + 600,
|
||||
"scope": auth_mod.NOUS_LEGACY_AGENT_KEY_SCOPE,
|
||||
}
|
||||
captured_state = {}
|
||||
|
||||
def fake_refresh_nous_oauth_from_state(state, **kwargs):
|
||||
captured_state.update(state)
|
||||
return {**state, "agent_key": "legacy-agent-key"}
|
||||
|
||||
monkeypatch.setattr(
|
||||
auth_mod,
|
||||
"_poll_for_token",
|
||||
lambda **kwargs: {
|
||||
"access_token": "access-token",
|
||||
"refresh_token": "refresh-token",
|
||||
"expires_in": 3600,
|
||||
"token_type": "Bearer",
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
auth_mod,
|
||||
"refresh_nous_oauth_from_state",
|
||||
fake_refresh_nous_oauth_from_state,
|
||||
)
|
||||
monkeypatch.setattr(auth_mod, "persist_nous_credentials", lambda state: None)
|
||||
|
||||
try:
|
||||
ws._nous_poller(session_id)
|
||||
assert captured_state["scope"] == auth_mod.NOUS_LEGACY_AGENT_KEY_SCOPE
|
||||
assert ws._oauth_sessions[session_id]["status"] == "approved"
|
||||
finally:
|
||||
ws._oauth_sessions.pop(session_id, None)
|
||||
|
||||
|
||||
def test_minimax_dashboard_poller_accepts_absolute_ms_expired_in():
|
||||
"""Dashboard MiniMax completion must accept unix-ms token expiry values."""
|
||||
from hermes_cli import web_server as ws
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue