mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-01 07:01:41 +00:00
Follow-up to #32053. The OAuth-over-SSH guide and the MCP feature page previously only covered xAI and Spotify. Now that MCP servers can complete OAuth via stdin paste-back on remote/headless hosts, document it. oauth-over-ssh.md: - Add MCP servers to the 'Which Providers Need This' table. - New 'MCP Servers' section covering: paste-back (no setup, works anywhere), SSH port forward (same pattern as xAI/Spotify), and the 30s config-auto-reload race pitfall (use 'hermes mcp login <server>' from a fresh terminal instead of editing config from inside a running session). mcp.md: - New 'OAuth-authenticated HTTP servers' section under HTTP servers, covering auth: oauth config, token cache path, paste-back vs SSH tunnel for headless hosts, and the same reload-race pitfall. - Cross-links to the OAuth-over-SSH guide anchor.
187 lines
10 KiB
Markdown
187 lines
10 KiB
Markdown
---
|
|
sidebar_position: 17
|
|
title: "OAuth over SSH / Remote Hosts"
|
|
description: "How to complete browser-based OAuth (xAI, Spotify, MCP servers) when Hermes runs on a remote machine, container, or behind a jump box"
|
|
---
|
|
|
|
# OAuth over SSH / Remote Hosts
|
|
|
|
Some Hermes providers — **xAI Grok OAuth**, **Spotify**, and **remote MCP servers** (Linear, Sentry, Atlassian, Asana, Figma, …) — use a *loopback redirect* OAuth flow. The auth server redirects your browser to `http://127.0.0.1:<port>/callback` so a tiny HTTP listener started by Hermes can grab the authorization code.
|
|
|
|
This works perfectly when Hermes and your browser are on the same machine. It breaks the moment they aren't: your laptop's browser tries to reach `127.0.0.1` on **your laptop**, but the listener is bound to `127.0.0.1` on **the remote server**.
|
|
|
|
The fix is a one-line SSH local-forward — **or**, when you don't have a real SSH client (GCP Cloud Shell, GitHub Codespaces, EC2 Instance Connect, Gitpod, browser-based web IDEs), the new `--manual-paste` flag introduced in [#26923](https://github.com/NousResearch/hermes-agent/issues/26923).
|
|
|
|
## TL;DR
|
|
|
|
```bash
|
|
# On your local machine (laptop), in a separate terminal:
|
|
ssh -N -L 56121:127.0.0.1:56121 user@remote-host
|
|
|
|
# In your existing SSH session on the remote machine:
|
|
hermes auth add xai-oauth --no-browser
|
|
# → Hermes prints an authorize URL. Open it in a browser on your laptop.
|
|
# → Your browser redirects to 127.0.0.1:56121/callback, the tunnel forwards
|
|
# the request to the remote listener, login completes.
|
|
```
|
|
|
|
Port `56121` is what xAI OAuth uses. For Spotify, replace it with `43827`. Hermes prints the exact port it bound to on the `Waiting for callback on ...` line — copy it from there.
|
|
|
|
## Browser-only remote (Cloud Shell / Codespaces / EC2 Instance Connect)
|
|
|
|
If you don't have a regular SSH client — for example because you're running Hermes inside GCP Cloud Shell, GitHub Codespaces, AWS EC2 Instance Connect, Gitpod, or another browser-based console — the SSH tunnel above isn't available. Use `--manual-paste` instead:
|
|
|
|
```bash
|
|
hermes auth add xai-oauth --manual-paste
|
|
# → Hermes prints an authorize URL. Open it in a browser on your laptop.
|
|
# → Approve in the browser. The redirect to 127.0.0.1:56121/callback fails
|
|
# to load — that's expected.
|
|
# → Copy the FULL URL from the failed page's address bar.
|
|
# → Paste it back into the terminal at the "Callback URL:" prompt.
|
|
```
|
|
|
|
The same flag works on `hermes model --manual-paste` for the integrated model picker. A bare `?code=...&state=...` query fragment is accepted too if you don't want to paste the whole URL.
|
|
|
|
Hermes uses the **same PKCE verifier, state and nonce** for both paths, so the upstream OAuth flow is byte-identical — `--manual-paste` is purely a transport change for the callback hop and is not a security downgrade.
|
|
|
|
## Which Providers Need This
|
|
|
|
| Provider | Loopback port | Tunnel needed? |
|
|
|----------|---------------|----------------|
|
|
| `xai-oauth` (Grok SuperGrok) | `56121` | Yes, when Hermes is remote |
|
|
| Spotify | `43827` | Yes, when Hermes is remote |
|
|
| MCP servers (`auth: oauth`) | auto-picked per server | Yes, when Hermes is remote |
|
|
| `anthropic` (Claude Pro/Max) | n/a | No — paste-the-code flow |
|
|
| `openai-codex` (ChatGPT Plus/Pro) | n/a | No — device code flow |
|
|
| `minimax`, `nous-portal` | n/a | No — device code flow |
|
|
|
|
If your provider isn't in the table, you don't need a tunnel.
|
|
|
|
## MCP Servers
|
|
|
|
Remote MCP servers (Linear, Sentry, Atlassian, Asana, Figma, etc.) use the same loopback redirect flow. Hermes auto-picks a free port per server and prints the authorize URL when the OAuth flow kicks off — either at startup (when a new server appears in `mcp_servers:`) or when you run `hermes mcp login <server>`.
|
|
|
|
You have two ways to complete it from a remote host:
|
|
|
|
**Option 1 — paste the redirect URL back (no setup, works anywhere).** On an interactive terminal, Hermes prompts you to paste the redirect URL alongside running the local listener. After approving in your browser, the redirect to `http://127.0.0.1:<port>/callback` will show a connection error — that's expected. Copy the **full URL from the browser's address bar** and paste it at the Hermes prompt:
|
|
|
|
```
|
|
MCP OAuth: authorization required.
|
|
Open this URL in your browser:
|
|
|
|
https://mcp.linear.app/authorize?response_type=code&...
|
|
|
|
Or paste the redirect URL here (or the ?code=...&state=... portion) and press Enter:
|
|
> https://mcp.linear.app/callback?code=abc123&state=xyz
|
|
Got authorization code from paste — completing flow.
|
|
```
|
|
|
|
A bare `?code=...&state=...` query string is accepted too. This works for any MCP server with `auth: oauth` and requires no SSH config changes.
|
|
|
|
**Option 2 — SSH port forward (same as xAI / Spotify).** Hermes prints the exact port it bound to in the SSH-session hint. Open a separate terminal on your laptop:
|
|
|
|
```bash
|
|
ssh -N -L <port>:127.0.0.1:<port> user@remote-host
|
|
```
|
|
|
|
Then open the authorize URL in your browser as normal; the redirect tunnels through and the listener picks it up. Use this when you need the flow to complete unattended (e.g. scripted re-auth where you can't paste interactively).
|
|
|
|
**Pitfall — the 30s config-reload race.** If you edit `~/.hermes/config.yaml` to add an OAuth MCP server from inside a running Hermes session, the CLI auto-reloads MCP connections with a 30s timeout. That's not enough time to complete an interactive OAuth flow, and the reload will give up. Use `hermes mcp login <server>` from a fresh terminal instead — it has no such cap and waits the full 5 min for you to paste back.
|
|
|
|
## Why the listener can't just bind 0.0.0.0
|
|
|
|
xAI and Spotify both validate the `redirect_uri` parameter against an allowlist. Both require the loopback form (`http://127.0.0.1:<exact-port>/callback`). Binding the listener to `0.0.0.0` or a different port would cause the auth server to reject the request as a redirect_uri mismatch. The SSH tunnel keeps the loopback URI intact end-to-end.
|
|
|
|
## Step-by-step: single SSH hop
|
|
|
|
### 1. Start the tunnel from your local machine
|
|
|
|
```bash
|
|
# xAI Grok OAuth (port 56121)
|
|
ssh -N -L 56121:127.0.0.1:56121 user@remote-host
|
|
|
|
# Or for Spotify (port 43827)
|
|
ssh -N -L 43827:127.0.0.1:43827 user@remote-host
|
|
```
|
|
|
|
`-N` means "don't open a remote shell, just hold the tunnel open." Keep this terminal running for the duration of the login.
|
|
|
|
### 2. In a separate SSH session, run the auth command
|
|
|
|
```bash
|
|
ssh user@remote-host
|
|
hermes auth add xai-oauth --no-browser
|
|
# or for Spotify:
|
|
# hermes auth add spotify --no-browser
|
|
```
|
|
|
|
Hermes detects the SSH session, skips the browser auto-open, and prints an authorize URL plus a `Waiting for callback on http://127.0.0.1:<port>/callback` line.
|
|
|
|
### 3. Open the URL in your local browser
|
|
|
|
Copy the authorize URL from the remote terminal and paste it into the browser on your laptop. Approve the consent screen. The auth server redirects to `http://127.0.0.1:<port>/callback`. Your browser hits the tunnel, the request is forwarded to the remote listener, and Hermes prints `Login successful!`.
|
|
|
|
You can tear down the tunnel (Ctrl+C in the first terminal) once you see the success line.
|
|
|
|
## Step-by-step: through a jump box
|
|
|
|
If you reach Hermes through a bastion / jump host, use SSH's built-in `-J` (ProxyJump):
|
|
|
|
```bash
|
|
ssh -N -L 56121:127.0.0.1:56121 -J jump-user@jump-host user@final-host
|
|
```
|
|
|
|
This chains a SSH connection through the jump host without putting the loopback port on the jump box itself. The local `127.0.0.1:56121` on your laptop tunnels straight through to `127.0.0.1:56121` on the final remote host.
|
|
|
|
For older OpenSSH that doesn't support `-J`, the long form is:
|
|
|
|
```bash
|
|
ssh -N \
|
|
-o "ProxyCommand=ssh -W %h:%p jump-user@jump-host" \
|
|
-L 56121:127.0.0.1:56121 \
|
|
user@final-host
|
|
```
|
|
|
|
## Mosh, tmux, ssh ControlMaster
|
|
|
|
The tunnel is a property of the underlying SSH connection. If you're running Hermes inside `tmux` over a mosh session, the mosh roaming doesn't carry the `-L` forwarding. Open a *separate* plain SSH session **only** for the `-L` tunnel — that's the connection that has to stay alive during the auth flow. Your interactive mosh/tmux session can keep running Hermes normally.
|
|
|
|
If you use `ssh -o ControlMaster=auto`, port forwards on a multiplexed connection share the master's lifetime. Restart the master if the tunnel doesn't come up:
|
|
|
|
```bash
|
|
ssh -O exit user@remote-host
|
|
ssh -N -L 56121:127.0.0.1:56121 user@remote-host
|
|
```
|
|
|
|
## Troubleshooting
|
|
|
|
### `bind [127.0.0.1]:56121: Address already in use`
|
|
|
|
Something on your laptop is already using that port. Either the previous tunnel didn't shut down cleanly, or a local Hermes is also listening on it. Find and kill the offender:
|
|
|
|
```bash
|
|
# macOS / Linux
|
|
lsof -iTCP:56121 -sTCP:LISTEN
|
|
kill <PID>
|
|
```
|
|
|
|
Then retry the `ssh -L` command.
|
|
|
|
### "Could not establish connection. We couldn't reach your app." (xAI)
|
|
|
|
xAI's authorize page shows this when its redirect to `127.0.0.1:<port>/callback` doesn't reach a listener. Either the tunnel isn't running, the port is wrong, or you're using the port Hermes printed in a previous run (the port can be auto-bumped if the preferred one is busy — always read the latest `Waiting for callback on ...` line).
|
|
|
|
### `xAI authorization timed out waiting for the local callback`
|
|
|
|
Same root cause as above — the redirect never made it back. Check the tunnel is still alive (`ssh -N` doesn't show output, so look at the terminal you started it from), restart it if needed, and re-run `hermes auth add xai-oauth --no-browser`.
|
|
|
|
### Tokens land in the wrong `~/.hermes`
|
|
|
|
The tokens are written under the Linux user that ran `hermes auth add ...`. If your gateway / systemd service runs as a different user (e.g. `root` or a dedicated `hermes` user), authenticate as **that** user so the tokens land in their `~/.hermes/auth.json`. `sudo -u hermes -i` or equivalent.
|
|
|
|
## See Also
|
|
|
|
- [xAI Grok OAuth](./xai-grok-oauth.md)
|
|
- [Spotify (`Running over SSH`)](../user-guide/features/spotify.md#running-over-ssh--in-a-headless-environment)
|
|
- [Native MCP client (OAuth section)](../user-guide/features/mcp.md#oauth-authenticated-http-servers)
|
|
- [SSH `-J` / ProxyJump (man page)](https://man.openbsd.org/ssh#J)
|