Files
wehub-resource-sync b4fbd6fe9f
Deploy Site / deploy-vercel (push) Has been skipped
Deploy Site / deploy-docs (push) Has been skipped
Build Skills Index / build-index (push) Has been skipped
CI / Deny unrelated histories (push) Has been skipped
CI / Detect affected areas (push) Successful in 27m35s
CI / OSV scan (push) Failing after 4s
CI / Build&Test Docker image (push) Successful in 9s
CI / Supply-chain scan (push) Has been skipped
CI / Lint Docker scripts (push) Failing after 5m13s
CI / Check contributors (push) Failing after 12m8s
CI / Docs Site (push) Failing after 12m8s
CI / TypeScript (push) Failing after 12m8s
CI / Python lints (push) Failing after 12m9s
CI / Python tests (push) Failing after 12m9s
CI / Check uv.lock (push) Failing after 23m22s
CI / CI timing report (push) Has been cancelled
Build Skills Index / trigger-deploy (push) Has been cancelled
CI / All required checks pass (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 11:56:03 +08:00

84 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}")