mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-29 06:31:32 +00:00
fix(mcp-oauth): print SSH tunnel hint in _redirect_handler
When Hermes runs on a remote host over SSH, MCP OAuth loopback flows silently fail: the OAuth provider redirects the user's browser to http://127.0.0.1:<port>/callback, which reaches the callback server on the *remote* machine — not the local machine where the browser is running. _redirect_handler already detected SSH (via _can_open_browser) and printed "Headless environment detected — open the URL manually." but gave no guidance on how to actually reach the callback server. Users got silent timeouts or "Could not establish connection" errors. This is the same bug fixed for xAI-oauth and Spotify in #26592, which added _print_loopback_ssh_hint() in hermes_cli/auth.py. mcp_oauth.py uses the identical loopback callback pattern (http://127.0.0.1:<port>/callback via _configure_callback_port / _wait_for_callback) but was missing the hint. Fix: when SSH_CLIENT or SSH_TTY is set and _oauth_port is available, print the ssh -N -L port-forward command and the OAuth-over-SSH guide URL to stderr, consistent with the rest of _redirect_handler's output. Tests: 4 new cases in TestRedirectHandlerSshHint covering SSH_CLIENT, SSH_TTY, local session (no hint), and missing _oauth_port (no hint).
This commit is contained in:
parent
cc59880ab0
commit
ad00777f04
2 changed files with 78 additions and 0 deletions
|
|
@ -10,6 +10,8 @@ from unittest.mock import patch, MagicMock, AsyncMock
|
|||
|
||||
import pytest
|
||||
|
||||
import asyncio
|
||||
|
||||
from tools.mcp_oauth import (
|
||||
HermesTokenStorage,
|
||||
OAuthNonInteractiveError,
|
||||
|
|
@ -20,6 +22,7 @@ from tools.mcp_oauth import (
|
|||
_is_interactive,
|
||||
_wait_for_callback,
|
||||
_make_callback_handler,
|
||||
_redirect_handler,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -241,6 +244,64 @@ class TestUtilities:
|
|||
assert _can_open_browser() is True
|
||||
|
||||
|
||||
class TestRedirectHandlerSshHint:
|
||||
"""_redirect_handler must print an SSH tunnel hint on remote sessions."""
|
||||
|
||||
def _run(self, coro):
|
||||
return asyncio.get_event_loop().run_until_complete(coro)
|
||||
|
||||
def test_ssh_hint_shown_on_ssh_session(self, monkeypatch, capsys):
|
||||
import tools.mcp_oauth as mco
|
||||
monkeypatch.setattr(mco, "_oauth_port", 49200)
|
||||
monkeypatch.setenv("SSH_CLIENT", "1.2.3.4 1234 22")
|
||||
monkeypatch.delenv("SSH_TTY", raising=False)
|
||||
monkeypatch.setattr(mco, "_can_open_browser", lambda: False)
|
||||
|
||||
self._run(_redirect_handler("https://example.com/auth?foo=bar"))
|
||||
|
||||
err = capsys.readouterr().err
|
||||
assert "49200" in err
|
||||
assert "ssh -N -L" in err
|
||||
assert "Remote session detected" in err
|
||||
|
||||
def test_ssh_hint_shown_via_ssh_tty(self, monkeypatch, capsys):
|
||||
import tools.mcp_oauth as mco
|
||||
monkeypatch.setattr(mco, "_oauth_port", 49201)
|
||||
monkeypatch.delenv("SSH_CLIENT", raising=False)
|
||||
monkeypatch.setenv("SSH_TTY", "/dev/pts/1")
|
||||
monkeypatch.setattr(mco, "_can_open_browser", lambda: False)
|
||||
|
||||
self._run(_redirect_handler("https://example.com/auth"))
|
||||
|
||||
err = capsys.readouterr().err
|
||||
assert "49201" in err
|
||||
assert "ssh -N -L" in err
|
||||
|
||||
def test_no_ssh_hint_on_local_session(self, monkeypatch, capsys):
|
||||
import tools.mcp_oauth as mco
|
||||
monkeypatch.setattr(mco, "_oauth_port", 49202)
|
||||
monkeypatch.delenv("SSH_CLIENT", raising=False)
|
||||
monkeypatch.delenv("SSH_TTY", raising=False)
|
||||
monkeypatch.setattr(mco, "_can_open_browser", lambda: True)
|
||||
monkeypatch.setattr("webbrowser.open", lambda url, **kw: True)
|
||||
|
||||
self._run(_redirect_handler("https://example.com/auth"))
|
||||
|
||||
err = capsys.readouterr().err
|
||||
assert "ssh -N -L" not in err
|
||||
|
||||
def test_no_ssh_hint_when_port_not_set(self, monkeypatch, capsys):
|
||||
import tools.mcp_oauth as mco
|
||||
monkeypatch.setattr(mco, "_oauth_port", None)
|
||||
monkeypatch.setenv("SSH_CLIENT", "1.2.3.4 1234 22")
|
||||
monkeypatch.setattr(mco, "_can_open_browser", lambda: False)
|
||||
|
||||
self._run(_redirect_handler("https://example.com/auth"))
|
||||
|
||||
err = capsys.readouterr().err
|
||||
assert "ssh -N -L" not in err
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Path traversal protection
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -401,6 +401,23 @@ async def _redirect_handler(authorization_url: str) -> None:
|
|||
)
|
||||
print(msg, file=sys.stderr)
|
||||
|
||||
# On a remote SSH session the OAuth provider redirects to
|
||||
# http://127.0.0.1:<port>/callback, which reaches the callback server on
|
||||
# the *remote* machine — not the user's local machine where the browser
|
||||
# opened. Print a port-forward hint so the user knows to tunnel first.
|
||||
if _oauth_port and (os.getenv("SSH_CLIENT") or os.getenv("SSH_TTY")):
|
||||
print(
|
||||
f" Remote session detected. The OAuth provider will redirect your browser to\n"
|
||||
f" http://127.0.0.1:{_oauth_port}/callback\n"
|
||||
f" which the callback listener on THIS machine is waiting on. If your browser\n"
|
||||
f" is on a different machine, forward the port first in a separate terminal:\n"
|
||||
f"\n"
|
||||
f" ssh -N -L {_oauth_port}:127.0.0.1:{_oauth_port} <user>@<this-host>\n"
|
||||
f"\n"
|
||||
f" Then open the URL above. See: https://hermes-agent.nousresearch.com/docs/guides/oauth-over-ssh\n",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
if _can_open_browser():
|
||||
try:
|
||||
opened = webbrowser.open(authorization_url)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue