mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-22 05:22:09 +00:00
fix(auth): point SSH OAuth users at the tunnel they actually need (#26592)
Two loopback-redirect OAuth flows (xAI Grok, Spotify) silently fail when Hermes runs on a remote host: the auth server redirects to 127.0.0.1:<port> on the user's laptop, not on the remote box. The --no-browser flag only suppresses webbrowser.open() — it doesn't change the bind address. Symptom xAI surfaces is 'Could not establish connection. We couldn't reach your app.', followed by a 'xAI authorization timed out waiting for the local callback' on the CLI side. Changes - hermes_cli/auth.py: new _print_loopback_ssh_hint() helper, called from _xai_oauth_loopback_login() and _spotify_login() right after they print the redirect URI. Silent off SSH; on SSH prints the exact 'ssh -N -L <port>:127.0.0.1:<port>' command using the actually-bound port (not the hardcoded constant — the listener auto-bumps when the preferred port is busy), a provider-specific docs URL, and a link to the new shared guide. - website/docs/guides/oauth-over-ssh.md (new): single source of truth for the tunnel pattern — TL;DR command, jump-box / ProxyJump variant, mosh+tmux+ControlMaster gotchas, troubleshooting. - website/docs/guides/xai-grok-oauth.md: fix the two sections that claimed --no-browser alone was enough; link to the shared guide. - website/docs/user-guide/features/spotify.md: expand the existing one-liner; link to the shared guide. - website/sidebars.ts: register the new page. - tests/hermes_cli/test_auth_loopback_ssh_hint.py: 7 unit tests covering SSH-vs-not, loopback-vs-not, malformed URIs, port echo, with and without provider docs URL.
This commit is contained in:
parent
9e67c8e8be
commit
3b9368a0c4
6 changed files with 304 additions and 6 deletions
|
|
@ -107,6 +107,9 @@ DEFAULT_SPOTIFY_REDIRECT_URI = "http://127.0.0.1:43827/spotify/callback"
|
|||
SPOTIFY_DOCS_URL = "https://hermes-agent.nousresearch.com/docs/user-guide/features/spotify"
|
||||
SPOTIFY_DASHBOARD_URL = "https://developer.spotify.com/dashboard"
|
||||
SPOTIFY_ACCESS_TOKEN_REFRESH_SKEW_SECONDS = 120
|
||||
|
||||
XAI_OAUTH_DOCS_URL = "https://hermes-agent.nousresearch.com/docs/guides/xai-grok-oauth"
|
||||
OAUTH_OVER_SSH_DOCS_URL = "https://hermes-agent.nousresearch.com/docs/guides/oauth-over-ssh"
|
||||
DEFAULT_SPOTIFY_SCOPE = " ".join((
|
||||
"user-modify-playback-state",
|
||||
"user-read-playback-state",
|
||||
|
|
@ -2528,6 +2531,8 @@ def login_spotify_command(args) -> None:
|
|||
print(f"Full setup guide: {SPOTIFY_DOCS_URL}")
|
||||
print()
|
||||
|
||||
_print_loopback_ssh_hint(redirect_uri, docs_url=SPOTIFY_DOCS_URL)
|
||||
|
||||
if open_browser and not _is_remote_session():
|
||||
try:
|
||||
opened = webbrowser.open(authorize_url)
|
||||
|
|
@ -2584,6 +2589,45 @@ def _is_remote_session() -> bool:
|
|||
return bool(os.getenv("SSH_CLIENT") or os.getenv("SSH_TTY"))
|
||||
|
||||
|
||||
def _print_loopback_ssh_hint(redirect_uri: str, *, docs_url: str | None = None) -> None:
|
||||
"""Print an SSH tunnel hint when running a loopback-redirect OAuth flow on a
|
||||
remote host. The auth server (xAI, Spotify, ...) will redirect the user's
|
||||
browser to ``127.0.0.1:<port>/callback``. If the browser is on a different
|
||||
machine than the loopback listener (the usual SSH case), the redirect can't
|
||||
reach the listener without a local port forward.
|
||||
|
||||
The hint is best-effort: silent if we don't think we're remote, or if we
|
||||
can't parse a host/port out of the redirect URI.
|
||||
|
||||
Pass ``docs_url`` for a provider-specific guide (e.g. the xAI Grok OAuth
|
||||
page); the generic OAuth-over-SSH guide is always shown after it.
|
||||
"""
|
||||
if not _is_remote_session():
|
||||
return
|
||||
try:
|
||||
parsed = urlparse(redirect_uri)
|
||||
except Exception:
|
||||
return
|
||||
host = parsed.hostname or ""
|
||||
port = parsed.port
|
||||
if host not in ("127.0.0.1", "::1", "localhost") or not port:
|
||||
return
|
||||
print()
|
||||
print("Remote session detected. Your browser will redirect to")
|
||||
print(f" {redirect_uri}")
|
||||
print("which the loopback listener on THIS machine is waiting on. If your")
|
||||
print("browser is on a different machine, forward the port first from your")
|
||||
print("local machine in a separate terminal:")
|
||||
print()
|
||||
print(f" ssh -N -L {port}:127.0.0.1:{port} <user>@<this-host>")
|
||||
print()
|
||||
print("Then open the authorize URL above in your local browser.")
|
||||
if docs_url:
|
||||
print(f"Provider docs: {docs_url}")
|
||||
print(f"SSH/jump-box guide: {OAUTH_OVER_SSH_DOCS_URL}")
|
||||
print()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# OpenAI Codex auth — tokens stored in ~/.hermes/auth.json (not ~/.codex/)
|
||||
#
|
||||
|
|
@ -5297,6 +5341,8 @@ def _xai_oauth_loopback_login(
|
|||
print()
|
||||
print(f"Waiting for callback on {redirect_uri}")
|
||||
|
||||
_print_loopback_ssh_hint(redirect_uri, docs_url=XAI_OAUTH_DOCS_URL)
|
||||
|
||||
if open_browser and not _is_remote_session():
|
||||
try:
|
||||
opened = webbrowser.open(authorize_url)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue