mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-24 10:52:21 +00:00
* feat(memory): OAuth token storage and refresh for the Honcho provider * feat(memory): refresh the Honcho OAuth token in the client and session * feat(memory): zero-CLI loopback OAuth authorization flow * feat(memory): generic memory-provider OAuth connect endpoints * feat(desktop): memory-provider OAuth connect link * feat(memory): CLI OAuth sign-in with source-tagged authorize links * fix(memory): IP-literal loopback redirect and consent config_path on the authorize link * fix(memory): profile-scope the memory-provider OAuth endpoints * refactor(desktop): generic memory-provider OAuth client functions * docs(memory): trim OAuth module docstrings to the invariants * docs(memory): document OAuth connect as an optional auth method * fix(memory): send home-relative display path to consent, not the absolute path * perf(memory): cache OAuth token expiry in memory to skip the hot-path disk read * fix(memory): log OAuth refresh failures at warning, not debug * feat(memory): fall back to an OS-assigned loopback port when 8765 is taken * test(memory): cover the desktop Connect launcher, status, and provider dispatch * fix(desktop): keep the memory-provider dropdown one size regardless of connect state * fix(desktop): move the memory connect link to the description line, leaving the dropdown untouched * refactor(memory): move OAuth connect routes out of web_server into a memory-layer router * refactor(desktop): import MemoryConnect directly, drop the single-export barrel * fix(memory): launch CLI OAuth sign-in right after the auth choice, not after the wizard * fix(desktop): auto-clear the OAuth error state instead of leaving it sticky * test(honcho): isolate auth-method prompt from deployment-shape wizard tests main's wizard suite scripts the cloud prompts without the OAuth auth-method step; auto-answer it in the shared helper so the answer lists stay shape-only. * docs(honcho): document query-adaptive reasoning level (reasoningHeuristic) README never mentioned reasoningHeuristic and listed reasoningLevelCap as an orphaned cap with the wrong default (— vs "high"). Add the query-adaptive scaling note + the reasoningHeuristic/reasoningLevelCap rows (grouped under Dialectic & Reasoning), matching the wording already on the hosted honcho.md page, and add a pointer from the memory-providers overview. * fix(honcho): default the CLI peer prompt to the OAuth consent name The CLI runs the grant with apply_config=False, so the peerName the user just entered at consent was dropped and the wizard's 'Your name' prompt fell back to $USER. Surface it as a transient OAuthCredential.consent_peer_name (set even when config isn't merged) and seed the prompt default from it. * feat(honcho): split OAuth client_id by surface (cli=hermes-agent, desktop=hermes-desktop) resolve_endpoints now picks the client_id from the initiating surface and threads it through authorize -> token exchange -> persisted grant -> refresh, so the CLI and desktop register as distinct OAuth clients. Surface-specific env overrides (HONCHO_OAUTH_CLIENT_ID_CLI/_DESKTOP) win over the generic HONCHO_OAUTH_CLIENT_ID, which still overrides every surface. * feat(honcho): show OAuth vs API key in status; detect existing OAuth in setup status now prints 'Auth: OAuth (clientId, token valid Xm/expired)' instead of masking the OAuth access token as a generic API key; setup notes an existing OAuth grant when re-run. * docs(honcho): drop 'shared pool' wording from unified observation mode help * fix(honcho): cross-process lock around OAuth refresh to prevent grant revocation The in-process threading lock can't stop a sibling process (another profile or the desktop app sharing honcho.json) from replaying the single-use refresh token and tripping reuse-detection, which revokes the whole grant. Guard the read-refresh-persist section with an OS file lock on <config>.lock so only one process rotates at a time; the others re-read the freshly-persisted token. Best-effort: platforms without flock degrade to in-process serialization. * refactor(honcho): one OAuth client (hermes-agent) for all surfaces Collapse the per-surface client_id split. CLI and desktop now use a single client_id (hermes-agent); consent branding/UI still adapt via the source query param. One grant identity means no clientId-vs-refresh-token desync that could get the grant revoked. HONCHO_OAUTH_CLIENT_ID still overrides for self-hosting. * fix(honcho): per-session resolves to session_id, never remapped by title Reorder resolve_session_name so stable identifiers win over labels: gateway per-chat key first, then the per-session session_id, then the cwd map / title. A (possibly auto-generated) title can no longer remap a live per-session conversation onto a second Honcho session mid-stream — fixes the desktop, which is per-conversation via session_id. Consequence: a gateway's per-chat key now also wins over a title (titles never remap a stable id).
83 lines
3.2 KiB
Python
83 lines
3.2 KiB
Python
"""HTTP routes for memory-provider OAuth connect, mounted by ``web_server``.
|
|
|
|
Kept out of ``web_server.py`` so the memory feature's surface stays in the
|
|
memory layer. Dispatch is by convention: a provider's flow lives at
|
|
``plugins.memory.<provider>.oauth_flow`` exposing ``start_loopback_flow_background``
|
|
and ``get_flow_status``; a provider without that module simply 404s. No provider
|
|
is named here.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
from contextlib import contextmanager
|
|
from typing import Optional
|
|
|
|
from fastapi import APIRouter, HTTPException
|
|
|
|
router = APIRouter(prefix="/api/memory/providers")
|
|
|
|
|
|
def _resolve_flow(provider: str):
|
|
"""Return a provider's OAuth flow module by convention, or raise 404."""
|
|
if not provider.isidentifier():
|
|
raise HTTPException(status_code=404, detail=f"unknown memory provider {provider!r}")
|
|
try:
|
|
return importlib.import_module(f"plugins.memory.{provider}.oauth_flow")
|
|
except ImportError:
|
|
raise HTTPException(status_code=404, detail=f"{provider} does not support OAuth connect")
|
|
|
|
|
|
@contextmanager
|
|
def _scope_to_profile(profile: Optional[str]):
|
|
"""Scope config resolution to ``profile`` so the flow's eager path resolve
|
|
targets that profile's honcho.json. None/""/"current" leaves it untouched."""
|
|
requested = (profile or "").strip()
|
|
if not requested or requested.lower() == "current":
|
|
yield
|
|
return
|
|
|
|
from hermes_cli import profiles as profiles_mod
|
|
from hermes_constants import reset_hermes_home_override, set_hermes_home_override
|
|
|
|
try:
|
|
profiles_mod.validate_profile_name(requested)
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
if not profiles_mod.profile_exists(requested):
|
|
raise HTTPException(status_code=404, detail=f"Profile '{requested}' does not exist.")
|
|
|
|
token = set_hermes_home_override(str(profiles_mod.get_profile_dir(requested)))
|
|
try:
|
|
yield
|
|
finally:
|
|
reset_hermes_home_override(token)
|
|
|
|
|
|
@router.post("/{provider}/oauth/start")
|
|
async def start_memory_oauth(provider: str, profile: Optional[str] = None):
|
|
"""Begin a provider's zero-CLI OAuth flow — opens the browser and captures
|
|
the grant via the loopback listener. Returns immediately; poll status."""
|
|
flow = _resolve_flow(provider)
|
|
try:
|
|
# The flow resolves its config path eagerly inside this scope; the worker
|
|
# thread it spawns outlives the request and the override.
|
|
with _scope_to_profile(profile):
|
|
return flow.start_loopback_flow_background()
|
|
except HTTPException:
|
|
raise
|
|
except Exception as exc:
|
|
raise HTTPException(status_code=500, detail=f"Failed to start {provider} OAuth: {exc}")
|
|
|
|
|
|
@router.get("/{provider}/oauth/status")
|
|
async def memory_oauth_status(provider: str, profile: Optional[str] = None):
|
|
"""Poll a provider's OAuth flow: idle | pending | connected | error."""
|
|
flow = _resolve_flow(provider)
|
|
try:
|
|
with _scope_to_profile(profile):
|
|
return flow.get_flow_status()
|
|
except HTTPException:
|
|
raise
|
|
except Exception as exc:
|
|
raise HTTPException(status_code=500, detail=f"Failed to read {provider} OAuth status: {exc}")
|