OAuth manual paste fallback for remote console environments

NousResearch/hermes-agent · cluster 26923 · hermes cli

Data as of

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

The short answer

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

These issues and PRs address OAuth loopback redirect failures in remote or browser-based environments (e.g., GCP, Codespaces) where 127.0.0.1 is unreachable. They propose and implement a manual paste fallback for xAI and Google Gemini CLI OAuth flows.

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.

  • #28358 feat(oauth): add --manual-paste fallback for browser-only remote consoles (closes #26923)

    merged to main

    files: hermes_cli/auth.py, hermes_cli/auth_commands.py, hermes_cli/main.py, tests/hermes_cli/test_auth_manual_paste.py, website/docs/guides/oauth-over-ssh.md, website/docs/guides/xai-grok-oauth.md

  • #33880 fix(xai-oauth): accept bare-code manual paste (state=None) (#26923)

    merged to main

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

    Show the diff
    • hermes_cli/auth.py
      @@ -3181,6 +3181,9 @@ def _prompt_manual_callback_paste(redirect_uri: str) -> dict:
           print("not on your laptop) — that is expected.  Copy the FULL URL")
           print("from your browser's address bar of that failed page and paste")
           print("it below.  A bare '?code=...&state=...' fragment also works.")
      +    print("If the consent page shows the authorization code in-page")
      +    print("(xAI's current behavior) rather than redirecting, paste the")
      +    print("bare code value on its own.")
           print("───────────────────────────────────────────────────────────────")
           try:
               raw = input("Callback URL: ")
      @@ -6965,7 +6968,21 @@ def _stdin_supports_manual_paste() -> bool:
                   provider="xai-oauth",
                   code="xai_authorization_failed",
               )
      -    if callback.get("state") != state:
      +    callback_state = callback.get("state")
      +    # Manual-paste bare-code path: when a user pastes only the opaque
      +    # authorization code (no ``code=``/``state=`` query parameters),
      +    # ``_parse_pasted_callback`` returns ``state=None``.  xAI's consent
      +    # page renders the code in-page rather than redirecting through the
      +    # 127.0.0.1 callback, so on many remote setups (Cloud Shell, headless
      +    # VPS, container consoles) the bare code is the only thing the user
      +    # can obtain.  PKCE (code_verifier) still binds the exchange to this
      +    # client, so the local state-equality check is redundant on the
      +    # bare-code path — we substitute the locally generated state to keep
      +    # the rest of the validation chain (and the token exchange) unchanged.
      +    # See #26923 (AccursedGalaxy comment, 2026-05-20).
      +    if callback_state is None and manual_paste:
      +        callback_state = state
      +    if callback_state != state:
               raise AuthError(
                   "xAI authorization failed: state mismatch.",
                   provider="xai-oauth",
    • tests/hermes_cli/test_auth_manual_paste.py
      @@ -330,6 +330,107 @@ def test_xai_loopback_login_manual_paste_state_mismatch_raises(monkeypatch):
           assert exc.value.code == "xai_state_mismatch"
       
       
      +def test_xai_loopback_login_manual_paste_bare_code_succeeds(monkeypatch):
      +    """Bare-code paste (state=None) must complete login under manual_paste.
      +
      +    xAI's consent page renders the authorization code in-page rather than
      +    redirecting through 127.0.0.1, so on remote/headless setups the only
      +    value the user can obtain is the opaque code with no ``state=``
      +    parameter. ``_parse_pasted_callback`` correctly returns
      +    ``state=None`` for that input. The login flow must accept this case
      +    (PKCE still protects the exchange); historically it raised
      +    ``xai_state_mismatch``. Regression for the bare-code branch of #26923.
      +    """
      +    monkeypatch.setattr(
      +        auth_mod, "_xai_oauth_discovery",
      +        lambda *_a, **_k: {
      +            "authorization_endpoint": "https://auth.x.ai/oauth2/authorize",
      +            "token_endpoint": "https://auth.x.ai/oauth2/token",
      +        },
      +    )
      +    monkeypatch.setattr(
      +        auth_mod, "_prompt_manual_callback_paste",
      +        lambda _ru: {
      +            "code": "bare-opaque-code",
      +            "state": None,
      +            "error": None,
      +            "error_description": None,
      +        },
      +    )
      +
      +    def _fake_token_post(*_a, **_k):
      +        return _StubTokenResponse(
      +            {
      +                "access_token": "at",
      +                "refresh_token": "rt",
      +                "id_token": "",
      +                "expires_in": 3600,
      +                "token_type": "Bearer",
      +            }
      +        )
      +
      +    monkeypatch.setattr(auth_mod.httpx, "post", _fake_token_post)
      +
      +    with contextlib.redirect_stdout(io.StringIO()):
      +        creds = auth_mod._xai_oauth_loopback_login(manual_paste=True)
      +
      +    assert creds["tokens"]["access_token"] == "at"
      +    assert creds["tokens"]["refresh_token"] == "rt"
      +
      +
      +def test_xai_loopback_login_loopback_path_rejects_missing_state(monkeypatch):
      +    """Loopback (manual_paste=False) must NOT accept ``state=None``.
      +
      +    The bare-code relaxation only applies to the manual-paste path,
      +    where the user demonstrably has no way to supply ``state``. The
      +    HTTP-server path always sees ``state`` populated from the real
      +    callback query string, so missing state there means something is
      +    wrong (a malformed callback, an attacker-supplied request) and
      +    must still raise ``xai_state_mismatch``.
      +    """
      +    monkeypatch.setattr(
      +        auth_mod, "_xai_oauth_discovery",
      +        lambda *_a, **_k: {
      +            "authorization_endpoint": "https://auth.x.ai/oauth2/authorize",
      +            "token_endpoint": "https://auth.x.ai/oauth2/token",
      +        },
      +    )
      +
      +    class _StubServer:
      +        def shutdown(self):
      +            return None
      +
      +        def server_close(self):
      +            return None
      +
      +    monkeypatch.setattr(
      +        auth_mod, "_xai_start_callback_server",
      +        lambda *_a, **_k: (
      +            _StubServer(),
      +            None,
      +            {"code": "fake", "state": None, "error": None,
      +             "error_description": None},
      +            "http://127.0.0.1:56121/callback",
      +        ),
      +    )
      +    monkeypatch.setattr(
      +        auth_mod, "_xai_wait_for_callback",
      +        lambda *_a, **_k: {
      +            "code": "fake",
      +            "state": None,
      +            "error": None,
      +            "error_description": None,
      +        },
      +    )
      +    monkeypatch.setattr(auth_mod, "_xai_validate_loopback_redirect_uri", lambda _u: None)
      +    monkeypatch.setattr(auth_mod, "_print_loopback_ssh_hint", lambda *_a, **_k: None)
      +
      +    with contextlib.redirect_stdout(io.StringIO()):
      +        with pytest.raises(auth_mod.AuthError) as exc:
      +            auth_mod._xai_oauth_loopback_login(manual_paste=False, open_browser=False)
      +    assert exc.value.code == "xai_state_mismatch"
      +
      +
       def test_xai_loopback_login_manual_paste_missing_code_raises(monkeypatch):
           """Empty paste must surface as ``xai_code_missing``, not crash."""
           monkeypatch.setattr(

2 PRs closed without merging

From the close comments: merged via another change 1 · superseded by another PR 1

  • #26929 feat(oauth): add --manual-paste fallback for browser-only remote consoles (closes #26923) · merged via another change → #28358
  • #33297 fix: improve xAI Grok OAuth manual paste flow (#26923) · superseded by another PR → 442a920, #33880, #26923
Closed issues · 2
  • Closed#26923OAuth loopback login broken for remote/browser-based consoles (GCP, Codespaces, etc.)
  • Closed#28971Google Gemini CLI OAuth fails on remote servers — redirect to 127.0.0.1 unreachable
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.29: 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
hermes cli
Merged PRs
#28358, #33880
Closed issues
2
Attempts
2 PRs closed without merging
Fix link checked
28 May 2026

For your agent

The same facts, typed.