{"repo":"NousResearch/hermes-agent","public_id":"26990","display_label":"xAI OAuth PKCE flow fixes and improvements","summary":"These items address a PKCE code_challenge/code_verifier problem in the xAI OAuth token exchange, along with related OAuth flow unification, a macOS race condition, and API key fallback handling.","subsystem":null,"counts":{"members":3,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":27560,"merged_at":"2026-05-17T19:35:02+00:00"},"resolution":{"state":"resolved","by":"pr-27560"},"freshness":{"state":"verified","last_verified_at":"2026-05-17T19:35:02+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":26990,"pr_number":27560,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-05-17T19:35:02+00:00"}],"members":[{"kind":"issue","number":26990,"title":"xai-oauth token exchange fails: code_challenge is required (PKCE code_verifier missing)","url":null,"state":"closed"},{"kind":"pr","number":27560,"title":"fix(xai-oauth): echo code_challenge in token POST so PKCE exchange succeeds (#26990)","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["hermes_cli/auth.py","tests/hermes_cli/test_xai_oauth_pkce_token_exchange.py"],"fix_hunks":[{"file":"hermes_cli/auth.py","hunks":["@@ -5312,6 +5312,107 @@ def _xai_oauth_build_authorize_url(\n     return f\"{authorization_endpoint}?{urlencode(authorize_params)}\"\n \n \n+def _xai_oauth_exchange_code_for_tokens(\n+    *,\n+    token_endpoint: str,\n+    code: str,\n+    redirect_uri: str,\n+    code_verifier: str,\n+    code_challenge: str,\n+    timeout_seconds: float = 20.0,\n+) -> Dict[str, Any]:\n+    \"\"\"POST the authorization code to xAI's token endpoint and return\n+    the parsed JSON payload.\n+\n+    Sends ``code_verifier`` as required by RFC 7636 §4.5.  Also echoes\n+    ``code_challenge`` + ``code_challenge_method`` in the request body\n+    as a defense-in-depth measure for OAuth servers (xAI's among them,\n+    per #26990) that re-validate the challenge at the token step\n+    instead of relying solely on server-side session state captured\n+    during the authorize step.  Echoing the challenge is harmless for\n+    strict RFC-compliant servers — RFC 7636 doesn't forbid additional\n+    parameters at the token endpoint — and decisively fixes the\n+    ``code_challenge is required`` failure mode users hit on the\n+    loopback flow.\n+\n+    Raises :class:`AuthError` on any non-2xx response or transport\n+    failure; the error message embeds the HTTP status code and the\n+    full response body so users can disambiguate cause at a glance.\n+    \"\"\"\n+    # Paranoia: if upstream call sites ever drop ``code_verifier`` we\n+    # want to surface a precise, local error rather than send a\n+    # missing-PKCE request to xAI and receive their generic \"code\n+    # challenge required\" message back.\n+    if not code_verifier:\n+        raise AuthError(\n+            \"xAI token exchange refused locally: PKCE code_verifier is empty. \"\n+            \"This is a bug in Hermes — please report at \"\n+            \"https://github.com/NousResearch/hermes-agent/issues/26990.\",\n+            provider=\"xai-oauth\",\n+            code=\"xai_pkce_verifier_missing\",\n+        )\n+\n+    data = {\n+        \"grant_type\": \"authorization_code\",\n+        \"code\": code,\n+        \"redirect_uri\": redirect_uri,\n+        \"client_id\": XAI_OAUTH_CLIENT_ID,\n+        \"code_verifier\": code_verifier,\n+    }\n+    # Defense-in-depth: include the original ``code_challenge`` and\n+    # ``code_challenge_method``.  Some OAuth servers (including xAI's\n+    # auth.x.ai implementation, per the symptom reported in #26990)\n+    # validate these at the token endpoint instead of relying purely on\n+    # state captured during the authorize step — without them, xAI\n+    # rejects the exchange with ``code_challenge is required`` even\n+    # though we sent a valid ``code_verifier``.\n+    if code_challenge:\n+        data[\"code_challenge\"] = code_challenge\n+        data[\"code_challenge_method\"] = \"S256\"\n+\n+    try:\n+        response = httpx.post(\n+            token_endpoint,\n+            headers={\n+                \"Content-Type\": \"application/x-www-form-urlencoded\",\n+                \"Accept\": \"application/json\",\n+            },\n+            data=data,\n+            timeout=max(20.0, timeout_seconds),\n+        )\n+    except Exception as exc:\n+        raise AuthError(\n+            f\"xAI token exchange failed: {exc}\",\n+            provider=\"xai-oauth\",\n+            code=\"xai_token_exchange_failed\",\n+        ) from exc\n+\n+    if response.status_code != 200:\n+        body = response.text.strip()\n+        raise AuthError(\n+            f\"xAI token exchange failed (HTTP {response.status_code}).\"\n+            + (f\" Response: {body}\" if body else \"\"),\n+            provider=\"xai-oauth\",\n+            code=\"xai_token_exchange_failed\",\n+        )\n+\n+    try:\n+        payload = response.json()\n+    except Exception as exc:\n+        raise AuthError(\n+            f\"xAI token exchange returned invalid JSON: {exc}\",\n+            provider=\"xai-oauth\",\n+            code=\"xai_token_exchange_invalid\",\n+        ) from exc\n+    if not isinstance(payload, dict):\n+        raise AuthError(\n+            \"xAI token exchange response was not a JSON object.\",\n+            provider=\"xai-oauth\",\n+            code=\"xai_token_exchange_invalid\",\n+        )\n+    return payload\n+\n+\n def _xai_oauth_loopback_login(\n     *,\n     timeout_seconds: float = 20.0,","@@ -5392,47 +5493,14 @@ def _xai_oauth_loopback_login(\n             code=\"xai_code_missing\",\n         )\n \n-    try:\n-        response = httpx.post(\n-            token_endpoint,\n-            headers={\"Content-Type\": \"application/x-www-form-urlencoded\", \"Accept\": \"application/json\"},\n-            data={\n-                \"grant_type\": \"authorization_code\",\n-                \"code\": code,\n-                \"redirect_uri\": redirect_uri,\n-                \"client_id\": XAI_OAUTH_CLIENT_ID,\n-                \"code_verifier\": code_verifier,\n-            },\n-            timeout=max(20.0, timeout_seconds),\n-        )\n-    except Exception as exc:\n-        raise AuthError(\n-            f\"xAI token exchange failed: {exc}\",\n-            provider=\"xai-oauth\",\n-            code=\"xai_token_exchange_failed\",\n-        ) from exc\n-    if response.status_code != 200:\n-        detail = response.text.strip()\n-        raise AuthError(\n-            \"xAI token exchange failed.\"\n-            + (f\" Response: {detail}\" if detail else \"\"),\n-            provider=\"xai-oauth\",\n-            code=\"xai_token_exchange_failed\",\n-        )\n-    try:\n-        payload = response.json()\n-    except Exception as exc:\n-        raise AuthError(\n-            f\"xAI token exchange returned invalid JSON: {exc}\",\n-            provider=\"xai-oauth\",\n-            code=\"xai_token_exchange_invalid\",\n-        ) from exc\n-    if not isinstance(payload, dict):\n-        raise AuthError(\n-            \"xAI token exchange response was not a JSON object.\",\n-            provider=\"xai-oauth\",\n-            code=\"xai_token_exchange_invalid\",\n-        )\n+    payload = _xai_oauth_exchange_code_for_tokens(\n+        token_endpoint=[redacted:secret-assignment]\n+        code=code,\n+        redirect_uri=redirect_uri,\n+        code_verifier=code_verifier,\n+        code_challenge=code_challenge,\n+        timeout_seconds=timeout_seconds,\n+    )\n     access_token = [redacted:secret-assignment]\"access_token\", \"\") or \"\").strip()\n     refresh_token = [redacted:secret-assignment]\"refresh_token\", \"\") or \"\").strip()\n     if not access_token:"]},{"file":"tests/hermes_cli/test_xai_oauth_pkce_token_exchange.py","hunks":["@@ -0,0 +1,359 @@\n+\"\"\"Regression coverage for xAI OAuth PKCE token exchange (issue #26990).\n+\n+Issue [#26990] reported that ``hermes auth add xai-oauth`` succeeds at the\n+browser-side authorize step but fails at the token endpoint with\n+``code_challenge is required`` — the symptom of an OAuth server that\n+re-validates PKCE at the token step instead of relying purely on\n+state captured during the authorize redirect.\n+\n+The fix in ``hermes_cli/auth.py`` extracts the token POST into\n+:func:`_xai_oauth_exchange_code_for_tokens` and:\n+\n+* Sends ``code_verifier`` (RFC 7636 §4.5 requirement).\n+* **Also** echoes ``code_challenge`` and ``code_challenge_method``\n+  in the request body as defense-in-depth — strictly compliant\n+  servers ignore extras at the token endpoint, but xAI's server\n+  needs them.\n+* Refuses to fire the POST locally when ``code_verifier`` is empty\n+  (avoids leaking the auth code to a server that can't redeem it).\n+* Surfaces the HTTP status code prominently in the error message so\n+  users / maintainers can tell a 400 (bad request) from a 403\n+  (entitlement denied) at a glance.\n+\n+These tests pin all three behaviors so the fix can't silently regress.\n+\"\"\"\n+\n+from __future__ import annotations\n+\n+from typing import Any, Dict, List\n+from urllib.parse import parse_qs\n+\n+import httpx\n+import pytest\n+\n+from hermes_cli.auth import (\n+    AuthError,\n+    XAI_OAUTH_CLIENT_ID,\n+    _xai_oauth_exchange_code_for_tokens,\n+)\n+\n+\n+# ---------------------------------------------------------------------------\n+# httpx.post recorder\n+# ---------------------------------------------------------------------------\n+\n+\n+class _PostRecorder:\n+    \"\"\"Capture every ``httpx.post`` call without touching the network.\"\"\"\n+\n+    def __init__(self, response: httpx.Response) -> None:\n+        self.response = response\n+        self.calls: List[Dict[str, Any]] = []\n+\n+    def __call__(self, url, *, headers=None, data=None, timeout=None, **kw):\n+        self.calls.append(\n+            {\"url\": url, \"headers\": headers or {}, \"data\": data or {},\n+             \"timeout\": timeout, \"extra\": kw}\n+        )\n+        return self.response\n+\n+\n+def _ok_response(payload: dict) -> httpx.Response:\n+    return httpx.Response(200, json=payload)\n+\n+\n+def _err_response(status: int, body: str) -> httpx.Response:\n+    return httpx.Response(status, text=body)\n+\n+\n[redacted:email]\n+def post_recorder(monkeypatch):\n+    \"\"\"Default: 200 response with a full xAI token payload.\"\"\"\n+    recorder = _PostRecorder(\n+        _ok_response(\n+            {\n+                \"access_token\": \"[redacted:secret-assignment]\",\n+                \"refresh_token\": \"[redacted:secret-assignment]\",\n+                \"id_token\": \"ID\",\n+                \"expires_in\": 3600,\n+                \"token_type\": \"[redacted:secret-assignment]\",\n+            }\n+        )\n+    )\n+    monkeypatch.setattr(\"hermes_cli.auth.httpx.post\", recorder)\n+    return recorder\n+\n+\n+# ---------------------------------------------------------------------------\n+# Core contract: which fields go on the wire?\n+# ---------------------------------------------------------------------------\n+\n+\n+def test_token_exchange_includes_code_verifier(post_recorder):\n+    \"\"\"RFC 7636 §4.5 — ``code_verifier`` MUST be sent.\"\"\"\n+    _xai_oauth_exchange_code_for_tokens(\n+        token_endpoint=\"[redacted:secret-assignment]\",\n+        code=\"AUTHCODE\",\n+        redirect_uri=\"http://127.0.0.1:56121/callback\",\n+        code_verifier=\"theVerifier_43_to_128_chars_____________________\",\n+        code_challenge=\"aBcDeF\",\n+    )\n+    sent = post_recorder.calls[-1][\"data\"]\n+    assert sent[\"code_verifier\"] == \"theVerifier_43_to_128_chars_____________________\"\n+\n+\n+def test_token_exchange_also_echoes_code_challenge_for_xai(post_recorder):\n+    \"\"\"Defense-in-depth for #26990 — xAI re-validates the challenge\n+    at the token endpoint, not just at authorize.  Without this echo\n+    we get ``code_challenge is required`` even though we send a valid\n+    ``code_verifier``.\"\"\"\n+    _xai_oauth_exchange_code_for_tokens(\n+        token_endpoint=\"[redacted:secret-assignment]\",\n+        code=\"AUTHCODE\",\n+        redirect_uri=\"http://127.0.0.1:56121/callback\",\n+        code_verifier=\"v\" * 64,\n+        code_challenge=\"aBcDeF\",\n+    )\n+    sent = post_recorder.calls[-1][\"data\"]\n+    assert sent[\"code_challenge\"] == \"aBcDeF\"\n+    assert sent[\"code_challenge_method\"] == \"S256\"\n+\n+\n+def test_token_exchange_uses_correct_grant_and_client(post_recorder):\n+    \"\"\"Lock the static fields too — a future refactor must not flip\n+    these to ``client_credentials`` or drop ``client_id``.\"\"\"\n+    _xai_oauth_exchange_code_for_tokens(\n+        token_endpoint=\"[redacted:secret-assignment]\",\n+        code=\"AUTHCODE\",\n+        redirect_uri=\"http://127.0.0.1:56121/callback\",\n+        code_verifier=\"v\" * 64,\n+        code_challenge=\"c\" * 43,\n+    )\n+    sent = post_recorder.calls[-1][\"data\"]\n+    assert sent[\"grant_type\"] == \"authorization_code\"\n+    assert sent[\"code\"] == \"AUTHCODE\"\n+    assert sent[\"redirect_uri\"] == \"http://127.0.0.1:56121/callback\"\n+    assert sent[\"client_id\"] == XAI_OAUTH_CLIENT_ID\n+\n+\n+def test_token_exchange_uses_form_urlencoded_content_type(post_recorder):\n+    \"\"\"xAI's token endpoint expects ``application/x-www-form-urlencoded``.\"\"\"\n+    _xai_oauth_exchange_code_for_tokens(\n+        token_endpoint=\"[redacted:secret-assignment]\",\n+        code=\"AUTHCODE\",\n+        redirect_uri=\"http://127.0.0.1:56121/callback\",\n+        code_verifier=\"v\" * 64,\n+        code_challenge=\"c\" * 43,\n+    )\n+    headers = post_recorder.calls[-1][\"headers\"]\n+    assert headers[\"Content-Type\"] == \"application/x-www-form-urlencoded\"\n+    assert headers[\"Accept\"] == \"application/json\"\n+\n+\n+def test_token_exchange_targets_the_supplied_endpoint(post_recorder):\n+    \"\"\"Some test fixtures sniff the discovered token endpoint dynamically.\n+    We must POST to the URL the caller passed, not a hard-coded constant.\"\"\"\n+    _xai_oauth_exchange_code_for_tokens(\n+        token_endpoint=\"[redacted:secret-assignment]\",\n+        code=\"AUTHCODE\",\n+        redirect_uri=\"http://127.0.0.1:56121/callback\",\n+        code_verifier=\"v\" * 64,\n+        code_challenge=\"c\" * 43,\n+    )\n+    assert post_recorder.calls[-1][\"url\"] == \"https://auth.x.ai/some/other/token/path\"\n+\n+\n+def test_token_exchange_passes_timeout_through(post_recorder):\n+    \"\"\"Operators on slow networks pass a higher ``timeout_seconds``;\n+    the helper must forward it (and bump the floor to 20s).\"\"\"\n+    _xai_oauth_exchange_code_for_tokens(\n+        token_endpoint=\"[redacted:secret-assignment]\",\n+        code=\"AUTHCODE\",\n+        redirect_uri=\"http://127.0.0.1:56121/callback\",\n+        code_verifier=\"v\" * 64,\n+        code_challenge=\"c\" * 43,\n+        timeout_seconds=45.0,\n+    )\n+    assert post_recorder.calls[-1][\"timeout\"] == 45.0\n+\n+\n+def test_token_exchange_floor_timeout_is_20s(post_recorder):\n+    _xai_oauth_exchange_code_for_tokens(\n+        token_endpoint=\"[redacted:secret-assignment]\",\n+        code=\"AUTHCODE\",\n+        redirect_uri=\"http://127.0.0.1:56121/callback\",\n+        code_verifier=\"v\" * 64,\n+        code_challenge=\"c\" * 43,\n+        timeout_seconds=2.0,\n+    )\n+    assert post_recorder.calls[-1][\"timeout\"] == 20.0\n+\n+\n+# ---------------------------------------------------------------------------\n+# Sanity guard: refuse to POST with an empty code_verifier\n+# ---------------------------------------------------------------------------\n+\n+\n+def test_empty_code_verifier_raises_without_posting(post_recorder):\n+    \"\"\"If ``code_verifier`` is somehow lost upstream, we must refuse to\n+    send the request — leaking an authorization code to xAI without a\n+    verifier is worse than failing locally with an actionable error.\"\"\"\n+    with pytest.raises(AuthError) as exc_info:\n+        _xai_oauth_exchange_code_for_tokens(\n+            token_endpoint=\"[redacted:secret-assignment]\",\n+            code=\"AUTHCODE\",\n+            redirect_uri=\"http://127.0.0.1:56121/callback\",\n+            code_verifier=\"\",\n+            code_challenge=\"c\" * 43,\n+        )\n+    assert exc_info.value.code == \"xai_pkce_verifier_missing\"\n+    assert \"26990\" in str(exc_info.value)\n+    # And critically: nothing was sent.\n+    assert post_recorder.calls == []\n+\n+\n+def test_missing_code_challenge_omits_echo_but_still_sends_verifier(post_recorder):\n+    \"\"\"``code_challenge`` is defensive — if a caller doesn't have it\n+    handy, we must still send the standards-compliant request rather\n+    than refusing.  This keeps RFC-compliant servers happy.\"\"\"\n+    _xai_oauth_exchange_code_for_tokens(\n+        token_endpoint=\"[redacted:secret-assignment]\",\n+        code=\"AUTHCODE\",\n+        redirect_uri=\"http://127.0.0.1:56121/callback\",\n+        code_verifier=\"v\" * 64,\n+        code_challenge=\"\",\n+    )\n+    sent = post_recorder.calls[-1][\"data\"]\n+    assert sent[\"code_verifier\"] == \"v\" * 64\n+    assert \"code_challenge\" not in sent\n+    assert \"code_challenge_method\" not in sent\n+\n+\n+# ---------------------------------------------------------------------------\n+# Error surfacing\n+# ---------------------------------------------------------------------------\n+\n+\n+def test_non_200_response_surfaces_status_and_body(monkeypatch):\n+    \"\"\"When xAI returns a 4xx, the operator needs both the HTTP status\n+    code (to tell 400 from 401 from 403 at a glance) and the response\n+    body (the actual server-side reason).\"\"\"\n+    recorder = _PostRecorder(\n+        _err_response(400, '{\"error\":\"invalid_grant\",\"error_description\":\"code_challenge is required\"}')\n+    )\n+    monkeypatch.setattr(\"hermes_cli.auth.httpx.post\", recorder)\n+    with pytest.raises(AuthError) as exc_info:\n+        _xai_oauth_exchange_code_for_tokens(\n+            token_endpoint=\"[redacted:secret-assignment]\",\n+            code=\"AUTHCODE\",\n+            redirect_uri=\"http://127.0.0.1:56121/callback\",\n+            code_verifier=\"v\" * 64,\n+            code_challenge=\"c\" * 43,\n+        )\n+    msg = str(exc_info.value)\n+    assert \"HTTP 400\" in msg, (\n+        \"Status code must be in the error so callers can disambiguate \"\n+        \"tier-denied (403) from bad-request (400) without inspecting \"\n+        \"exc.code.\"\n+    )\n+    assert \"code_challenge is required\" in msg\n+    assert exc_info.value.code == \"xai_token_exchange_failed\"\n+\n+\n+def test_transport_error_wraps_as_auth_error(monkeypatch):\n+    \"\"\"A connection failure must come back as ``AuthError`` so the\n+    surrounding ``format_auth_error`` UI mapping fires correctly.\"\"\"\n+\n+    def _boom(*args, **kwargs):\n+        raise httpx.ConnectError(\"dns failure\")\n+\n+    monkeypatch.setattr(\"hermes_cli.auth.httpx.post\", _boom)\n+    with pytest.raises(AuthError) as exc_info:\n+        _xai_oauth_exchange_code_for_tokens(\n+            token_endpoint=\"[redacted:secret-assignment]\",\n+            code=\"AUTHCODE\",\n+            redirect_uri=\"http://127.0.0.1:56121/callback\",\n+            code_verifier=\"v\" * 64,\n+            code_challenge=\"c\" * 43,\n+        )\n+    assert exc_info.value.code == \"xai_token_exchange_failed\"\n+    assert \"dns failure\" in str(exc_info.value)\n+\n+\n+def test_non_dict_payload_raises_invalid_json(monkeypatch):\n+    \"\"\"xAI returning ``[]`` or a string at 200 is a server bug — fail\n+    with a precise error rather than crashing later in token storage.\"\"\"\n+    recorder = _PostRecorder(_ok_response([1, 2, 3]))  # type: ignore[arg-type]\n+    monkeypatch.setattr(\"hermes_cli.auth.httpx.post\", recorder)\n+    with pytest.raises(AuthError) as exc_info:\n+        _xai_oauth_exchange_code_for_tokens(\n+            token_endpoint=\"[redacted:secret-assignment]\",\n+            code=\"AUTHCODE\",\n+            redirect_uri=\"http://127.0.0.1:56121/callback\",\n+            code_verifier=\"v\" * 64,\n+            code_challenge=\"c\" * 43,\n+        )\n+    assert exc_info.value.code == \"xai_token_exchange_invalid\"\n+\n+\n+def test_success_returns_full_payload_dict(post_recorder):\n+    \"\"\"200 happy path: the parsed JSON dict comes back verbatim so the\n+    caller can pluck ``access_token`` / ``refresh_token`` etc.\"\"\"\n+    out = _xai_oauth_exchange_code_for_tokens(\n+        token_endpoint=\"[redacted:secret-assignment]\",\n+        code=\"AUTHCODE\",\n+        redirect_uri=\"http://127.0.0.1:56121/callback\",\n+        code_verifier=\"v\" * 64,\n+        code_challenge=\"c\" * 43,\n+    )\n+    assert out[\"access_token\"] == \"AT-fresh\"\n+    assert out[\"refresh_token\"] == \"RT-fresh\"\n+\n+\n+# ---------------------------------------------------------------------------\n+# Wire-format guard: httpx must serialise ``data`` as form-urlencoded\n+# ---------------------------------------------------------------------------\n+\n+\n+def test_wire_format_is_form_urlencoded_with_all_pkce_fields(monkeypatch):\n+    \"\"\"End-to-end check on the actual bytes httpx puts on the wire.\n+    If anyone ever swaps ``data=`` for ``json=`` or refactors the dict,\n+    xAI will start rejecting again — this catches it locally.\"\"\"\n+\n+    captured: Dict[str, Any] = {}\n+\n+    class _Transport(httpx.BaseTransport):\n+        def handle_request(self, request):\n+            captured[\"body\"] = bytes(request.read())\n+            captured[\"content_type\"] = request.headers.get(\"content-type\", \"\")\n+            return httpx.Response(\n+                200,\n+                json={\"access_token\": \"AT\", \"refresh_token\": \"RT\",\n+                      \"id_token\": \"\", \"expires_in\": 60, \"token_type\": \"[redacted:secret-assignment]\"},\n+            )\n+\n+    real_post = httpx.post\n+\n+    def _post(*args, **kwargs):\n+        with httpx.Client(transport=_Transport()) as c:\n+            return c.post(*args, **kwargs)\n+\n+    monkeypatch.setattr(\"hermes_cli.auth.httpx.post\", _post)\n+\n+    _xai_oauth_exchange_code_for_tokens(\n+        token_endpoint=\"[redacted:secret-assignment]\",\n+        code=\"AUTHCODE\",\n+        redirect_uri=\"http://127.0.0.1:56121/callback\",\n+        code_verifier=\"theVerifier_43+\",\n+        code_challenge=\"theChallenge_43+\",\n+    )\n+\n+    assert \"application/x-www-form-urlencoded\" in captured[\"content_type\"]\n+    parsed = parse_qs(captured[\"body\"].decode())\n+    assert parsed[\"grant_type\"] == [\"authorization_code\"]\n+    assert parsed[\"code\"] == [\"AUTHCODE\"]\n+    assert parsed[\"redirect_uri\"] == [\"http://127.0.0.1:56121/callback\"]\n+    assert parsed[\"client_id\"] == [XAI_OAUTH_CLIENT_ID]\n+    assert parsed[\"code_verifier\"] == [\"theVerifier_43+\"]\n+    assert parsed[\"code_challenge\"] == [\"theChallenge_43+\"]\n+    assert parsed[\"code_challenge_method\"] == [\"S256\"]\n"]}]},{"kind":"pr","number":27677,"title":"feat(xai-oauth): unify OAuth flow, fix macOS race, return OpenAI for aux, add API key fallback","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"eac198b6d5035dac78e6dc5b20fb2f6f85c3fd52","number":0},{"kind":"commit","value":"0d63661702162bf36bcb695000280f9c1687d742","number":0},{"kind":"commit","value":"cb53c40e459f1913d086a3ba942746eb605ec6f5","number":0},{"kind":"pr","value":"#27677","number":27677},{"kind":"pr","value":"#26929","number":26929}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}