Merge branch 'main' of github.com:NousResearch/hermes-agent into bb/gui

# Conflicts:
#	cli.py
#	hermes_cli/main.py
#	run_agent.py
#	tests/hermes_cli/test_cmd_update.py
#	tools/mcp_tool.py
#	web/src/lib/gatewayClient.ts
This commit is contained in:
Brooklyn Nicholson
2026-05-18 01:26:56 -05:00
260 changed files with 24547 additions and 13573 deletions
+37 -10
View File
@@ -2609,7 +2609,11 @@ async def _start_device_code_flow(provider_id: str) -> Dict[str, Any]:
so the UI can render the verification page link + user code.
"""
if provider_id == "nous":
from hermes_cli.auth import _request_device_code, PROVIDER_REGISTRY
from hermes_cli.auth import (
_nous_device_scope_with_env_override,
_request_nous_device_code_with_scope_fallback,
PROVIDER_REGISTRY,
)
import httpx
pconfig = PROVIDER_REGISTRY["nous"]
portal_base_url = (
@@ -2618,22 +2622,34 @@ async def _start_device_code_flow(provider_id: str) -> Dict[str, Any]:
or pconfig.portal_base_url
).rstrip("/")
client_id = pconfig.client_id
scope = pconfig.scope
scope, explicit_scope = _nous_device_scope_with_env_override(
None,
default_scope=pconfig.scope,
)
def _do_nous_device_request():
with httpx.Client(timeout=httpx.Timeout(15.0), headers={"Accept": "application/json"}) as client:
return _request_device_code(
with httpx.Client(
timeout=httpx.Timeout(15.0),
headers={"Accept": "application/json"},
) as client:
return _request_nous_device_code_with_scope_fallback(
client=client,
portal_base_url=portal_base_url,
client_id=client_id,
scope=scope,
allow_legacy_fallback=not explicit_scope,
)
device_data = await asyncio.get_running_loop().run_in_executor(None, _do_nous_device_request)
device_data, effective_scope = await asyncio.get_running_loop().run_in_executor(
None, _do_nous_device_request
)
sid, sess = _new_oauth_session("nous", "device_code")
sess["device_code"] = str(device_data["device_code"])
sess["interval"] = int(device_data["interval"])
sess["expires_at"] = time.time() + int(device_data["expires_in"])
sess["portal_base_url"] = portal_base_url
sess["client_id"] = client_id
sess["scope"] = effective_scope
threading.Thread(
target=_nous_poller, args=(sid,), daemon=True, name=f"oauth-poll-{sid[:6]}"
).start()
@@ -2762,7 +2778,11 @@ async def _start_device_code_flow(provider_id: str) -> Dict[str, Any]:
def _nous_poller(session_id: str) -> None:
"""Background poller that drives a Nous device-code flow to completion."""
from hermes_cli.auth import _poll_for_token, refresh_nous_oauth_from_state
from hermes_cli.auth import (
NOUS_INFERENCE_AUTH_MODE_FRESH,
_poll_for_token,
refresh_nous_oauth_from_state,
)
from datetime import datetime, timezone
import httpx
with _oauth_sessions_lock:
@@ -2773,6 +2793,7 @@ def _nous_poller(session_id: str) -> None:
client_id = sess["client_id"]
device_code = sess["device_code"]
interval = sess["interval"]
scope = sess.get("scope")
expires_in = max(60, int(sess["expires_at"] - time.time()))
try:
with httpx.Client(timeout=httpx.Timeout(15.0), headers={"Accept": "application/json"}) as client:
@@ -2791,7 +2812,7 @@ def _nous_poller(session_id: str) -> None:
"portal_base_url": portal_base_url,
"inference_base_url": token_data.get("inference_base_url"),
"client_id": client_id,
"scope": token_data.get("scope"),
"scope": token_data.get("scope") or scope,
"token_type": token_data.get("token_type", "Bearer"),
"access_token": token_data["access_token"],
"refresh_token": token_data.get("refresh_token"),
@@ -2803,8 +2824,11 @@ def _nous_poller(session_id: str) -> None:
"expires_in": token_ttl,
}
full_state = refresh_nous_oauth_from_state(
auth_state, min_key_ttl_seconds=300, timeout_seconds=15.0,
force_refresh=False, force_mint=True,
auth_state,
min_key_ttl_seconds=300,
timeout_seconds=15.0,
force_refresh=False,
inference_auth_mode=NOUS_INFERENCE_AUTH_MODE_FRESH,
)
from hermes_cli.auth import persist_nous_credentials
persist_nous_credentials(full_state)
@@ -5381,4 +5405,7 @@ def start_server(
open_browser,
)
print(f" Hermes Web UI → http://{host}:{port}")
uvicorn.run(app, host=host, port=port, log_level="warning")
# proxy_headers=False so _ws_client_is_allowed sees the real connection peer
# rather than X-Forwarded-For's rewritten value (which would defeat the
# loopback gate when behind a reverse proxy).
uvicorn.run(app, host=host, port=port, log_level="warning", proxy_headers=False)