diff --git a/agent/anthropic_adapter.py b/agent/anthropic_adapter.py index c63c71da7bc..4f7595c94d5 100644 --- a/agent/anthropic_adapter.py +++ b/agent/anthropic_adapter.py @@ -1297,7 +1297,15 @@ def run_oauth_setup_token() -> Optional[str]: # Stores credentials in ~/.hermes/.anthropic_oauth.json (our own file). _OAUTH_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e" -_OAUTH_TOKEN_URL = "https://console.anthropic.com/v1/oauth/token" +# Anthropic migrated the OAuth token endpoint to platform.claude.com; +# console.anthropic.com now 404s. Callers should iterate _OAUTH_TOKEN_URLS +# (new host first, console fallback). _OAUTH_TOKEN_URL is kept as the primary +# for backward compatibility with existing imports and now points at the live host. +_OAUTH_TOKEN_URLS = [ + "https://platform.claude.com/v1/oauth/token", + "https://console.anthropic.com/v1/oauth/token", +] +_OAUTH_TOKEN_URL = _OAUTH_TOKEN_URLS[0] _OAUTH_REDIRECT_URI = "https://console.anthropic.com/oauth/code/callback" _OAUTH_SCOPES = "org:create_api_key user:profile user:inference" _HERMES_OAUTH_FILE = get_hermes_home() / ".anthropic_oauth.json" @@ -1395,18 +1403,34 @@ def run_hermes_oauth_login_pure() -> Optional[Dict[str, Any]]: "code_verifier": verifier, }).encode() - req = urllib.request.Request( - _OAUTH_TOKEN_URL, - data=exchange_data, - headers={ - "Content-Type": "application/json", - "User-Agent": f"claude-cli/{_get_claude_code_version()} (external, cli)", - }, - method="POST", - ) + # Anthropic migrated the OAuth token endpoint to platform.claude.com; + # console.anthropic.com now 404s. Try the new host first, then fall + # back to console for older deployments (mirrors the refresh path). + result = None + last_error = None + for endpoint in _OAUTH_TOKEN_URLS: + req = urllib.request.Request( + endpoint, + data=exchange_data, + headers={ + "Content-Type": "application/json", + "User-Agent": f"claude-cli/{_get_claude_code_version()} (external, cli)", + }, + method="POST", + ) + try: + with urllib.request.urlopen(req, timeout=15) as resp: + result = json.loads(resp.read().decode()) + break + except Exception as exc: + last_error = exc + logger.debug("Anthropic token exchange failed at %s: %s", endpoint, exc) + continue - with urllib.request.urlopen(req, timeout=15) as resp: - result = json.loads(resp.read().decode()) + if result is None: + raise last_error if last_error is not None else ValueError( + "Anthropic token exchange failed" + ) except Exception as e: print(f"Token exchange failed: {e}") return None diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index 0a14fb0ef23..3c38717e01c 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -6123,6 +6123,7 @@ try: from agent.anthropic_adapter import ( _OAUTH_CLIENT_ID as _ANTHROPIC_OAUTH_CLIENT_ID, _OAUTH_TOKEN_URL as _ANTHROPIC_OAUTH_TOKEN_URL, + _OAUTH_TOKEN_URLS as _ANTHROPIC_OAUTH_TOKEN_URLS, _OAUTH_REDIRECT_URI as _ANTHROPIC_OAUTH_REDIRECT_URI, _OAUTH_SCOPES as _ANTHROPIC_OAUTH_SCOPES, _generate_pkce as _generate_pkce_pair, @@ -6311,22 +6312,31 @@ def _submit_anthropic_pkce( "redirect_uri": _ANTHROPIC_OAUTH_REDIRECT_URI, "code_verifier": sess["verifier"], }).encode() - req = urllib.request.Request( - _ANTHROPIC_OAUTH_TOKEN_URL, - data=exchange_data, - headers={ - "Content-Type": "application/json", - "User-Agent": "hermes-dashboard/1.0", - }, - method="POST", - ) - try: - with urllib.request.urlopen(req, timeout=20) as resp: - result = json.loads(resp.read().decode()) - except Exception as e: + # Anthropic migrated the OAuth token endpoint to platform.claude.com; + # console.anthropic.com now 404s. Try the new host first, then fall back. + result = None + last_exc = None + for _endpoint in _ANTHROPIC_OAUTH_TOKEN_URLS: + req = urllib.request.Request( + _endpoint, + data=exchange_data, + headers={ + "Content-Type": "application/json", + "User-Agent": "hermes-dashboard/1.0", + }, + method="POST", + ) + try: + with urllib.request.urlopen(req, timeout=20) as resp: + result = json.loads(resp.read().decode()) + break + except Exception as e: + last_exc = e + continue + if result is None: with _oauth_sessions_lock: sess["status"] = "error" - sess["error_message"] = f"Token exchange failed: {e}" + sess["error_message"] = f"Token exchange failed: {last_exc}" return {"ok": False, "status": "error", "message": sess["error_message"]} access_token = result.get("access_token", "")