xAI OAuth loopback callback timeout and fallback fixes

NousResearch/hermes-agent · cluster 27385 · agent

Data as of

Nothing open on GitHub. Fix merged in #28110. Check it covers your case.

The short answer

0open issues
0open PRs
8items grouped
Known?
Yes. 8 related items grouped here, 2 issues already closed.
Fix event
A fix was merged to main in PR #28110. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this partially resolved, deciding PR #33231. That is separate from the fix event.
Still open
Nothing open on GitHub.

Multiple issues and fixes addressing xAI OAuth loopback server failures on macOS, where the local callback is received but the auth flow times out. Changes include diagnostics, threaded callback handling, 'not received' page display, and manual paste fallback on timeout.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #28110 fix(xai-oauth): make loopback callback server threaded + latch first result

    merged to main

    files: hermes_cli/auth.py, scripts/release.py, tests/hermes_cli/test_auth_xai_oauth_provider.py

  • #28112 fix(xai-oauth): show 'not received' page when loopback callback has no code

    merged to main

    files: hermes_cli/auth.py, tests/hermes_cli/test_auth_xai_oauth_provider.py

    Show the diff
    • hermes_cli/auth.py
      @@ -2405,15 +2405,37 @@ def do_GET(self) -> None:  # noqa: N802
                       "error": params.get("error", [None])[0],
                       "error_description": params.get("error_description", [None])[0],
                   }
      +
      +            # Treat a hit on the callback path with neither `code` nor `error`
      +            # as a missing OAuth callback (e.g. xAI's auth backend failed to
      +            # redirect and the user navigated to the bare loopback URL by hand).
      +            # Show an explicit "not received" page rather than the success page —
      +            # otherwise the browser claims authorization succeeded while the CLI
      +            # is still waiting for a real callback and eventually times out.
      +            if incoming["code"] is None and incoming["error"] is None:
      +                self.send_response(400)
      +                self._maybe_write_cors_headers()
      +                self.send_header("Content-Type", "text/html; charset=utf-8")
      +                self.end_headers()
      +                body = (
      +                    "<html><body>"
      +                    "<h1>xAI authorization not received.</h1>"
      +                    "<p>No authorization code was present in this callback URL. "
      +                    "Return to the terminal and re-run "
      +                    "<code>hermes auth add xai-oauth</code> to retry.</p>"
      +                    "</body></html>"
      +                )
      +                self.wfile.write(body.encode("utf-8"))
      +                return
      +
                   # 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)
      +            with result_lock:
      +                if not (result["code"] or result["error"]):
      +                    result.update(incoming)
       
                   self.send_response(200)
                   self._maybe_write_cors_headers()
    • tests/hermes_cli/test_auth_xai_oauth_provider.py
      @@ -326,6 +326,84 @@ def test_xai_callback_server_latches_first_terminal_callback_result():
               thread.join(timeout=1.0)
       
       
      +# ---------------------------------------------------------------------------
      +# Loopback callback handler GET responses
      +# ---------------------------------------------------------------------------
      +
      +
      +def _get_callback(redirect_uri: str, query: str = "") -> tuple[int, str]:
      +    """GET the loopback callback URL with an optional query string."""
      +    from urllib.request import Request, urlopen
      +    from urllib.error import HTTPError
      +
      +    target = redirect_uri + (("?" + query) if query else "")
      +    req = Request(target, method="GET")
      +    try:
      +        with urlopen(req, timeout=5.0) as resp:
      +            return resp.getcode(), resp.read().decode("utf-8", "replace")
      +    except HTTPError as exc:
      +        return exc.code, exc.read().decode("utf-8", "replace")
      +
      +
      +def test_xai_callback_handler_returns_400_when_callback_url_lacks_code_and_error():
      +    """Bare loopback URL (no code, no error) must not claim authorization received.
      +
      +    Regression for #27385: when xAI's auth backend fails to redirect and the user
      +    manually navigates to http://127.0.0.1:<port>/callback, the handler used to
      +    return 200 "xAI authorization received" while the CLI's wait loop still timed
      +    out — leaving the user with a contradictory success page and a CLI error.
      +    """
      +    server, thread, result, redirect_uri = _xai_start_callback_server(preferred_port=0)
      +    try:
      +        status, body = _get_callback(redirect_uri)
      +        assert status == 400
      +        assert "not received" in body.lower()
      +        assert "hermes auth add xai-oauth" in body
      +        # Wait loop must still see no code/error so it raises a real timeout,
      +        # rather than treating this empty hit as a successful callback.
      +        assert result["code"] is None
      +        assert result["error"] is None
      +    finally:
      +        server.shutdown()
      +        server.server_close()
      +        thread.join(timeout=1.0)
      +
      +
      +def test_xai_callback_handler_accepts_callback_with_code():
      +    """A real OAuth redirect (code + state) still records both and shows success."""
      +    server, thread, result, redirect_uri = _xai_start_callback_server(preferred_port=0)
      +    try:
      +        status, body = _get_callback(redirect_uri, query="code=abc&state=xyz")
      +        assert status == 200
      +        assert "xAI authorization received" in body
      +        assert result["code"] == "abc"
      +        assert result["state"] == "xyz"
      +        assert result["error"] is None
      +    finally:
      +        server.shutdown()
      +        server.server_close()
      +        thread.join(timeout=1.0)
      +
      +
      +def test_xai_callback_handler_records_error_callback():
      +    """A redirect carrying an `error` param must surface the failure page and capture detail."""
      +    server, thread, result, redirect_uri = _xai_start_callback_server(preferred_port=0)
      +    try:
      +        status, body = _get_callback(
      +            redirect_uri,
      +            query="error=access_denied&error_description=user%20cancelled",
      +        )
      +        assert status == 200
      +        assert "xAI authorization failed" in body
      +        assert result["error"] == "access_denied"
      +        assert result["error_description"] == "user cancelled"
      +        assert result["code"] is None
      +    finally:
      +        server.shutdown()
      +        server.server_close()
      +        thread.join(timeout=1.0)
      +
      +
       # ---------------------------------------------------------------------------
       # Token roundtrip + reads
       # ---------------------------------------------------------------------------
  • #33231 fix(xai-oauth): fall back to manual paste on loopback timeout (#33006)

    merged to main

    files: hermes_cli/auth.py, tests/hermes_cli/test_auth_manual_paste.py

3 PRs closed without merging

From the close comments: merged via another change 2 · duplicate 1

  • #27439 fix(xai-oauth): show "not received" page when loopback callback has no code (#27385) · merged via another change → #28112, #28110
  • #33006 Fix xAI OAuth timeout manual fallback · merged via another change → 1b9c247, #33231, #31894
  • #33289 fix(xai-oauth): fall back to manual paste on loopback timeout (#33006) · duplicate → #33231
Closed issues · 2
  • Closed#27385[Bug]: xAI OAuth loopback on macOS: local callback received, but Hermes times out
  • Closed#32984[Setup]: Problem connecting to xAI Grok OAuth (Grok Business, includes SuperGrok)
How this cluster was grouped
  • Link evidence (partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
  • Members outside that structural core may belong to a different underlying problem.
  • Pairwise link density 0.23: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
agent
Merged PRs
#28110, #28112, #33231
Closed issues
2
Attempts
3 PRs closed without merging
Fix link checked
28 May 2026

For your agent

The same facts, typed.