fix: guard OAuth account removal

This commit is contained in:
Teknium 2026-06-13 21:32:05 -07:00
parent e986e3fc68
commit 1b16c48170
11 changed files with 175 additions and 30 deletions

View file

@ -5134,6 +5134,15 @@ def _resolve_provider_status(provider_id: str, status_fn) -> Dict[str, Any]:
return {"logged_in": False}
def _oauth_provider_disconnect_hint(provider: Dict[str, Any], status: Dict[str, Any]) -> Optional[str]:
"""Return the manual disconnect path when the API cannot clear this provider."""
if provider.get("flow") == "external":
return f"Use `{provider['cli_command']}` or that provider's CLI to remove it."
if status.get("source") == "env_var":
return "Remove the API key from Settings → Keys instead."
return None
@app.get("/api/providers/oauth")
async def list_oauth_providers():
"""Enumerate every OAuth-capable LLM provider with current status.
@ -5155,12 +5164,15 @@ async def list_oauth_providers():
providers = []
for p in _OAUTH_PROVIDER_CATALOG:
status = _resolve_provider_status(p["id"], p.get("status_fn"))
disconnect_hint = _oauth_provider_disconnect_hint(p, status)
providers.append({
"id": p["id"],
"name": p["name"],
"flow": p["flow"],
"cli_command": p["cli_command"],
"docs_url": p["docs_url"],
"disconnect_hint": disconnect_hint,
"disconnectable": disconnect_hint is None,
"status": status,
})
return {"providers": providers}
@ -5171,37 +5183,56 @@ async def disconnect_oauth_provider(provider_id: str, request: Request):
"""Disconnect an OAuth provider. Token-protected (matches /env/reveal)."""
_require_token(request)
valid_ids = {p["id"] for p in _OAUTH_PROVIDER_CATALOG}
if provider_id not in valid_ids:
catalog_by_id = {p["id"]: p for p in _OAUTH_PROVIDER_CATALOG}
provider = catalog_by_id.get(provider_id)
if provider is None:
raise HTTPException(
status_code=400,
detail=f"Unknown provider: {provider_id}. "
f"Available: {', '.join(sorted(valid_ids))}",
f"Available: {', '.join(sorted(catalog_by_id))}",
)
# Anthropic and claude-code clear the same Hermes-managed PKCE file
# AND forget the Claude Code import. We don't touch ~/.claude/* directly
# — that's owned by the Claude Code CLI; users can re-auth there if they
# want to undo a disconnect.
if provider_id in {"anthropic", "claude-code"}:
disconnect_hint = _oauth_provider_disconnect_hint(provider, {})
if disconnect_hint:
raise HTTPException(
status_code=400,
detail=f"{provider['name']} cannot be disconnected automatically. {disconnect_hint}",
)
status = _resolve_provider_status(provider_id, provider.get("status_fn"))
disconnect_hint = _oauth_provider_disconnect_hint(provider, status)
if disconnect_hint:
raise HTTPException(
status_code=400,
detail=f"{provider['name']} cannot be disconnected automatically. {disconnect_hint}",
)
# Anthropic clears only the Hermes-managed PKCE file and auth-store entry.
# The separate claude-code catalog row is external/read-only and rejected
# above so we never pretend to remove ~/.claude/* credentials owned by the CLI.
if provider_id == "anthropic":
cleared = False
try:
from agent.anthropic_adapter import _HERMES_OAUTH_FILE
if _HERMES_OAUTH_FILE.exists():
_HERMES_OAUTH_FILE.unlink()
cleared = True
except Exception:
pass
# Also clear the credential pool entry if present.
try:
from hermes_cli.auth import clear_provider_auth
clear_provider_auth("anthropic")
cleared = clear_provider_auth("anthropic") or cleared
except Exception:
pass
_log.info("oauth/disconnect: %s", provider_id)
return {"ok": True, "provider": provider_id}
return {"ok": bool(cleared), "provider": provider_id}
try:
from hermes_cli.auth import clear_provider_auth
from hermes_cli.auth import clear_provider_auth, invalidate_nous_auth_status_cache
cleared = clear_provider_auth(provider_id)
if provider_id == "nous":
invalidate_nous_auth_status_cache()
_log.info("oauth/disconnect: %s (cleared=%s)", provider_id, cleared)
return {"ok": bool(cleared), "provider": provider_id}
except Exception as e: