Skip to content

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

Closed
xxxigm wants to merge 3 commits into
NousResearch:mainfrom
xxxigm:fix/oauth-loopback-manual-paste-26923
Closed

feat(oauth): add --manual-paste fallback for browser-only remote consoles (closes #26923)#26929
xxxigm wants to merge 3 commits into
NousResearch:mainfrom
xxxigm:fix/oauth-loopback-manual-paste-26923

Conversation

@xxxigm

@xxxigm xxxigm commented May 16, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Adds a --manual-paste fallback to hermes auth add and hermes model for the xAI Grok OAuth loopback flow, so users on browser-only remote consoles can complete OAuth login without a real SSH client.

Issue #26923 reported that hermes model (and hermes auth add xai-oauth) hangs forever on GCP Cloud Shell, GitHub Codespaces, AWS EC2 Instance Connect, Gitpod, etc., because the loopback callback http://127.0.0.1:56121/callback is bound on the remote VM and the user's laptop browser can't reach it. The existing SSH-tunnel hint only fires when SSH_CLIENT / SSH_TTY are set — which Cloud Shell and friends don't set — leaving these users with literally no path forward.

The reporter confirmed the workaround that works: after approving in their laptop's browser, manually curl the failed callback URL (with ?code=...&state=...) at the loopback listener on the VM. They asked: would the maintainers be open to a PR that formalizes this manual code paste flow for all OAuth providers?

This PR is that formalization, scoped to xAI Grok OAuth (the documented reproduction in the issue). The same helpers — _parse_pasted_callback, _prompt_manual_callback_paste — are designed to be reused by Spotify (the only other true loopback flow in hermes_cli/auth.py) in a follow-up; the rest of the OAuth providers in this codebase use device-code flows that aren't affected.

Three pieces:

  1. Broaden remote detection_is_remote_session() now recognises CLOUD_SHELL, CODESPACES, CODESPACE_NAME, GITPOD_WORKSPACE_ID, REPL_ID, STACKBLITZ in addition to SSH_CLIENT / SSH_TTY. The existing tunnel hint at least fires in those environments — and the hint now also mentions --manual-paste so users without a real SSH client see a path forward instead of a dead-end recipe.
  2. Manual paste path_xai_oauth_loopback_login(manual_paste=True) skips the HTTP listener entirely. The redirect URI, PKCE verifier, state, and nonce are byte-identical to the loopback path so the upstream OAuth flow at xAI is exactly the same — this is purely a transport change for the callback hop, not a security downgrade.
  3. CLI wiring--manual-paste is registered on both hermes auth add and hermes model, forwarded through _login_xai_oauth and _model_flow_xai_oauth. The model picker now also forwards --no-browser and --timeout (previously hardcoded regardless of CLI flags) for consistency.

The _parse_pasted_callback helper accepts any of: a full URL (http(s)://127.0.0.1:56121/callback?code=...&state=...), a bare query string (?code=...&state=... or code=...&state=...), or a bare opaque code value — whichever shape the user finds easiest to copy. The CSRF state check still runs against the parsed result, so a wrong-state paste is rejected with xai_state_mismatch (no CSRF bypass).

Related Issue

Fixes #26923

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

  • hermes_cli/auth.py — Broaden _is_remote_session() to recognise Cloud Shell / Codespaces / Gitpod / Replit / StackBlitz env vars. New _parse_pasted_callback(raw) and _prompt_manual_callback_paste(redirect_uri) helpers. _xai_oauth_loopback_login gains a manual_paste: bool = False kwarg that skips the HTTP listener (still validating state / error / code through the existing branches, so no CSRF bypass). _print_loopback_ssh_hint now also points users at --manual-paste. _login_xai_oauth forwards args.manual_paste.
  • hermes_cli/auth_commands.py — Pool-add path forwards args.manual_paste to _xai_oauth_loopback_login (one-line wiring).
  • hermes_cli/main.py — Register --manual-paste on both auth add and model parsers (help text names OAuth loopback login broken for remote/browser-based consoles (GCP, Codespaces, etc.) #26923 + the affected consoles). _model_flow_xai_oauth accepts and forwards args so --manual-paste / --no-browser / --timeout reach the inner login call.
  • tests/hermes_cli/test_auth_manual_paste.py24 new tests in 5 sections:
    • 9 parametrised + scalar cases for the broadened _is_remote_session covering every documented remote env var (and a negative case when none are set).
    • 9 cases for _parse_pasted_callback covering every paste form, including malformed-URL safety.
    • 3 cases for _prompt_manual_callback_paste (happy path, EOF, Ctrl-C).
    • 3 end-to-end cases for _xai_oauth_loopback_login(manual_paste=True): (a) the HTTP server must not be started (asserted via a callable that raises if invoked), (b) wrong state is still rejected with xai_state_mismatch, (c) empty paste surfaces xai_code_missing.
    • 1 case that _print_loopback_ssh_hint now mentions --manual-paste for non-SSH remotes.
  • website/docs/guides/oauth-over-ssh.md — TL;DR note for --manual-paste, plus new "Browser-only remote (Cloud Shell / Codespaces / EC2 Instance Connect)" section with the recipe.
  • website/docs/guides/xai-grok-oauth.md — short subsection pointing at the same recipe.

How to Test

# 1. New + neighbour OAuth tests pass
python -m pytest tests/hermes_cli/test_auth_manual_paste.py \
                tests/hermes_cli/test_auth_loopback_ssh_hint.py \
                tests/hermes_cli/test_auth_xai_oauth_provider.py \
                tests/hermes_cli/test_auth_commands.py -q
# expected: 141 passed (24 new)

# 2. Simulated Cloud Shell environment trips _is_remote_session
python -c "
import os
os.environ.pop('SSH_CLIENT', None); os.environ.pop('SSH_TTY', None)
os.environ['CODESPACES'] = '1'
from hermes_cli.auth import _is_remote_session
assert _is_remote_session(), 'CODESPACES must be detected'
print('CODESPACES detected: OK')
"

# 3. Paste parser accepts every plausible form
python -c "
from hermes_cli.auth import _parse_pasted_callback
print(_parse_pasted_callback('http://127.0.0.1:56121/callback?code=abc&state=xyz'))
print(_parse_pasted_callback('?code=abc&state=xyz'))
print(_parse_pasted_callback('code=abc&state=xyz'))
print(_parse_pasted_callback('opaque-code-value'))
"
# expected: dicts with code='abc'/state='xyz' on the first three,
#           code='opaque-code-value'/state=None on the fourth

For end-to-end verification on a real Cloud Shell / Codespaces session:

hermes auth add xai-oauth --manual-paste
# → "Open this URL to authorize Hermes with xAI: ..."
# → "─── Manual callback paste ─────────────────────────────────────"
# → Open URL on laptop browser, approve, copy failed-page URL, paste.
# → Token exchange runs and credentials land in ~/.hermes/auth.json.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, test+docs(scope):)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix (no unrelated commits)
  • I've run the new + neighbour auth test suites and all 141 tests pass
  • I've added tests for my changes (24 new regression tests)
  • I've tested on my platform: macOS 15.6 (darwin 24.6.0)

Documentation & Housekeeping

  • I've updated relevant documentation (website/docs/guides/oauth-over-ssh.md, website/docs/guides/xai-grok-oauth.md)
  • I've updated cli-config.yaml.example if I added/changed config keys — N/A (no new config keys)
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — the paste path is pure stdlib (urlparse + input), no platform-specific code
  • I've updated tool descriptions/schemas if I changed tool behavior — N/A (auth flow only)

Screenshots / Logs

Before the fix (issue #26923 reproduction on GitHub Codespaces — CODESPACES=1, no SSH_CLIENT):

$ hermes auth add xai-oauth
Open this URL to authorize Hermes with xAI:
https://auth.x.ai/oauth2/authorize?...

Waiting for callback on http://127.0.0.1:56121/callback
# (hangs forever — laptop browser redirects to 127.0.0.1 locally,
#  the Codespaces VM listener never sees a request)

After the fix:

$ hermes auth add xai-oauth --manual-paste
Open this URL to authorize Hermes with xAI:
https://auth.x.ai/oauth2/authorize?...

─── Manual callback paste ─────────────────────────────────────
After approving in your browser, your browser will try to load
  http://127.0.0.1:56121/callback
which fails (the loopback listener is on this remote machine,
not on your laptop) — that is expected.  Copy the FULL URL
from your browser's address bar of that failed page and paste
it below.  A bare '?code=...&state=...' fragment also works.
───────────────────────────────────────────────────────────────
Callback URL: http://127.0.0.1:56121/callback?code=abc...&state=xyz...
Added xai-oauth OAuth credential #1: "..."

The hint when running without --manual-paste in a detected remote now ends with:

No SSH client (Cloud Shell / Codespaces / web IDE)?  Re-run with
`--manual-paste` to skip the loopback listener and paste the failed
callback URL directly.

xxxigm added 3 commits May 16, 2026 19:47
xAI Grok OAuth (and Spotify) use a loopback redirect to
``http://127.0.0.1:<port>/callback`` to capture the authorization
code. That works when the browser and Hermes run on the same
machine, and the SSH tunnel recipe handles the regular remote
case. It breaks completely on **browser-only remote consoles**
(GCP Cloud Shell, GitHub Codespaces, AWS EC2 Instance Connect,
Gitpod, Replit, …) where the user has a browser but no real SSH
client to forward a port — the redirect to 127.0.0.1 on the
remote VM simply isn't reachable from the laptop, and there's
nothing the existing flow can do about it (NousResearch#26923).

This commit adds the foundation for a manual-paste fallback:

* ``_is_remote_session`` now also recognises Cloud Shell,
  Codespaces, Gitpod, Replit, StackBlitz (in addition to SSH),
  so the existing tunnel hint at least fires in those
  environments.
* ``_parse_pasted_callback`` accepts any of: a full
  ``http(s)://...?code=...&state=...`` URL, a bare ``?code=...``
  query string, a bare ``code=...&state=...`` fragment, or a
  bare opaque code value.  Returns the same dict shape the HTTP
  callback handler produces, so the caller's state / error
  validation works unchanged (no CSRF bypass).
* ``_prompt_manual_callback_paste`` reads stdin with a clear
  multi-line explanation of what's happening and what to paste.
* ``_xai_oauth_loopback_login`` gains a ``manual_paste`` kwarg
  that skips the HTTP listener entirely.  The redirect_uri,
  PKCE verifier, state, and nonce are byte-identical to the
  loopback path so xAI's token endpoint can't tell the
  difference at the protocol level.
* ``_print_loopback_ssh_hint`` now also mentions
  ``--manual-paste`` so users without a real SSH client see a
  path forward instead of a dead-end tunnel recipe.
* ``_login_xai_oauth`` threads ``args.manual_paste`` into the
  loopback helper.
…model``

Register the new ``--manual-paste`` flag on both entry points and
thread it through to the xAI loopback login:

* ``hermes auth add xai-oauth --manual-paste`` — pool-add path,
  forwarded inside ``auth_commands.handle_auth_add``.
* ``hermes model --manual-paste`` — model-picker path, forwarded
  by ``_model_flow_xai_oauth`` into the synthetic ``argparse.Namespace``
  it passes to ``_login_xai_oauth``.  The picker also now forwards
  ``--no-browser`` and ``--timeout`` for consistency (previously
  hardcoded to defaults regardless of CLI flags).

Help text on both flags points at NousResearch#26923 and names the
browser-only remote consoles (Cloud Shell, Codespaces, EC2
Instance Connect) so users searching ``hermes --help`` can find
the workaround.
…y path (NousResearch#26923)

Tests (``tests/hermes_cli/test_auth_manual_paste.py``):

* 9 parametrised + scalar cases for ``_is_remote_session`` covering
  the new Cloud Shell / Codespaces / Gitpod / Replit / StackBlitz
  env vars (plus the existing SSH ones).
* 9 cases for ``_parse_pasted_callback`` covering every paste form
  (full URL, https URL with extra params, bare ``?code=...``, bare
  ``code=...`` fragment, bare opaque value, error+description,
  empty, whitespace-only, malformed URL).
* 3 cases for ``_prompt_manual_callback_paste`` (happy path, EOF,
  Ctrl-C).
* 3 end-to-end ``_xai_oauth_loopback_login(manual_paste=True)``
  cases: the HTTP server MUST NOT be started (asserted via a
  callable that raises if invoked), wrong state still rejected
  with ``xai_state_mismatch`` (no CSRF bypass), and empty paste
  surfaces ``xai_code_missing``.
* SSH-hint mention test ensures the ``--manual-paste`` instruction
  is printed in the remote-session hint.

Docs:

* ``oauth-over-ssh.md`` — new "Browser-only remote (Cloud Shell /
  Codespaces / EC2 Instance Connect)" section with the
  ``--manual-paste`` recipe, plus a TL;DR note for the new flag.
* ``xai-grok-oauth.md`` — short subsection pointing at the same
  recipe and the OAuth-over-SSH guide anchor.
@alt-glitch alt-glitch added type/feature New feature or request P2 Medium — degraded but workaround exists comp/cli CLI entry point, hermes_cli/, setup wizard area/auth Authentication, OAuth, credential pools labels May 16, 2026
@cardtest15-coder

This comment was marked as spam.

@welliv

welliv commented May 18, 2026

Copy link
Copy Markdown

Good job!

@teknium1

Copy link
Copy Markdown
Contributor

Merged via PR #28358 — cherry-picked your three commits onto current main with authorship preserved (rebase-merge). 157/157 auth + manual-paste tests passing, 14/14 argparse propagation tests passing. Thanks for the broad remote-env detection (Cloud Shell / Codespaces / Gitpod / Replit / StackBlitz) — that's the right shape and is what unblocks the WSL2 firewall users too.

#28358

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/auth Authentication, OAuth, credential pools comp/cli CLI entry point, hermes_cli/, setup wizard P2 Medium — degraded but workaround exists type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OAuth loopback login broken for remote/browser-based consoles (GCP, Codespaces, etc.)

5 participants