feat(dashboard): always enable embedded chat; remove dashboard --tui flag

The dashboard's embedded Chat surface (/chat, /api/ws, /api/pty) was gated
behind `hermes dashboard --tui` / HERMES_DASHBOARD_TUI=1. The desktop app and
the dashboard's own Chat tab both drive the agent over the /api/ws + /api/pty
WebSockets, so a dashboard started without the flag would pass the /api/status
health check but slam the chat WebSocket shut with WS code 4403 — the app
connects, reports "ready", and chat stays dead. This was the root cause behind
multiple user reports of the desktop app failing to connect to a self-hosted
gateway/dashboard, and it bit Docker and host installs alike.

Make the embedded chat unconditional:

- web_server.py: _DASHBOARD_EMBEDDED_CHAT_ENABLED defaults to True; drop the
  embedded_chat parameter and the runtime reassignment from start_server().
  The WS gates still read the constant (now always true) so the seam — and its
  "rejects when disabled" contract test — stays meaningful.
- main.py: remove the `--tui` argument from the dashboard subparser and the
  `embedded_chat = args.tui or HERMES_DASHBOARD_TUI==1` derivation.
- web/: isDashboardEmbeddedChatEnabled() returns true unconditionally; drop the
  deprecated __HERMES_DASHBOARD_TUI__ alias and the dead LEGACY_TUI_RE scrape in
  the vite dev-token plugin.
- apps/desktop/electron/main.cjs: drop `--tui` from the spawned dashboardArgs
  (it would now error with "unrecognized arguments: --tui") and the redundant
  HERMES_DASHBOARD_TUI env injection.
- Docker: no s6 run-script change needed — the script never passed --tui; the
  HERMES_DASHBOARD_TUI env var is now simply a no-op, so the image works out of
  the box with no extra var.
- Docs: remove every dashboard --tui / HERMES_DASHBOARD_TUI reference across the
  CLI reference, env-var reference, docker/desktop/web-dashboard guides, in-app
  tips, and the zh-Hans translations. The terminal `hermes --tui` / HERMES_TUI
  references are intentionally left untouched.

Tests: 270 passing across web_server, dashboard lifecycle, host-header,
auth-gate, and docker-override-scripts suites.
This commit is contained in:
Ben
2026-06-04 10:53:49 +10:00
committed by Teknium
parent bf82a7f1cc
commit cae6b5486f
18 changed files with 43 additions and 80 deletions

View File

@ -94,7 +94,7 @@ Installers are built and uploaded to GitHub Releases manually. macOS/Windows sig
### How it works
The packaged app ships only the Electron shell. On first launch it installs the Hermes Agent runtime into `HERMES_HOME` (`~/.hermes`, or `%LOCALAPPDATA%\hermes` on Windows) — the **same layout a CLI install uses**, so the two are interchangeable. The renderer (React, in `src/`) talks to a `hermes dashboard --tui` backend over the standard gateway APIs and reuses the embedded TUI rather than reimplementing chat. The install, backend-resolution, and self-update logic all live in `electron/main.cjs`.
The packaged app ships only the Electron shell. On first launch it installs the Hermes Agent runtime into `HERMES_HOME` (`~/.hermes`, or `%LOCALAPPDATA%\hermes` on Windows) — the **same layout a CLI install uses**, so the two are interchangeable. The renderer (React, in `src/`) talks to a `hermes dashboard` backend over the standard gateway APIs and reuses the embedded TUI rather than reimplementing chat. The install, backend-resolution, and self-update logic all live in `electron/main.cjs`.
### Verification

View File

@ -3747,7 +3747,7 @@ async function startHermes() {
await advanceBootProgress('backend.port', 'Finding an open local port', 16)
const port = await pickPort()
const token = crypto.randomBytes(32).toString('base64url')
const dashboardArgs = ['dashboard', '--no-open', '--tui', '--host', '127.0.0.1', '--port', String(port)]
const dashboardArgs = ['dashboard', '--no-open', '--host', '127.0.0.1', '--port', String(port)]
await advanceBootProgress('backend.runtime', 'Resolving Hermes runtime', 28)
const backend = await ensureRuntime(resolveHermesBackend(dashboardArgs))
const hermesCwd = resolveHermesCwd()
@ -3771,7 +3771,6 @@ async function startHermes() {
HERMES_HOME,
...backend.env,
HERMES_DASHBOARD_SESSION_TOKEN: token,
HERMES_DASHBOARD_TUI: '1',
HERMES_WEB_DIST: webDist
},
shell: backend.shell,

View File

@ -12022,13 +12022,14 @@ def cmd_dashboard(args):
from hermes_cli.web_server import start_server
embedded_chat = args.tui or os.environ.get("HERMES_DASHBOARD_TUI") == "1"
# The in-browser Chat tab (the embedded TUI over PTY/WebSocket) is always
# available — the desktop app and the dashboard's own Chat tab both rely on
# the `/api/ws` + `/api/pty` sockets, so there is no reason to gate them.
start_server(
host=args.host,
port=args.port,
open_browser=not args.no_open,
allow_public=getattr(args, "insecure", False),
embedded_chat=embedded_chat,
)
@ -15314,14 +15315,6 @@ Examples:
action="store_true",
help="Allow binding to non-localhost (DANGEROUS: exposes API keys on the network)",
)
dashboard_parser.add_argument(
"--tui",
action="store_true",
help=(
"Expose the in-browser Chat tab (embedded `hermes --tui` via PTY/WebSocket). "
"Alternatively set HERMES_DASHBOARD_TUI=1."
),
)
dashboard_parser.add_argument(
"--skip-build",
action="store_true",

View File

@ -381,7 +381,7 @@ TIPS = [
'Ctrl+G or Ctrl+X Ctrl+E in the TUI opens the input buffer in $EDITOR for long multi-line prompts.',
'The TUI renders LaTeX inline — $E=mc^2$ becomes Unicode math instead of raw TeX.',
'hermes dashboard launches a local web UI at 127.0.0.1:9119 — zero data leaves localhost.',
'hermes dashboard --tui embeds the full Hermes TUI in your browser via xterm.js and a WebSocket PTY.',
'hermes dashboard embeds the full Hermes TUI in your browser via xterm.js and a WebSocket PTY.',
'Drop a YAML in ~/.hermes/dashboard-themes/ with two palette colors to reskin the entire dashboard.',
'Dashboard plugins are drop-in: manifest.json + JS bundle in ~/.hermes/dashboard-plugins/ — no npm build required.',
'layoutVariant: cockpit in a dashboard theme adds a 260px left rail that plugins can populate via the sidebar slot.',

View File

@ -135,9 +135,13 @@ app = FastAPI(title="Hermes Agent", version=__version__, lifespan=_lifespan)
_SESSION_TOKEN = os.environ.get("HERMES_DASHBOARD_SESSION_TOKEN") or secrets.token_urlsafe(32)
_SESSION_HEADER_NAME = "X-Hermes-Session-Token"
# In-browser Chat tab (/chat, /api/pty, …). Off unless ``hermes dashboard --tui``
# or HERMES_DASHBOARD_TUI=1. Set from :func:`start_server`.
_DASHBOARD_EMBEDDED_CHAT_ENABLED = False
# In-browser Chat tab (/chat, /api/pty, /api/ws, …). Always enabled: the
# desktop app and the dashboard's own Chat tab both drive the agent over the
# `/api/ws` + `/api/pty` WebSockets, so the embedded-chat surface is an
# unconditional part of the dashboard. Kept as a module-level constant (rather
# than inlining ``True`` at every gate) so the WS endpoints and the SPA token
# injection share a single, testable seam.
_DASHBOARD_EMBEDDED_CHAT_ENABLED = True
# Simple rate limiter for the reveal endpoint
_reveal_timestamps: List[float] = []
@ -8677,15 +8681,10 @@ def start_server(
port: int = 9119,
open_browser: bool = True,
allow_public: bool = False,
*,
embedded_chat: bool = False,
):
"""Start the web UI server."""
import uvicorn
global _DASHBOARD_EMBEDDED_CHAT_ENABLED
_DASHBOARD_EMBEDDED_CHAT_ENABLED = embedded_chat
# Phase 0: stash the auth-gate flag on app.state so middleware / SPA-token
# injection / WS-auth paths can branch on it consistently. Phase 3.5
# uses this to decide whether to refuse the bind, log the gate-on

View File

@ -389,7 +389,7 @@ def test_dashboard_insecure_env_var_opts_out_of_gate(
"""``HERMES_DASHBOARD_INSECURE=1`` re-enables the legacy no-gate mode
for operators running on trusted LANs behind a reverse proxy without
the OAuth contract. Same opt-out shape as the rest of the s6 boolean
envs (``HERMES_DASHBOARD``, ``HERMES_DASHBOARD_TUI``).
envs (e.g. ``HERMES_DASHBOARD``).
With the gate off, ``/api/status`` (a public endpoint under the
legacy ``_SESSION_TOKEN`` middleware) returns 200 with the

View File

@ -22,7 +22,7 @@ def _ns(**kw):
"""Build an argparse.Namespace with dashboard defaults plus overrides."""
defaults = dict(
port=9119, host="127.0.0.1", no_open=False, insecure=False,
tui=False, stop=False, status=False,
stop=False, status=False,
)
defaults.update(kw)
return argparse.Namespace(**defaults)

View File

@ -72,7 +72,7 @@ def test_dashboard_run_does_not_derive_insecure_from_bind_host() -> None:
"Explicit HERMES_DASHBOARD_INSECURE opt-in is missing."
)
# Truthy values aligned with the rest of the s6 scripts
# (HERMES_DASHBOARD, HERMES_DASHBOARD_TUI).
# (e.g. HERMES_DASHBOARD).
for truthy in ("1", "true", "TRUE", "True", "yes", "YES", "Yes"):
assert truthy in text, (
f"HERMES_DASHBOARD_INSECURE should accept truthy value {truthy!r}"

View File

@ -1,15 +1,24 @@
declare global {
interface Window {
/** Set true by the server only for `hermes dashboard --tui` (or HERMES_DASHBOARD_TUI=1). */
/**
* Injected by the server as `true`. The embedded TUI Chat surface
* (`/chat`, `/api/ws`, `/api/pty`) is always enabled, so this is
* effectively a constant; kept on `window` for any consumer that reads
* it directly and for parity with the server's bootstrap script.
*/
__HERMES_DASHBOARD_EMBEDDED_CHAT__?: boolean;
/** @deprecated Older injected name; treated as on when true. */
__HERMES_DASHBOARD_TUI__?: boolean;
}
}
/** True only when the dashboard was started with embedded TUI Chat (`hermes dashboard --tui`). */
/**
* Whether the dashboard's embedded TUI Chat surface is available.
*
* The embedded chat (`/chat` tab, `/api/ws` + `/api/pty` WebSockets) is now
* an unconditional part of the dashboard — the desktop app and the in-browser
* Chat tab both depend on it — so this always returns `true`. The function is
* retained as a stable seam so call sites don't need to change if the surface
* ever becomes conditional again.
*/
export function isDashboardEmbeddedChatEnabled(): boolean {
if (typeof window === "undefined") return false;
if (window.__HERMES_DASHBOARD_EMBEDDED_CHAT__ === true) return true;
return window.__HERMES_DASHBOARD_TUI__ === true;
return true;
}

View File

@ -19,8 +19,6 @@ function hermesDevToken(): Plugin {
const TOKEN_RE = /window\.__HERMES_SESSION_TOKEN__\s*=\s*"([^"]+)"/;
const EMBEDDED_RE =
/window\.__HERMES_DASHBOARD_EMBEDDED_CHAT__\s*=\s*(true|false)/;
const LEGACY_TUI_RE =
/window\.__HERMES_DASHBOARD_TUI__\s*=\s*(true|false)/;
return {
name: "hermes:dev-session-token",
@ -38,12 +36,7 @@ function hermesDevToken(): Plugin {
return;
}
const embeddedMatch = html.match(EMBEDDED_RE);
const legacyMatch = html.match(LEGACY_TUI_RE);
const embeddedJs = embeddedMatch
? embeddedMatch[1]
: legacyMatch
? legacyMatch[1]
: "false";
const embeddedJs = embeddedMatch ? embeddedMatch[1] : "true";
return [
{
tag: "script",

View File

@ -1342,14 +1342,13 @@ hermes claw migrate --source /home/user/old-openclaw
hermes dashboard [options]
```
Launch the web dashboard — a browser-based UI for managing configuration, API keys, and monitoring sessions. Requires `pip install hermes-agent[web]` (FastAPI + Uvicorn). The embedded browser Chat tab requires `--tui` plus the `pty` extra. See [Web Dashboard](/user-guide/features/web-dashboard) for full documentation.
Launch the web dashboard — a browser-based UI for managing configuration, API keys, and monitoring sessions. Requires `pip install hermes-agent[web]` (FastAPI + Uvicorn). The embedded browser Chat tab is always available and additionally needs the `pty` extra (`pip install 'hermes-agent[web,pty]'`) plus a POSIX PTY environment such as Linux, macOS, or WSL2. See [Web Dashboard](/user-guide/features/web-dashboard) for full documentation.
| Option | Default | Description |
|--------|---------|-------------|
| `--port` | `9119` | Port to run the web server on |
| `--host` | `127.0.0.1` | Bind address |
| `--no-open` | — | Don't auto-open the browser |
| `--tui` | off | Enable the in-browser Chat tab by running `hermes --tui` behind a PTY/WebSocket bridge. Requires `pip install 'hermes-agent[web,pty]'` and a POSIX PTY environment such as Linux, macOS, or WSL2. |
| `--insecure` | off | Allow binding to non-localhost hosts. Exposes dashboard credentials on the network; use only behind trusted network controls. |
| `--stop` | — | Stop running `hermes dashboard` processes and exit. |
| `--status` | — | List running `hermes dashboard` processes and exit. |
@ -1360,9 +1359,6 @@ hermes dashboard
# Custom port, no browser
hermes dashboard --port 8080 --no-open
# Enable the browser Chat tab
hermes dashboard --tui
```
## `hermes profile`

View File

@ -425,7 +425,6 @@ Auth for the [web dashboard](/user-guide/features/web-dashboard) and for connect
| `HERMES_DASHBOARD_SESSION_TOKEN` | Pins the dashboard session token instead of generating a random one per boot. Set this (e.g. `openssl rand -base64 32`) on the backend, then paste the same value into Hermes Desktop → Settings → Gateway → Remote gateway → Session token. Required for a stable remote desktop connection. |
| `HERMES_DESKTOP_REMOTE_URL` | (Desktop side) Base URL of the remote backend, e.g. `http://host:9119`. When set, overrides the in-app Gateway settings. Must be paired with `HERMES_DESKTOP_REMOTE_TOKEN`. |
| `HERMES_DESKTOP_REMOTE_TOKEN` | (Desktop side) The session token to authenticate with the remote backend — the same value as the backend's `HERMES_DASHBOARD_SESSION_TOKEN`. |
| `HERMES_DASHBOARD_TUI` | `1` exposes the in-browser Chat tab (embedded `hermes --tui`), same as the `--tui` flag. |
| `HERMES_DASHBOARD_OAUTH_CLIENT_ID` | OAuth client id (`agent:{instance_id}`) for the gated/public dashboard. Overrides `dashboard.oauth.client_id`. Provisioned by the Nous Portal for hosted deploys. |
| `HERMES_DASHBOARD_PORTAL_URL` | OAuth portal URL (default: `https://portal.nousresearch.com`). Override only for staging/custom deploys. |
| `HERMES_DASHBOARD_PUBLIC_URL` | Complete public URL the dashboard is reached at, for OAuth callback construction behind reverse proxies. Overrides `dashboard.public_url`. |

View File

@ -57,7 +57,7 @@ The center of the app. You get:
- **Drag-and-drop files** anywhere in the chat area to attach them to your next message.
- **A right-hand preview rail** — render web pages, files, and tool outputs side by side while you keep chatting.
Chatting against a Hermes instance on another machine instead of the bundled local backend? See [Connecting to a remote backend](#connecting-to-a-remote-backend) below — and for the full picture of how the remote-hosted dashboard connection works (the `/api/ws` chat socket, the `--tui` requirement, session-token pinning, and WebSocket close-code triage), see [Web Dashboard → Connecting Hermes Desktop to a remote backend](./features/web-dashboard.md#connecting-hermes-desktop-to-a-remote-backend).
Chatting against a Hermes instance on another machine instead of the bundled local backend? See [Connecting to a remote backend](#connecting-to-a-remote-backend) below — and for the full picture of how the remote-hosted dashboard connection works (the `/api/ws` chat socket, session-token pinning, and WebSocket close-code triage), see [Web Dashboard → Connecting Hermes Desktop to a remote backend](./features/web-dashboard.md#connecting-hermes-desktop-to-a-remote-backend).
### File browser
@ -104,7 +104,7 @@ To launch via the CLI, simply run `hermes desktop`. By default it installs works
## How it works
The packaged app ships only the Electron shell. On first launch it installs the Hermes Agent runtime into `HERMES_HOME` (`~/.hermes`, or `%LOCALAPPDATA%\hermes` on Windows) — **the same layout a CLI install uses**, which is why the two are interchangeable. The React renderer talks to a `hermes dashboard --tui` backend over the standard gateway APIs and reuses the agent rather than reimplementing it. Install, backend-resolution, and self-update logic live in the Electron main process.
The packaged app ships only the Electron shell. On first launch it installs the Hermes Agent runtime into `HERMES_HOME` (`~/.hermes`, or `%LOCALAPPDATA%\hermes` on Windows) — **the same layout a CLI install uses**, which is why the two are interchangeable. The React renderer talks to a `hermes dashboard` backend over the standard gateway APIs and reuses the agent rather than reimplementing it. Install, backend-resolution, and self-update logic live in the Electron main process.
## Connecting to a remote backend
@ -115,8 +115,6 @@ By default the app starts and manages its own **local** backend. You can instead
The session token is the part that trips people up. **Hermes does not print it for you to copy** — by default the backend mints a fresh random token on every boot and injects it straight into the served HTML, so there is nothing in `config.yaml`, in `/gateway`, or in the logs to grab. For a remote connection you pin the token yourself on the backend, then paste that same value into the app.
The backend also has to be started with **`--tui`** (or `HERMES_DASHBOARD_TUI=1`). The desktop's chat runs over the dashboard's `/api/ws` + `/api/pty` WebSockets, and those endpoints are refused unless the embedded-chat surface is enabled. Without `--tui` the connection still passes the `/api/status` health check and the app reports "Remote Hermes backend is ready" — but chat never works because the WebSocket is closed immediately. A plain `hermes dashboard` or `hermes gateway` is not enough.
### On the backend (the remote machine)
```bash
@ -129,12 +127,10 @@ chmod 600 ~/.hermes/.env
echo "$TOKEN" # copy this value into the desktop app
# 2. Run the dashboard bound to a reachable address.
# --tui enables the embedded chat (the /api/ws + /api/pty WebSockets the
# desktop drives) — without it the app connects but chat stays dead.
# --insecure is required for any non-loopback bind and keeps the legacy
# session-token auth path (a non-loopback bind WITHOUT --insecure engages
# the OAuth gate, which ignores the session token).
hermes dashboard --tui --no-open --insecure --host 0.0.0.0 --port 9119
hermes dashboard --no-open --insecure --host 0.0.0.0 --port 9119
```
Running the dashboard as a systemd service? Give the unit `EnvironmentFile=%h/.hermes/.env` so the token is in the environment at boot.
@ -157,7 +153,6 @@ The token is stored encrypted in the app's local config; leave the field blank o
### Troubleshooting
- **Test fails with 401** — the token doesn't match the backend's `HERMES_DASHBOARD_SESSION_TOKEN`, or the backend is bound non-loopback *without* `--insecure` (OAuth gate is on, ignoring the token). Verify with `curl -s -H "X-Hermes-Session-Token: $TOKEN" http://<host>:9119/api/status` — that should return JSON, not a 401.
- **App says "Remote Hermes backend is ready" but chat does nothing** — the backend was started without `--tui` (or `HERMES_DASHBOARD_TUI=1`). The status probe passes, but the chat WebSocket (`/api/ws` / `/api/pty`) is refused. Restart the backend with `--tui`.
- **Connection refused / times out** — the backend bound to `127.0.0.1` (the default) or a firewall/VPN is blocking the port. Bind to `0.0.0.0` or the tailscale IP and open the port to your trusted network.
- **No token to copy** — expected. You mint it yourself; Hermes never surfaces the default ephemeral one.

View File

@ -96,7 +96,6 @@ The dashboard is supervised by s6 — if it crashes, `s6-supervise` restarts it
| `HERMES_DASHBOARD` | Set to `1` (or `true` / `yes`) to enable the supervised dashboard service | *(unset — service is registered but stays down)* |
| `HERMES_DASHBOARD_HOST` | Bind address for the dashboard HTTP server | `0.0.0.0` |
| `HERMES_DASHBOARD_PORT` | Port for the dashboard HTTP server | `9119` |
| `HERMES_DASHBOARD_TUI` | Set to `1` to expose the in-browser Chat tab (embedded `hermes --tui` via PTY/WebSocket) | *(unset)* |
| `HERMES_DASHBOARD_INSECURE` | Set to `1` (or `true` / `yes`) to bind without the OAuth auth gate. Only use on trusted networks behind a reverse proxy without the OAuth contract — the dashboard exposes API keys and session data | *(unset — gate enforced when a `DashboardAuthProvider` is registered)* |
The dashboard inside the container defaults to binding `0.0.0.0` — without it, the published `-p 9119:9119` port would not be reachable from the host. To restrict the bind to container loopback (for sidecar / reverse-proxy setups), set `HERMES_DASHBOARD_HOST=127.0.0.1`.

View File

@ -28,7 +28,6 @@ This starts a local web server and opens `http://127.0.0.1:9119` in your browser
| `--host` | `127.0.0.1` | Bind address |
| `--no-open` | — | Don't auto-open the browser |
| `--insecure` | off | Allow binding to non-localhost hosts (**DANGEROUS** — exposes API keys on the network; pair with a firewall and strong auth) |
| `--tui` | off | Expose the in-browser Chat tab (embedded `hermes --tui` via PTY/WebSocket). Alternatively set `HERMES_DASHBOARD_TUI=1`. |
```bash
# Custom port
@ -39,9 +38,6 @@ hermes dashboard --host 0.0.0.0
# Start without opening browser
hermes dashboard --no-open
# Enable the in-browser Chat tab
hermes dashboard --tui
```
## Prerequisites
@ -56,7 +52,7 @@ The `web` extra pulls in FastAPI/Uvicorn; `pty` pulls in `ptyprocess` (POSIX) or
When you run `hermes dashboard` without the dependencies, it will tell you what to install. If the frontend hasn't been built yet and `npm` is available, it builds automatically on first launch.
The Chat tab is intentionally off for a plain `hermes dashboard` launch. Start the dashboard with `hermes dashboard --tui` or set `HERMES_DASHBOARD_TUI=1` when you want the embedded browser chat pane.
The Chat tab is part of every `hermes dashboard` launch — the embedded browser chat pane (running the TUI over PTY/WebSocket) is always available, with no extra flag required.
## Pages
@ -101,9 +97,8 @@ Hermes Desktop normally launches its own local backend, but it can also attach t
Desktop's "remote backend is ready" probe only hits `GET /api/status`, which is a public endpoint — it answers as soon as *any* dashboard is running on the host. The live chat connection is a **separate** WebSocket to `/api/ws` (and `/api/pty`), and that socket is gated by two more checks the status probe never touches:
1. **The embedded chat must be enabled.** `/api/ws` and `/api/pty` close immediately with WS code **4403** unless the dashboard was started with `--tui` (or `HERMES_DASHBOARD_TUI=1`). A plain `hermes dashboard` or `hermes gateway` serves the status page but refuses the chat socket.
2. **The session token must match.** Even with chat enabled, the socket closes with WS code **4401** if the token Desktop sends doesn't match the dashboard's session token. By default the dashboard generates a **fresh random token on every restart**, so a token you saved in Desktop yesterday is invalid after the service restarts. Pin it by setting `HERMES_DASHBOARD_SESSION_TOKEN` to a stable value.
3. **The bind host must allow the client and match the Host header.** A loopback bind (`127.0.0.1`) only accepts loopback clients, so a remote machine is rejected at the socket layer regardless of token. Bind to a non-loopback address (`--host 0.0.0.0 --insecure` for a trusted LAN) so the peer-IP guard lets the remote client through. The remote URL you enter in Desktop must reach the dashboard by the same host it bound to — the DNS-rebinding guard requires the Host header to match.
1. **The session token must match.** The socket closes with WS code **4401** if the token Desktop sends doesn't match the dashboard's session token. By default the dashboard generates a **fresh random token on every restart**, so a token you saved in Desktop yesterday is invalid after the service restarts. Pin it by setting `HERMES_DASHBOARD_SESSION_TOKEN` to a stable value.
2. **The bind host must allow the client and match the Host header.** A loopback bind (`127.0.0.1`) only accepts loopback clients, so a remote machine is rejected at the socket layer regardless of token. Bind to a non-loopback address (`--host 0.0.0.0 --insecure` for a trusted LAN) so the peer-IP guard lets the remote client through. The remote URL you enter in Desktop must reach the dashboard by the same host it bound to — the DNS-rebinding guard requires the Host header to match.
#### Remote dashboard setup
@ -716,8 +711,6 @@ The "session token" is the dashboard's session token — the same secret the loc
The desktop app sends the token as an `X-Hermes-Session-Token` header. The backend accepts it only in legacy session-token mode — i.e. when bound non-loopback **with `--insecure`**. A non-loopback bind *without* `--insecure` engages the [OAuth gate](#oauth-authentication-gated-mode) instead, which ignores the session token. So a remote desktop connection means: `--insecure` + a token you control.
The backend must also be started with **`--tui`** (or `HERMES_DASHBOARD_TUI=1`). The desktop's chat runs over the `/api/ws` + `/api/pty` WebSockets, and those are refused unless the embedded-chat surface is enabled. Without `--tui` the desktop still passes the `/api/status` health check (so the app reports the backend "ready") but the chat WebSocket is closed on connect — connects, looks ready, chat stays dead. A plain `hermes dashboard` or `hermes gateway` is not enough.
### On the backend (the remote machine)
```bash
@ -731,11 +724,9 @@ chmod 600 ~/.hermes/.env
echo "$TOKEN" # copy this value into the desktop app
# 2. Run the dashboard bound to a reachable address.
# --tui enables the embedded chat (the /api/ws + /api/pty WebSockets the
# desktop drives) — without it the app connects but chat stays dead.
# --insecure is required for any non-loopback bind and keeps the
# legacy session-token auth path (instead of the OAuth gate).
hermes dashboard --tui --no-open --insecure --host 0.0.0.0 --port 9119
hermes dashboard --no-open --insecure --host 0.0.0.0 --port 9119
```
If you run the dashboard as a systemd service, `~/.hermes/.env` is picked up automatically when the unit has `EnvironmentFile=%h/.hermes/.env`, so the token is in the environment at boot.
@ -770,7 +761,6 @@ Both must be set together — setting only the URL is an error.
- **"Remote gateway incomplete"** — you haven't entered both a URL and a token. The token only needs re-entering if `remoteTokenSet` is false (no saved token yet).
- **Test remote fails with 401** — the token doesn't match the backend's `HERMES_DASHBOARD_SESSION_TOKEN`, or the backend is running *without* `--insecure` on a non-loopback bind (the OAuth gate is on and ignores the session token). Confirm `--insecure` and that the env var is actually loaded (`curl -s -H "X-Hermes-Session-Token: $TOKEN" http://<host>:9119/api/status` should return JSON, not 401).
- **Backend reports "ready" but chat does nothing** — the backend was started without `--tui` (or `HERMES_DASHBOARD_TUI=1`), so `/api/status` answers but the chat WebSocket (`/api/ws` / `/api/pty`) is refused. Restart the backend with `--tui`.
- **Connection refused / times out** — the backend bound to `127.0.0.1` (the default) instead of a reachable address, or a firewall/VPN is blocking the port. Bind to `0.0.0.0` or the tailscale IP and open the port to your trusted network.
- **No token anywhere to copy** — expected. You mint it (`HERMES_DASHBOARD_SESSION_TOKEN`); Hermes never auto-surfaces the default ephemeral one.

View File

@ -1144,14 +1144,13 @@ hermes claw migrate --source /home/user/old-openclaw
hermes dashboard [options]
```
启动 Web 控制台——基于浏览器的界面用于管理配置、API 密钥和监控会话。需要 `pip install hermes-agent[web]`FastAPI + Uvicorn。内嵌浏览器 Chat 标签页需要 `--tui` 加上 `pty` extra。完整文档请参阅 [Web 控制台](/user-guide/features/web-dashboard)。
启动 Web 控制台——基于浏览器的界面用于管理配置、API 密钥和监控会话。需要 `pip install hermes-agent[web]`FastAPI + Uvicorn。内嵌浏览器 Chat 标签页始终可用,但额外需要 `pty` extra`pip install 'hermes-agent[web,pty]'`)以及 POSIX PTY 环境(如 Linux、macOS 或 WSL2。完整文档请参阅 [Web 控制台](/user-guide/features/web-dashboard)。
| 选项 | 默认值 | 说明 |
|--------|---------|-------------|
| `--port` | `9119` | Web 服务器运行端口 |
| `--host` | `127.0.0.1` | 绑定地址 |
| `--no-open` | — | 不自动打开浏览器 |
| `--tui` | 关闭 | 通过 PTY/WebSocket 桥接在后台运行 `hermes --tui`,启用浏览器内 Chat 标签页。需要 `pip install 'hermes-agent[web,pty]'` 以及 Linux、macOS 或 WSL2 等 POSIX PTY 环境。 |
| `--insecure` | 关闭 | 允许绑定到非 localhost 主机。会在网络上暴露控制台凭据;仅在受信任的网络控制下使用。 |
| `--stop` | — | 停止正在运行的 `hermes dashboard` 进程并退出。 |
| `--status` | — | 列出正在运行的 `hermes dashboard` 进程并退出。 |
@ -1162,9 +1161,6 @@ hermes dashboard
# 自定义端口,不打开浏览器
hermes dashboard --port 8080 --no-open
# 启用浏览器 Chat 标签页
hermes dashboard --tui
```
## `hermes profile`

View File

@ -79,7 +79,6 @@ docker run -d \
| `HERMES_DASHBOARD` | 设为 `1`(或 `true` / `yes`)以在主命令旁启动 dashboard | *(未设置——不启动 dashboard* |
| `HERMES_DASHBOARD_HOST` | dashboard HTTP 服务器的绑定地址 | `127.0.0.1` |
| `HERMES_DASHBOARD_PORT` | dashboard HTTP 服务器的端口 | `9119` |
| `HERMES_DASHBOARD_TUI` | 设为 `1` 以启用浏览器内 Chat 标签页(通过 PTY/WebSocket 嵌入 `hermes --tui` | *(未设置)* |
| `HERMES_DASHBOARD_INSECURE` | 设为 `1`(或 `true` / `yes`)以在不启用 OAuth 鉴权门控的情况下绑定。仅在可信网络(且通过没有 OAuth 契约的反向代理时使用——dashboard 会暴露 API 密钥与会话数据 | *(未设置——当注册了 `DashboardAuthProvider` 时启用门控)* |
默认情况下dashboard 保持在回环地址(`127.0.0.1`),以避免将

View File

@ -24,7 +24,6 @@ hermes dashboard
| `--host` | `127.0.0.1` | 绑定地址 |
| `--no-open` | — | 不自动打开浏览器 |
| `--insecure` | 关闭 | 允许绑定到非 localhost 主机(**危险**——会在网络上暴露 API 密钥;请配合防火墙和强认证使用) |
| `--tui` | 关闭 | 启用浏览器内 Chat 标签页(通过 PTY/WebSocket 嵌入 `hermes --tui`)。也可设置 `HERMES_DASHBOARD_TUI=1`。 |
```bash
# 自定义端口
@ -35,9 +34,6 @@ hermes dashboard --host 0.0.0.0
# 启动时不打开浏览器
hermes dashboard --no-open
# 启用浏览器内 Chat 标签页
hermes dashboard --tui
```
## 前置条件
@ -52,7 +48,7 @@ pip install 'hermes-agent[web,pty]'
在没有依赖项的情况下运行 `hermes dashboard` 时,它会告诉你需要安装什么。如果前端尚未构建且 `npm` 可用,则会在首次启动时自动构建。
Chat 标签页在普通 `hermes dashboard` 启动时默认关闭。如需嵌入式浏览器聊天面板,请使用 `hermes dashboard --tui` 启动,或设置 `HERMES_DASHBOARD_TUI=1`
Chat 标签页是每次 `hermes dashboard` 启动的一部分——内嵌的浏览器聊天面板(通过 PTY/WebSocket 运行 TUI始终可用无需任何额外参数
## 页面