From a296bb42d2be9f444a4cc4bfbb6e339fbf317c02 Mon Sep 17 00:00:00 2001 From: Levi Hoyt Date: Sun, 17 May 2026 12:33:00 -0500 Subject: [PATCH] fix(xai-oauth): add --paste-code flag for WSL2 firewall workaround MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WSL2 ships with Hyper-V Firewall DefaultInboundAction=Block (Microsoft's current default with mirrored networking). xAI's OAuth redirect to http://127.0.0.1:56121/callback is silently dropped before reaching the WSL VM. xAI then renders its "Could not establish connection" fallback page showing the auth code as text — but stock Hermes has no way to accept that code, so the listener times out and the code is orphaned. This adds a stdin-paste path to `hermes auth add xai-oauth`: - New `--paste-code` flag (auto-enabled when `--no-browser` is set) - `_xai_wait_for_callback` spins up a daemon thread that reads one line from stdin and feeds the pasted code into the same callback handler via internal HTTP (127.0.0.1:/callback?code=...&state=...) - First-write-wins: whichever channel (browser redirect or stdin paste) delivers a code first short-circuits the wait loop Works on any platform with the loopback callback blocked: WSL2 + Hyper-V Firewall, GCP Cloud Shell, GitHub Codespaces, AWS Instance Connect, restrictive corporate proxies, etc. Refs: PR #27305 (community wrapper writeup), issue #27385 (loopback callback edge cases), issue #26923 (remote console OAuth fallback). --- hermes_cli/auth.py | 52 +++++++++++++++++++++++++++++++++++++ hermes_cli/auth_commands.py | 6 +++++ hermes_cli/main.py | 9 +++++++ 3 files changed, 67 insertions(+) diff --git a/hermes_cli/auth.py b/hermes_cli/auth.py index 6752b65829f7..1421ee5fe5ee 100644 --- a/hermes_cli/auth.py +++ b/hermes_cli/auth.py @@ -2193,7 +2193,47 @@ def _xai_wait_for_callback( result: dict[str, Any], *, timeout_seconds: float = 180.0, + allow_stdin_paste: bool = False, + paste_port: int | None = None, + paste_state: str | None = None, ) -> dict[str, Any]: + # === PATCH: xai-paste-code (hoyt-2026-05-17) === wait-for-callback + # On WSL2 with Hyper-V Firewall default-block policy (Microsoft's current + # default with mirrored networking), xAI's redirect to the loopback URI is + # silently dropped before reaching the WSL VM. xAI then shows a "Could not + # establish connection" page with the auth code as text. When + # allow_stdin_paste is True, this function also reads stdin in parallel + # and feeds any pasted code into the same callback handler via internal + # HTTP — same code path as the listener would take from a real redirect. + import sys as _sys + import threading as _threading + import urllib.request as _urlreq + + stdin_thread = None + if allow_stdin_paste and paste_port and paste_state and _sys.stdin and _sys.stdin.isatty(): + print() + print("If your browser shows 'Could not establish connection' with a code,") + print("paste the code here and press Enter (or wait for browser callback):") + def _stdin_reader(): + try: + line = _sys.stdin.readline().strip() + if not line: + return + # Strip surrounding quotes / accidental spaces + line = line.strip().strip('"').strip("'") + if result.get("code") or result.get("error"): + return # listener already won + # Deliver to local listener via internal curl-equivalent + url = f"http://127.0.0.1:{paste_port}/callback?code={line}&state={paste_state}" + try: + _urlreq.urlopen(url, timeout=5) + except Exception: + pass + except Exception: + pass + stdin_thread = _threading.Thread(target=_stdin_reader, daemon=True) + stdin_thread.start() + deadline = time.monotonic() + max(5.0, timeout_seconds) try: while time.monotonic() < deadline: @@ -5316,7 +5356,9 @@ def _xai_oauth_loopback_login( *, timeout_seconds: float = 20.0, open_browser: bool = True, + allow_stdin_paste: bool = False, ) -> Dict[str, Any]: + # === PATCH: xai-paste-code (hoyt-2026-05-17) === login-signature discovery = _xai_oauth_discovery(timeout_seconds) authorization_endpoint = discovery["authorization_endpoint"] token_endpoint = discovery["token_endpoint"] @@ -5353,11 +5395,21 @@ def _xai_oauth_loopback_login( else: print("Could not open the browser automatically; use the URL above.") + # Extract port from redirect_uri ("http://127.0.0.1:PORT/callback") + _paste_port = None + try: + from urllib.parse import urlparse as _urlparse + _paste_port = _urlparse(redirect_uri).port + except Exception: + pass callback = _xai_wait_for_callback( server, thread, callback_result, timeout_seconds=max(30.0, timeout_seconds * 9), + allow_stdin_paste=allow_stdin_paste, + paste_port=_paste_port, + paste_state=state, ) except Exception: try: diff --git a/hermes_cli/auth_commands.py b/hermes_cli/auth_commands.py index 10b040d8a1d4..6274a1252c3a 100644 --- a/hermes_cli/auth_commands.py +++ b/hermes_cli/auth_commands.py @@ -336,9 +336,15 @@ def auth_add_command(args) -> None: return if provider == "xai-oauth": + # === PATCH: xai-paste-code (hoyt-2026-05-17) === paste-fallback enabled + # by --paste-code OR auto-enabled when --no-browser (WSL2 firewall case). + _paste_fallback = bool(getattr(args, "paste_code", False)) or bool( + getattr(args, "no_browser", False) + ) creds = auth_mod._xai_oauth_loopback_login( timeout_seconds=getattr(args, "timeout", None) or 20.0, open_browser=not getattr(args, "no_browser", False), + allow_stdin_paste=_paste_fallback, ) label = (getattr(args, "label", None) or "").strip() or label_from_token( creds["tokens"]["access_token"], diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 662bc57b78de..cd07aeb8b236 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -10236,6 +10236,15 @@ def main(): action="store_true", help="Do not auto-open a browser for OAuth login", ) + # === PATCH: xai-paste-code (hoyt-2026-05-17) === --paste-code CLI flag + auth_add.add_argument( + "--paste-code", + action="store_true", + help=( + "Allow pasting the OAuth code via stdin (WSL2/firewall workaround). " + "Auto-enabled with --no-browser. xAI Grok OAuth only." + ), + ) auth_add.add_argument( "--timeout", type=float, help="OAuth/network timeout in seconds" )