Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions hermes_cli/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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"]
Expand Down Expand Up @@ -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:
Expand Down
6 changes: 6 additions & 0 deletions hermes_cli/auth_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
9 changes: 9 additions & 0 deletions hermes_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down