diff --git a/scripts/release.py b/scripts/release.py index 03295b4c35f..acf52f71e3b 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -47,6 +47,7 @@ ACP_REGISTRY_MANIFEST = REPO_ROOT / "acp_registry" / "agent.json" AUTHOR_MAP = { "145739220+wgu9@users.noreply.github.com": "wgu9", # PR #51468 salvage (WSL/no-systemd orphan gateway tracking, #51325) "165020384+uperLu@users.noreply.github.com": "uperLu", # PR #50958 salvage (rename plugins/cron → plugins/cron_providers; #50872) + "277269729+yusekiotacode@users.noreply.github.com": "yusekiotacode", # PR #48706 salvage (anthropic OAuth login token endpoint → platform.claude.com; #45250/#49821) "minz0721@outlook.com": "s010mn", # PR #29221 salvage (ollama-cloud reasoning_effort xhigh→max) "jeevesassistant00@gmail.com": "jeeves-assistant", # PR #50771 (computer-use CuaDriver vision capture routing) "21178861+ScotterMonk@users.noreply.github.com": "ScotterMonk", # PR #50145 salvage (cron output truncation: adapter-aware chunking, #50126) diff --git a/tests/agent/test_anthropic_oauth_pkce.py b/tests/agent/test_anthropic_oauth_pkce.py index 49045e94541..4127d32a473 100644 --- a/tests/agent/test_anthropic_oauth_pkce.py +++ b/tests/agent/test_anthropic_oauth_pkce.py @@ -148,6 +148,111 @@ def test_authorization_url_state_is_not_pkce_verifier(monkeypatch, tmp_path): ) +def test_login_token_exchange_uses_platform_claude_host(monkeypatch, tmp_path): + """The login token exchange must hit ``platform.claude.com`` first. + + Anthropic migrated the OAuth token endpoint to ``platform.claude.com``; + ``console.anthropic.com`` now 404s, so a hardcoded console host makes a + fresh login impossible (issue #45250 / #49821). The refresh path already + iterates the new host first — the login path must do the same. + """ + monkeypatch.setenv("HERMES_HOME", str(tmp_path)) + + captured_token: Dict[str, Any] = {} + captured_url: Dict[str, str] = {} + _patch_oauth_flow( + monkeypatch, + callback_code="placeholder", + capture_token_request=captured_token, + capture_auth_url=captured_url, + ) + + import builtins + + def fake_input(*_a, **_kw): + qs = parse_qs(urlparse(captured_url.get("url", "")).query) + state = qs.get("state", [""])[0] + return f"auth-code#{state}" + + monkeypatch.setattr(builtins, "input", fake_input) + + from agent.anthropic_adapter import run_hermes_oauth_login_pure + + result = run_hermes_oauth_login_pure() + + assert result is not None, "login should succeed against the live host" + assert captured_token["url"] == "https://platform.claude.com/v1/oauth/token", ( + "login token exchange must target platform.claude.com first, not the " + "dead console.anthropic.com host (regression of #45250 / #49821)" + ) + + +def test_login_token_exchange_falls_back_to_console_host(monkeypatch, tmp_path): + """If ``platform.claude.com`` is unreachable, the login path must fall back + to the legacy ``console.anthropic.com`` host — mirroring the refresh path's + fallback list — rather than failing outright. + """ + import urllib.request + + monkeypatch.setenv("HERMES_HOME", str(tmp_path)) + + captured_url: Dict[str, str] = {} + _patch_oauth_flow( + monkeypatch, + callback_code="placeholder", + capture_auth_url=captured_url, + ) + + attempts: list[str] = [] + + class _FakeResponse: + def __init__(self, body: bytes) -> None: + self._body = body + + def __enter__(self): + return self + + def __exit__(self, *_exc): + return False + + def read(self): + return self._body + + def fake_urlopen(req, *_a, **_kw): + attempts.append(req.full_url) + if req.full_url.startswith("https://platform.claude.com"): + raise RuntimeError("HTTP Error 404: Not Found") + body = json.dumps( + { + "access_token": "sk-ant-test-access", + "refresh_token": "sk-ant-test-refresh", + "expires_in": 3600, + } + ).encode() + return _FakeResponse(body) + + monkeypatch.setattr(urllib.request, "urlopen", fake_urlopen) + + import builtins + + def fake_input(*_a, **_kw): + qs = parse_qs(urlparse(captured_url.get("url", "")).query) + state = qs.get("state", [""])[0] + return f"auth-code#{state}" + + monkeypatch.setattr(builtins, "input", fake_input) + + from agent.anthropic_adapter import run_hermes_oauth_login_pure + + result = run_hermes_oauth_login_pure() + + assert result is not None, "login should succeed via the console fallback" + assert attempts == [ + "https://platform.claude.com/v1/oauth/token", + "https://console.anthropic.com/v1/oauth/token", + ], "login must try platform.claude.com first, then fall back to console" + + def test_callback_state_mismatch_aborts(monkeypatch, tmp_path, caplog): """If the state returned in the callback does not match the one we sent in the authorization URL, the flow must abort before exchanging the code.