From 0d63661702162bf36bcb695000280f9c1687d742 Mon Sep 17 00:00:00 2001 From: Fewmanism Date: Mon, 18 May 2026 02:06:00 +0900 Subject: [PATCH] fix: latch xAI OAuth callback result --- hermes_cli/auth.py | 22 ++++++++++++++----- .../test_auth_xai_oauth_provider.py | 22 +++++++++++++++++++ 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/hermes_cli/auth.py b/hermes_cli/auth.py index c32bb94b868..2a5e7a213fe 100644 --- a/hermes_cli/auth.py +++ b/hermes_cli/auth.py @@ -2372,6 +2372,7 @@ def _make_xai_callback_handler(expected_path: str) -> tuple[type[BaseHTTPRequest "error": None, "error_description": None, } + result_lock = threading.Lock() class _XAICallbackHandler(BaseHTTPRequestHandler): def _maybe_write_cors_headers(self) -> None: @@ -2398,16 +2399,27 @@ def _make_xai_callback_handler(expected_path: str) -> tuple[type[BaseHTTPRequest return params = parse_qs(parsed.query) - result["code"] = params.get("code", [None])[0] - result["state"] = params.get("state", [None])[0] - result["error"] = params.get("error", [None])[0] - result["error_description"] = params.get("error_description", [None])[0] + incoming = { + "code": params.get("code", [None])[0], + "state": params.get("state", [None])[0], + "error": params.get("error", [None])[0], + "error_description": params.get("error_description", [None])[0], + } + # ThreadingHTTPServer allows a fallback/manual callback to complete + # while a browser connection is stuck. Once we have a terminal + # OAuth result (code or error), keep the first one so a later + # concurrent/invalid callback cannot overwrite state before + # validation in _xai_oauth_loopback_login(). + if incoming["code"] or incoming["error"]: + with result_lock: + if not (result["code"] or result["error"]): + result.update(incoming) self.send_response(200) self._maybe_write_cors_headers() self.send_header("Content-Type", "text/html; charset=utf-8") self.end_headers() - if result["error"]: + if incoming["error"]: body = "

xAI authorization failed.

You can close this tab." else: body = "

xAI authorization received.

You can close this tab." diff --git a/tests/hermes_cli/test_auth_xai_oauth_provider.py b/tests/hermes_cli/test_auth_xai_oauth_provider.py index 76c1e6228fa..344f8d6f153 100644 --- a/tests/hermes_cli/test_auth_xai_oauth_provider.py +++ b/tests/hermes_cli/test_auth_xai_oauth_provider.py @@ -304,6 +304,28 @@ def test_xai_callback_server_accepts_fallback_code_while_browser_connection_is_s thread.join(timeout=1.0) +def test_xai_callback_server_latches_first_terminal_callback_result(): + server, thread, result, redirect_uri = _xai_start_callback_server(preferred_port=0) + try: + with urllib.request.urlopen(f"{redirect_uri}?code=first-code&state=state-1", timeout=2) as response: + assert response.status == 200 + with urllib.request.urlopen( + f"{redirect_uri}?error=access_denied&error_description=late&state=state-2", + timeout=2, + ) as response: + body = response.read().decode("utf-8") + assert response.status == 200 + assert "xAI authorization failed" in body + assert result["code"] == "first-code" + assert result["state"] == "state-1" + assert result["error"] is None + assert result["error_description"] is None + finally: + server.shutdown() + server.server_close() + thread.join(timeout=1.0) + + # --------------------------------------------------------------------------- # Token roundtrip + reads # ---------------------------------------------------------------------------