From 21c6e7e5d771125976ec73bc2d23958193037ae1 Mon Sep 17 00:00:00 2001 From: taljeon Date: Fri, 4 Sep 2026 10:28:28 +0900 Subject: [PATCH 01/22] docs: add Tailscale Serve setup --- docs/remote-access.md | 75 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 67 insertions(+), 8 deletions(-) diff --git a/docs/remote-access.md b/docs/remote-access.md index a5299497..8eb7e01f 100644 --- a/docs/remote-access.md +++ b/docs/remote-access.md @@ -34,23 +34,82 @@ The Hermes Web UI is fully responsive with a mobile-optimized layout (hamburger sidebar, sidebar top tabs in the drawer, touch-friendly controls), so it works well as a daily-driver agent interface from your phone. -**Setup:** +### Preferred: Tailscale Serve + +[Tailscale Serve](https://tailscale.com/docs/features/tailscale-serve) exposes +the WebUI at a tailnet-only HTTPS/MagicDNS URL while Hermes keeps listening on +`127.0.0.1`. Serve owns the network front door; continue launching Hermes +WebUI through the maintained `start.sh` or systemd path. 1. Install [Tailscale](https://tailscale.com/download) on your server and your iPhone/Android. -2. Start the WebUI listening on all interfaces with password auth enabled: +2. Keep password authentication enabled and start the WebUI on its default + loopback address: + + ```bash + HERMES_WEBUI_PASSWORD=your-secret ./start.sh + ``` + +3. On Linux, allow the unprivileged account that runs WebUI to manage + Tailscale. This is a one-time administrator action: + + ```bash + sudo tailscale set --operator="$USER" + ``` + +4. As that operator account, publish the loopback service in the background: + + ```bash + tailscale serve --bg 8787 + ``` + + The command prints the tailnet-only HTTPS URL to open on your phone. + Background Serve configuration persists across device reboots and + `tailscale down` / `tailscale up` restarts. + +5. Verify the active mapping: + + ```bash + tailscale serve status + ``` + + To remove all Serve mappings from this device, run: + + ```bash + tailscale serve reset + ``` + +Serve requires HTTPS support in the tailnet. If it is not enabled yet, the +command can print an interactive consent URL for enabling the prerequisite. +Tailnet encryption and access-control rules narrow exposure, but they do not +replace `HERMES_WEBUI_PASSWORD`. + +> **Serve, not Funnel:** Serve is available only inside your tailnet. +> [Tailscale Funnel](https://tailscale.com/docs/features/tailscale-funnel) is +> public on the internet and is not the recommended path for Hermes WebUI. + +For the full CLI and Linux permission contracts, see the official +[`tailscale serve` reference](https://tailscale.com/docs/reference/tailscale-cli/serve) +and [Linux operator permission guide](https://tailscale.com/docs/reference/troubleshooting/linux/linux-operator-permission). + +### Fallback: direct tailnet IP + +If Serve is disabled for your tailnet or you cannot configure an operator, +bind WebUI to all interfaces with application password authentication enabled, +then access it through the server's Tailscale IP: ```bash HERMES_WEBUI_HOST=0.0.0.0 HERMES_WEBUI_PASSWORD=your-secret ./start.sh ``` -3. Open `http://:8787` in your phone's browser - (find your server's Tailscale IP in the Tailscale app or with - `tailscale ip -4` on the server). +Open `http://:8787` in your phone's browser (find the IP +in the Tailscale app or with `tailscale ip -4` on the server). -That's it. Traffic is encrypted end-to-end by WireGuard, and password auth -protects the UI at the application level. You can add it to your home screen -for an app-like experience. +Never bind Hermes WebUI to `0.0.0.0` without `HERMES_WEBUI_PASSWORD`. +`0.0.0.0` listens on every interface, not only Tailscale, so keep the host +firewall restricted as well. Traffic inside the tailnet is encrypted, but the +application still needs its own authentication boundary. You can add the page +to your home screen for an app-like experience. ### Community field report: ARM64 Android via AVF From 84d7d75f0b536299a4882d146b88712ec805f068 Mon Sep 17 00:00:00 2001 From: taljeon Date: Fri, 4 Sep 2026 10:34:36 +0900 Subject: [PATCH 02/22] docs: pin Serve backend to loopback port --- docs/remote-access.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/remote-access.md b/docs/remote-access.md index 8eb7e01f..13d3996d 100644 --- a/docs/remote-access.md +++ b/docs/remote-access.md @@ -47,7 +47,7 @@ WebUI through the maintained `start.sh` or systemd path. loopback address: ```bash - HERMES_WEBUI_PASSWORD=your-secret ./start.sh + HERMES_WEBUI_HOST=127.0.0.1 HERMES_WEBUI_PORT=8787 HERMES_WEBUI_PASSWORD=your-secret ./start.sh ``` 3. On Linux, allow the unprivileged account that runs WebUI to manage From a24bfc886787966f9e8e7bb692e3b8017102d01b Mon Sep 17 00:00:00 2001 From: taljeon Date: Fri, 4 Sep 2026 10:36:51 +0900 Subject: [PATCH 03/22] docs: respect launcher configuration precedence --- docs/remote-access.md | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/docs/remote-access.md b/docs/remote-access.md index 13d3996d..b4fbda58 100644 --- a/docs/remote-access.md +++ b/docs/remote-access.md @@ -43,13 +43,24 @@ WebUI through the maintained `start.sh` or systemd path. 1. Install [Tailscale](https://tailscale.com/download) on your server and your iPhone/Android. -2. Keep password authentication enabled and start the WebUI on its default - loopback address: +2. Keep password authentication enabled. If this checkout has a `.env` file, + put the password there instead of relying on an inline environment value; + `start.sh` loads `.env` before launch: + + ```dotenv + HERMES_WEBUI_PASSWORD=your-secret + ``` + + Then pin the backend address with launcher arguments, which override any + conflicting host or port values from `.env`: ```bash - HERMES_WEBUI_HOST=127.0.0.1 HERMES_WEBUI_PORT=8787 HERMES_WEBUI_PASSWORD=your-secret ./start.sh + ./start.sh 8787 --host 127.0.0.1 ``` + For a systemd launch, set the same host, port, and non-empty password in + the service environment. + 3. On Linux, allow the unprivileged account that runs WebUI to manage Tailscale. This is a one-time administrator action: @@ -95,11 +106,12 @@ and [Linux operator permission guide](https://tailscale.com/docs/reference/troub ### Fallback: direct tailnet IP If Serve is disabled for your tailnet or you cannot configure an operator, -bind WebUI to all interfaces with application password authentication enabled, -then access it through the server's Tailscale IP: +keep a non-empty `HERMES_WEBUI_PASSWORD` in `.env` as shown above, bind WebUI +to all interfaces with an explicit launcher override, then access it through +the server's Tailscale IP: ```bash -HERMES_WEBUI_HOST=0.0.0.0 HERMES_WEBUI_PASSWORD=your-secret ./start.sh +./start.sh 8787 --host 0.0.0.0 ``` Open `http://:8787` in your phone's browser (find the IP From ccc6b189c2560510ffe657812fd01127e3cba475 Mon Sep 17 00:00:00 2001 From: taljeon Date: Fri, 4 Sep 2026 10:44:35 +0900 Subject: [PATCH 04/22] docs: require auth before enabling Serve --- docs/remote-access.md | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/docs/remote-access.md b/docs/remote-access.md index b4fbda58..08763e76 100644 --- a/docs/remote-access.md +++ b/docs/remote-access.md @@ -43,15 +43,16 @@ WebUI through the maintained `start.sh` or systemd path. 1. Install [Tailscale](https://tailscale.com/download) on your server and your iPhone/Android. -2. Keep password authentication enabled. If this checkout has a `.env` file, - put the password there instead of relying on an inline environment value; - `start.sh` loads `.env` before launch: +2. Before publishing anything, enable application authentication. Create or + edit `.env` in the WebUI checkout and set a non-empty, strong password: ```dotenv - HERMES_WEBUI_PASSWORD=your-secret + HERMES_WEBUI_PASSWORD=replace-with-a-strong-password ``` - Then pin the backend address with launcher arguments, which override any + Alternatively, configure a password through Settings while reaching WebUI + only over loopback or an SSH tunnel. +3. Pin the backend address with launcher arguments, which override any conflicting host or port values from `.env`: ```bash @@ -60,15 +61,18 @@ WebUI through the maintained `start.sh` or systemd path. For a systemd launch, set the same host, port, and non-empty password in the service environment. +4. Open `http://127.0.0.1:8787` locally or through an SSH tunnel and confirm + that an unauthenticated browser receives the login screen. Do not publish + the Serve endpoint until this check succeeds. -3. On Linux, allow the unprivileged account that runs WebUI to manage +5. On Linux, allow the unprivileged account that runs WebUI to manage Tailscale. This is a one-time administrator action: ```bash sudo tailscale set --operator="$USER" ``` -4. As that operator account, publish the loopback service in the background: +6. As that operator account, publish the loopback service in the background: ```bash tailscale serve --bg 8787 @@ -78,7 +82,7 @@ WebUI through the maintained `start.sh` or systemd path. Background Serve configuration persists across device reboots and `tailscale down` / `tailscale up` restarts. -5. Verify the active mapping: +7. Verify the active mapping: ```bash tailscale serve status @@ -106,9 +110,9 @@ and [Linux operator permission guide](https://tailscale.com/docs/reference/troub ### Fallback: direct tailnet IP If Serve is disabled for your tailnet or you cannot configure an operator, -keep a non-empty `HERMES_WEBUI_PASSWORD` in `.env` as shown above, bind WebUI -to all interfaces with an explicit launcher override, then access it through -the server's Tailscale IP: +complete the password setup and login-screen check above, bind WebUI to all +interfaces with an explicit launcher override, then access it through the +server's Tailscale IP: ```bash ./start.sh 8787 --host 0.0.0.0 From 38852d8311ae8f05839ddae995b7eb9c0d1075ad Mon Sep 17 00:00:00 2001 From: taljeon Date: Fri, 4 Sep 2026 11:08:12 +0900 Subject: [PATCH 05/22] docs: secure Serve session cookies --- docs/remote-access.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/docs/remote-access.md b/docs/remote-access.md index 08763e76..7c670daf 100644 --- a/docs/remote-access.md +++ b/docs/remote-access.md @@ -48,10 +48,13 @@ WebUI through the maintained `start.sh` or systemd path. ```dotenv HERMES_WEBUI_PASSWORD=replace-with-a-strong-password + HERMES_WEBUI_SECURE=1 ``` Alternatively, configure a password through Settings while reaching WebUI - only over loopback or an SSH tunnel. + only over loopback or an SSH tunnel. Keep `HERMES_WEBUI_SECURE=1` in the + service environment: Serve terminates HTTPS before proxying to the local + HTTP backend, so this setting keeps the session cookie HTTPS-only. 3. Pin the backend address with launcher arguments, which override any conflicting host or port values from `.env`: @@ -63,7 +66,8 @@ WebUI through the maintained `start.sh` or systemd path. the service environment. 4. Open `http://127.0.0.1:8787` locally or through an SSH tunnel and confirm that an unauthenticated browser receives the login screen. Do not publish - the Serve endpoint until this check succeeds. + the Serve endpoint until this check succeeds. Use the local HTTP URL only + for this preflight; sign in through the HTTPS Serve URL after publishing. 5. On Linux, allow the unprivileged account that runs WebUI to manage Tailscale. This is a one-time administrator action: @@ -112,7 +116,13 @@ and [Linux operator permission guide](https://tailscale.com/docs/reference/troub If Serve is disabled for your tailnet or you cannot configure an operator, complete the password setup and login-screen check above, bind WebUI to all interfaces with an explicit launcher override, then access it through the -server's Tailscale IP: +server's Tailscale IP. Because this fallback uses plain HTTP, change the cookie +setting before restarting WebUI while keeping the password non-empty: + +```dotenv +HERMES_WEBUI_PASSWORD=replace-with-a-strong-password +HERMES_WEBUI_SECURE=0 +``` ```bash ./start.sh 8787 --host 0.0.0.0 From 8adcabc4faff0d79579f6807aa23e34109ceadc1 Mon Sep 17 00:00:00 2001 From: taljeon Date: Fri, 4 Sep 2026 11:20:51 +0900 Subject: [PATCH 06/22] docs: pin systemd Serve configuration --- docs/remote-access.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/remote-access.md b/docs/remote-access.md index 7c670daf..3d161e7c 100644 --- a/docs/remote-access.md +++ b/docs/remote-access.md @@ -62,8 +62,18 @@ WebUI through the maintained `start.sh` or systemd path. ./start.sh 8787 --host 127.0.0.1 ``` - For a systemd launch, set the same host, port, and non-empty password in - the service environment. + For a systemd launch, keep the password (unless it is already configured + through Settings) and `HERMES_WEBUI_SECURE=1` in the checkout's `.env`. + Then adapt the unit from the [supervisor guide](supervisor.md) so its + `ExecStart` pins host and port with arguments too: + + ```ini + ExecStart=/bin/bash %h/hermes-webui/start.sh 8787 --host 127.0.0.1 --foreground + ``` + + Do not rely on systemd `Environment=` entries to override conflicting + values in `.env`: `start.sh` loads that file after the service environment. + Run `systemctl --user daemon-reload` and restart the unit after editing it. 4. Open `http://127.0.0.1:8787` locally or through an SSH tunnel and confirm that an unauthenticated browser receives the login screen. Do not publish the Serve endpoint until this check succeeds. Use the local HTTP URL only From ca4d2b0dc64e352ba5f11f39ddbac1eff7c279a7 Mon Sep 17 00:00:00 2001 From: taljeon Date: Fri, 4 Sep 2026 13:07:43 +0900 Subject: [PATCH 07/22] docs: clarify reverse-proxy cookie settings --- docs/remote-access.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/remote-access.md b/docs/remote-access.md index 3d161e7c..1490f882 100644 --- a/docs/remote-access.md +++ b/docs/remote-access.md @@ -74,6 +74,10 @@ WebUI through the maintained `start.sh` or systemd path. Do not rely on systemd `Environment=` entries to override conflicting values in `.env`: `start.sh` loads that file after the service environment. Run `systemctl --user daemon-reload` and restart the unit after editing it. + + For a different trusted reverse proxy that sets `X-Forwarded-Proto: https`, + `HERMES_WEBUI_TRUST_FORWARDED_PROTO=1` is the alternative. Keep the explicit + `HERMES_WEBUI_SECURE=1` setting for Tailscale Serve. 4. Open `http://127.0.0.1:8787` locally or through an SSH tunnel and confirm that an unauthenticated browser receives the login screen. Do not publish the Serve endpoint until this check succeeds. Use the local HTTP URL only @@ -120,6 +124,9 @@ replace `HERMES_WEBUI_PASSWORD`. For the full CLI and Linux permission contracts, see the official [`tailscale serve` reference](https://tailscale.com/docs/reference/tailscale-cli/serve) and [Linux operator permission guide](https://tailscale.com/docs/reference/troubleshooting/linux/linux-operator-permission). +The Simplified Chinese companion tracked in +[#7393](https://github.com/nesquena/hermes-webui/issues/7393) should mirror the +`HERMES_WEBUI_SECURE=1` Serve requirement when translated. ### Fallback: direct tailnet IP From d275815c1f334f539dc5c6e5fe77ffb123d568fe Mon Sep 17 00:00:00 2001 From: taljeon Date: Fri, 4 Sep 2026 13:21:10 +0900 Subject: [PATCH 08/22] docs: protect checkout password settings --- docs/remote-access.md | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/docs/remote-access.md b/docs/remote-access.md index 1490f882..d96670ee 100644 --- a/docs/remote-access.md +++ b/docs/remote-access.md @@ -43,18 +43,34 @@ WebUI through the maintained `start.sh` or systemd path. 1. Install [Tailscale](https://tailscale.com/download) on your server and your iPhone/Android. -2. Before publishing anything, enable application authentication. Create or - edit `.env` in the WebUI checkout and set a non-empty, strong password: +2. Before publishing anything, enable application authentication. Prefer + configuring a strong password through Settings while reaching WebUI only + over loopback or an SSH tunnel; Settings stores a password hash rather than + a plaintext password. Keep the Serve cookie setting in the checkout's + owner-only `.env`: + + ```bash + umask 077 + touch .env + chmod 600 .env + ``` ```dotenv - HERMES_WEBUI_PASSWORD=replace-with-a-strong-password HERMES_WEBUI_SECURE=1 ``` - Alternatively, configure a password through Settings while reaching WebUI - only over loopback or an SSH tunnel. Keep `HERMES_WEBUI_SECURE=1` in the - service environment: Serve terminates HTTPS before proxying to the local - HTTP backend, so this setting keeps the session cookie HTTPS-only. + If Settings is unavailable, the same owner-only file can carry the password: + + ```dotenv + HERMES_WEBUI_PASSWORD='replace-with-a-long-random-password' + HERMES_WEBUI_SECURE=1 + ``` + + `start.sh` sources `.env` as shell syntax. Keep the password single-quoted + and do not use a literal single quote in this path; use Settings for an + arbitrary generated password. Serve terminates HTTPS before proxying to the + local HTTP backend, so `HERMES_WEBUI_SECURE=1` keeps the session cookie + HTTPS-only. 3. Pin the backend address with launcher arguments, which override any conflicting host or port values from `.env`: From 833b44a42f63b3d2537045062e08921604763144 Mon Sep 17 00:00:00 2001 From: taljeon Date: Fri, 4 Sep 2026 13:25:16 +0900 Subject: [PATCH 09/22] docs: require restart before Serve rebinding --- docs/remote-access.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/remote-access.md b/docs/remote-access.md index d96670ee..40e0b17d 100644 --- a/docs/remote-access.md +++ b/docs/remote-access.md @@ -71,7 +71,15 @@ WebUI through the maintained `start.sh` or systemd path. arbitrary generated password. Serve terminates HTTPS before proxying to the local HTTP backend, so `HERMES_WEBUI_SECURE=1` keeps the session cookie HTTPS-only. -3. Pin the backend address with launcher arguments, which override any +3. Stop any existing WebUI before changing its binding. A process listening on + `0.0.0.0:8787` also answers the loopback health probe, so `start.sh` would + otherwise report "already running" without applying the new host argument. + Use `./ctl.sh stop` for a daemon started by `ctl.sh`, or + `systemctl --user stop hermes-webui.service` for the systemd unit. If it was + started directly, stop the exact listener using the PID instructions that + `start.sh` prints. Do not continue until the previous listener has stopped. + + Then pin and start the backend with launcher arguments, which override any conflicting host or port values from `.env`: ```bash @@ -89,7 +97,8 @@ WebUI through the maintained `start.sh` or systemd path. Do not rely on systemd `Environment=` entries to override conflicting values in `.env`: `start.sh` loads that file after the service environment. - Run `systemctl --user daemon-reload` and restart the unit after editing it. + Run `systemctl --user daemon-reload` and restart the stopped unit after + editing it. For a different trusted reverse proxy that sets `X-Forwarded-Proto: https`, `HERMES_WEBUI_TRUST_FORWARDED_PROTO=1` is the alternative. Keep the explicit From 56df98731188ca6185a46c5237beb33993b2bf9c Mon Sep 17 00:00:00 2001 From: taljeon Date: Fri, 4 Sep 2026 13:29:32 +0900 Subject: [PATCH 10/22] docs: cover supervised Serve rebinding --- docs/remote-access.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/remote-access.md b/docs/remote-access.md index 40e0b17d..54c34c1c 100644 --- a/docs/remote-access.md +++ b/docs/remote-access.md @@ -74,10 +74,13 @@ WebUI through the maintained `start.sh` or systemd path. 3. Stop any existing WebUI before changing its binding. A process listening on `0.0.0.0:8787` also answers the loopback health probe, so `start.sh` would otherwise report "already running" without applying the new host argument. - Use `./ctl.sh stop` for a daemon started by `ctl.sh`, or - `systemctl --user stop hermes-webui.service` for the systemd unit. If it was - started directly, stop the exact listener using the PID instructions that - `start.sh` prints. Do not continue until the previous listener has stopped. + Use `./ctl.sh stop` for a daemon started by `ctl.sh`. For any process + supervisor — systemd, launchd, supervisord, runit, or s6 — use that + supervisor's stop, unload, or down action from the + [supervisor guide](supervisor.md), then verify it reports the service stopped + and cannot auto-restart it. If WebUI was started directly, stop the exact + listener using the PID instructions that `start.sh` prints. Do not continue + until the previous listener has stopped. Then pin and start the backend with launcher arguments, which override any conflicting host or port values from `.env`: From 816ebd5e47c06f59ccd8f75e2c76fe0b57ffb2c3 Mon Sep 17 00:00:00 2001 From: taljeon Date: Fri, 4 Sep 2026 13:31:45 +0900 Subject: [PATCH 11/22] docs: preserve lifecycle owner for Serve setup --- docs/remote-access.md | 46 +++++++++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/docs/remote-access.md b/docs/remote-access.md index 54c34c1c..36b6bbe9 100644 --- a/docs/remote-access.md +++ b/docs/remote-access.md @@ -71,28 +71,30 @@ WebUI through the maintained `start.sh` or systemd path. arbitrary generated password. Serve terminates HTTPS before proxying to the local HTTP backend, so `HERMES_WEBUI_SECURE=1` keeps the session cookie HTTPS-only. -3. Stop any existing WebUI before changing its binding. A process listening on - `0.0.0.0:8787` also answers the loopback health probe, so `start.sh` would - otherwise report "already running" without applying the new host argument. - Use `./ctl.sh stop` for a daemon started by `ctl.sh`. For any process - supervisor — systemd, launchd, supervisord, runit, or s6 — use that - supervisor's stop, unload, or down action from the - [supervisor guide](supervisor.md), then verify it reports the service stopped - and cannot auto-restart it. If WebUI was started directly, stop the exact - listener using the PID instructions that `start.sh` prints. Do not continue - until the previous listener has stopped. +3. Restart WebUI through the same lifecycle owner that already manages it. A + process listening on `0.0.0.0:8787` also answers the loopback health probe, + so `start.sh` would otherwise report "already running" without applying the + new host or reloading `HERMES_WEBUI_SECURE=1`. - Then pin and start the backend with launcher arguments, which override any - conflicting host or port values from `.env`: + For a daemon started by `ctl.sh`, keep ownership with the controller: + + ```bash + ./ctl.sh stop + ./ctl.sh start 8787 --host 127.0.0.1 + ``` + + For a direct `start.sh` launch, stop the exact listener using the PID + instructions that `start.sh` prints, verify it has stopped, then run: ```bash ./start.sh 8787 --host 127.0.0.1 ``` - For a systemd launch, keep the password (unless it is already configured - through Settings) and `HERMES_WEBUI_SECURE=1` in the checkout's `.env`. - Then adapt the unit from the [supervisor guide](supervisor.md) so its - `ExecStart` pins host and port with arguments too: + For systemd, launchd, supervisord, runit, or s6, use that supervisor's stop, + unload, or down action from the [supervisor guide](supervisor.md), update its + configured launch command with `8787 --host 127.0.0.1`, and start it again + through the same supervisor. Do not launch `start.sh` directly while a + supervisor owns the service. For example, a systemd unit uses: ```ini ExecStart=/bin/bash %h/hermes-webui/start.sh 8787 --host 127.0.0.1 --foreground @@ -100,8 +102,11 @@ WebUI through the maintained `start.sh` or systemd path. Do not rely on systemd `Environment=` entries to override conflicting values in `.env`: `start.sh` loads that file after the service environment. - Run `systemctl --user daemon-reload` and restart the stopped unit after - editing it. + Keep the password (unless it is already configured through Settings) and + `HERMES_WEBUI_SECURE=1` in the checkout's `.env`; do not rely on systemd + `Environment=` entries to override that later-loaded file. Stop the unit, + edit it, run `systemctl --user daemon-reload`, and start it again. Do not + continue until the selected lifecycle owner reports the new process running. For a different trusted reverse proxy that sets `X-Forwarded-Proto: https`, `HERMES_WEBUI_TRUST_FORWARDED_PROTO=1` is the alternative. Keep the explicit @@ -165,10 +170,13 @@ server's Tailscale IP. Because this fallback uses plain HTTP, change the cookie setting before restarting WebUI while keeping the password non-empty: ```dotenv -HERMES_WEBUI_PASSWORD=replace-with-a-strong-password +HERMES_WEBUI_PASSWORD='replace-with-a-long-random-password' HERMES_WEBUI_SECURE=0 ``` +Stop and restart through the same lifecycle owner described in step 3, changing +that owner's host argument to `0.0.0.0`. For a direct launch only, run: + ```bash ./start.sh 8787 --host 0.0.0.0 ``` From ff69c0342ddfde4961c56c5d9c123601d2a2bdf4 Mon Sep 17 00:00:00 2001 From: taljeon Date: Fri, 4 Sep 2026 13:36:28 +0900 Subject: [PATCH 12/22] docs: add supervisor transition commands --- docs/remote-access.md | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/docs/remote-access.md b/docs/remote-access.md index 36b6bbe9..25896071 100644 --- a/docs/remote-access.md +++ b/docs/remote-access.md @@ -90,23 +90,34 @@ WebUI through the maintained `start.sh` or systemd path. ./start.sh 8787 --host 127.0.0.1 ``` - For systemd, launchd, supervisord, runit, or s6, use that supervisor's stop, - unload, or down action from the [supervisor guide](supervisor.md), update its - configured launch command with `8787 --host 127.0.0.1`, and start it again - through the same supervisor. Do not launch `start.sh` directly while a - supervisor owns the service. For example, a systemd unit uses: + For a supervisor-managed service, run its matching stop command, update the + configured launch command with `8787 --host 127.0.0.1`, then use the same + owner to start it again: + + - systemd: `systemctl --user stop hermes-webui.service`, then + `systemctl --user daemon-reload` and + `systemctl --user start hermes-webui.service`. + - launchd: `launchctl unload ~/Library/LaunchAgents/com.example.hermes-webui.plist`, + then `launchctl load` with the same plist path. + - supervisord: `sudo supervisorctl stop hermes-webui`, then + `sudo supervisorctl reread`, `sudo supervisorctl update`, and + `sudo supervisorctl start hermes-webui`. + - runit: `sv down `, then `sv up `. + - s6: `s6-svc -d `, then + `s6-svc -u `. + + The [supervisor guide](supervisor.md) shows the corresponding launch-command + locations. Do not launch `start.sh` directly while a supervisor owns the + service. For example, a systemd unit uses: ```ini ExecStart=/bin/bash %h/hermes-webui/start.sh 8787 --host 127.0.0.1 --foreground ``` - Do not rely on systemd `Environment=` entries to override conflicting - values in `.env`: `start.sh` loads that file after the service environment. Keep the password (unless it is already configured through Settings) and `HERMES_WEBUI_SECURE=1` in the checkout's `.env`; do not rely on systemd - `Environment=` entries to override that later-loaded file. Stop the unit, - edit it, run `systemctl --user daemon-reload`, and start it again. Do not - continue until the selected lifecycle owner reports the new process running. + `Environment=` entries to override that later-loaded file. Do not continue + until the selected lifecycle owner reports the new process running. For a different trusted reverse proxy that sets `X-Forwarded-Proto: https`, `HERMES_WEBUI_TRUST_FORWARDED_PROTO=1` is the alternative. Keep the explicit From b6aa179fd1ae7342280584b8e668f4e6502bcd10 Mon Sep 17 00:00:00 2001 From: taljeon Date: Fri, 4 Sep 2026 13:41:11 +0900 Subject: [PATCH 13/22] docs: locate runit and s6 launch commands --- docs/remote-access.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/remote-access.md b/docs/remote-access.md index 25896071..12f512ee 100644 --- a/docs/remote-access.md +++ b/docs/remote-access.md @@ -102,9 +102,12 @@ WebUI through the maintained `start.sh` or systemd path. - supervisord: `sudo supervisorctl stop hermes-webui`, then `sudo supervisorctl reread`, `sudo supervisorctl update`, and `sudo supervisorctl start hermes-webui`. - - runit: `sv down `, then `sv up `. - - s6: `s6-svc -d `, then - `s6-svc -u `. + - runit: `sv down `, edit + `/run` so its `exec` invokes + `/bin/bash /path/to/hermes-webui/start.sh 8787 --host 127.0.0.1 --foreground`, + then run `sv up `. + - s6: `s6-svc -d `, apply the same `exec` change in + `/run`, then run `s6-svc -u `. The [supervisor guide](supervisor.md) shows the corresponding launch-command locations. Do not launch `start.sh` directly while a supervisor owns the From c45ee592f2e93ea1b3e586053475a1becd5ed540 Mon Sep 17 00:00:00 2001 From: taljeon Date: Fri, 4 Sep 2026 13:42:54 +0900 Subject: [PATCH 14/22] docs: make Serve the canonical remote entry point --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7e73b9f1..9c074e95 100644 --- a/README.md +++ b/README.md @@ -444,7 +444,13 @@ Set `environmentFiles` for secrets like API keys. Protected WebUI runtime keys f ### Remote access (SSH tunnel, Tailscale, phone) -The server binds to `127.0.0.1` by default. To reach it from another machine use an SSH tunnel (`ssh -N -L 8787:127.0.0.1:8787 user@host`, which `start.sh` prints for you over SSH), or join your server and phone to a [Tailscale](https://tailscale.com) network and browse to `http://:8787` with `HERMES_WEBUI_HOST=0.0.0.0` + `HERMES_WEBUI_PASSWORD` set. Full walkthrough (incl. a community ARM64-Android field report): [`docs/remote-access.md`](docs/remote-access.md). +The server binds to `127.0.0.1` by default. To reach it from another machine, +use an SSH tunnel (`ssh -N -L 8787:127.0.0.1:8787 user@host`, which `start.sh` +prints for you over SSH) or use the preferred [Tailscale Serve](https://tailscale.com/docs/features/tailscale-serve) +flow, which keeps WebUI on loopback behind tailnet-only HTTPS. Direct access to +`http://:8787` with `HERMES_WEBUI_HOST=0.0.0.0` and +`HERMES_WEBUI_PASSWORD` is a fallback when Serve is unavailable. Full setup and +the community ARM64-Android field report: [`docs/remote-access.md`](docs/remote-access.md). ### Manual launch (without start.sh) From 270832cf4cf62fd2676e970b3700e2dfa79f95cb Mon Sep 17 00:00:00 2001 From: taljeon Date: Fri, 4 Sep 2026 13:52:45 +0900 Subject: [PATCH 15/22] docs: bound Serve to restricted tailnets --- README.md | 3 ++- docs/remote-access.md | 11 ++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9c074e95..b04abc72 100644 --- a/README.md +++ b/README.md @@ -446,7 +446,8 @@ Set `environmentFiles` for secrets like API keys. Protected WebUI runtime keys f The server binds to `127.0.0.1` by default. To reach it from another machine, use an SSH tunnel (`ssh -N -L 8787:127.0.0.1:8787 user@host`, which `start.sh` -prints for you over SSH) or use the preferred [Tailscale Serve](https://tailscale.com/docs/features/tailscale-serve) +prints for you over SSH) or, on a single-operator or access-restricted tailnet, +use the preferred [Tailscale Serve](https://tailscale.com/docs/features/tailscale-serve) flow, which keeps WebUI on loopback behind tailnet-only HTTPS. Direct access to `http://:8787` with `HERMES_WEBUI_HOST=0.0.0.0` and `HERMES_WEBUI_PASSWORD` is a fallback when Serve is unavailable. Full setup and diff --git a/docs/remote-access.md b/docs/remote-access.md index 12f512ee..9e061964 100644 --- a/docs/remote-access.md +++ b/docs/remote-access.md @@ -34,13 +34,22 @@ The Hermes Web UI is fully responsive with a mobile-optimized layout (hamburger sidebar, sidebar top tabs in the drawer, touch-friendly controls), so it works well as a daily-driver agent interface from your phone. -### Preferred: Tailscale Serve +### Preferred on a single-operator or access-restricted tailnet: Tailscale Serve [Tailscale Serve](https://tailscale.com/docs/features/tailscale-serve) exposes the WebUI at a tailnet-only HTTPS/MagicDNS URL while Hermes keeps listening on `127.0.0.1`. Serve owns the network front door; continue launching Hermes WebUI through the maintained `start.sh` or systemd path. +Hermes currently sees every Serve login request as coming from the local proxy, +so password and passkey logins share one five-attempt, 60-second rate-limit +bucket across all clients. Use this path for a single operator, or restrict +access to the Serve node to the intended operator with [tailnet access-control +rules](https://tailscale.com/docs/features/access-control). On a broader +multi-user tailnet, use the SSH tunnel above or the direct-IP fallback below +until Hermes can safely attribute login attempts per Serve client. If the +shared bucket is reached, wait 60 seconds before trying again. + 1. Install [Tailscale](https://tailscale.com/download) on your server and your iPhone/Android. 2. Before publishing anything, enable application authentication. Prefer From a4fc828c061f2c4ee7d9317e7a0cd89065982bba Mon Sep 17 00:00:00 2001 From: taljeon Date: Fri, 4 Sep 2026 13:57:28 +0900 Subject: [PATCH 16/22] docs: use canonical launchd service path --- docs/remote-access.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/remote-access.md b/docs/remote-access.md index 9e061964..5a503309 100644 --- a/docs/remote-access.md +++ b/docs/remote-access.md @@ -106,8 +106,11 @@ shared bucket is reached, wait 60 seconds before trying again. - systemd: `systemctl --user stop hermes-webui.service`, then `systemctl --user daemon-reload` and `systemctl --user start hermes-webui.service`. - - launchd: `launchctl unload ~/Library/LaunchAgents/com.example.hermes-webui.plist`, - then `launchctl load` with the same plist path. + - launchd: `launchctl unload ~/Library/LaunchAgents/com.parantoux.hermes-webui.plist`, + update that plist's `ProgramArguments`, then run + `launchctl load ~/Library/LaunchAgents/com.parantoux.hermes-webui.plist`. + If you customized the label or plist path, use the actual installed plist + for both commands. - supervisord: `sudo supervisorctl stop hermes-webui`, then `sudo supervisorctl reread`, `sudo supervisorctl update`, and `sudo supervisorctl start hermes-webui`. From 4dc00892547d51353423967f0a235376285e1cb4 Mon Sep 17 00:00:00 2001 From: taljeon Date: Fri, 4 Sep 2026 13:59:20 +0900 Subject: [PATCH 17/22] docs: preserve Settings password fallback --- docs/remote-access.md | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/docs/remote-access.md b/docs/remote-access.md index 5a503309..48cff7fb 100644 --- a/docs/remote-access.md +++ b/docs/remote-access.md @@ -193,7 +193,20 @@ If Serve is disabled for your tailnet or you cannot configure an operator, complete the password setup and login-screen check above, bind WebUI to all interfaces with an explicit launcher override, then access it through the server's Tailscale IP. Because this fallback uses plain HTTP, change the cookie -setting before restarting WebUI while keeping the password non-empty: +setting before restarting WebUI while preserving the authentication path you +already configured. + +If you configured the password through Settings, change only the cookie setting +in the owner-only `.env`: + +```dotenv +HERMES_WEBUI_SECURE=0 +``` + +Do not add `HERMES_WEBUI_PASSWORD` in this case: the environment value takes +precedence over the password hash stored by Settings and would replace that +login credential. If you chose the `.env` password path instead, keep its +password non-empty and single-quoted: ```dotenv HERMES_WEBUI_PASSWORD='replace-with-a-long-random-password' @@ -210,7 +223,9 @@ that owner's host argument to `0.0.0.0`. For a direct launch only, run: Open `http://:8787` in your phone's browser (find the IP in the Tailscale app or with `tailscale ip -4` on the server). -Never bind Hermes WebUI to `0.0.0.0` without `HERMES_WEBUI_PASSWORD`. +Never bind Hermes WebUI to `0.0.0.0` without confirmed application +authentication. For the `.env` password path, keep `HERMES_WEBUI_PASSWORD` +non-empty; for the Settings path, preserve the stored password hash instead. `0.0.0.0` listens on every interface, not only Tailscale, so keep the host firewall restricted as well. Traffic inside the tailnet is encrypted, but the application still needs its own authentication boundary. You can add the page From a77956fcfd759667dd774d54738c1dd5083cf268 Mon Sep 17 00:00:00 2001 From: xiaobao Date: Fri, 25 Sep 2026 02:37:52 +0800 Subject: [PATCH 18/22] docs(zh): add Chinese remote-access guide (Tailscale, SSH tunnel, native Windows) Companion to docs/remote-access.md for Chinese users, per the contribution shape requested in #7393: - Tailscale Serve preferred path (loopback + HTTPS MagicDNS), direct tailnet IP as fallback, SSH tunnel section - Native Windows deployment via the official start.ps1 launcher with password auth; firewall rule scoped to the Tailscale CGNAT range (100.64.0.0/10) instead of an all-interfaces open - Windows logon auto-start via the official scripts/windows/setup_webui_autostart.ps1 scheduled-task helper - Explicit security boundary section distinguishing tailnet-private, LAN, and public exposure - No machine-specific usernames/drives/model choices/popularity numbers - Linked from the README docs index Tests: test_readme_compat_section, test_issue3012_3006_docker_docs, test_nix_flake_module_source all pass (17/17). --- README.md | 1 + docs/remote-access-zh.md | 236 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 237 insertions(+) create mode 100644 docs/remote-access-zh.md diff --git a/README.md b/README.md index 676d6cd4..1b048f5c 100644 --- a/README.md +++ b/README.md @@ -684,6 +684,7 @@ The WebUI is still coupled to Hermes Agent internals for runtime execution, prov **Deploying & operating** - [`docs/remote-access.md`](docs/remote-access.md) — SSH tunnel, Tailscale, and phone access (incl. a community ARM64-Android field report) +- [`docs/remote-access-zh.md`](docs/remote-access-zh.md) — 中文远程访问指南:Tailscale、SSH 隧道、Windows 原生部署与开机自启 - [`docs/advanced-chat-setup.md`](docs/advanced-chat-setup.md) — optional dynamic recall-prefill and Gateway-backed browser chat for self-hosted deployments - [`docs/docker.md`](docs/docker.md) — Docker compose setup, common failures, and bind-mount migration - [`docs/supervisor.md`](docs/supervisor.md) — launchd, systemd, supervisord, runit, and s6 process-supervisor setup diff --git a/docs/remote-access-zh.md b/docs/remote-access-zh.md new file mode 100644 index 00000000..d67f35ad --- /dev/null +++ b/docs/remote-access-zh.md @@ -0,0 +1,236 @@ +# 远程访问(中文指南) + +> 本文是 [`docs/remote-access.md`](remote-access.md) 的中文姊妹篇,面向中文用户, +> 重点覆盖 Windows 原生环境。命令与安全约定以官方脚本(`start.sh` / `start.ps1` / +> `scripts/windows/setup_webui_autostart.ps1`)为准。 + +Hermes WebUI 默认只绑定 `127.0.0.1`(仅本机回环),这是刻意的安全默认值。 +要从手机或另一台电脑访问,需要显式选择一条安全通道。本文按推荐程度排序: + +| 方式 | 安全性 | 复杂度 | 适用场景 | +|---|---|---|---| +| Tailscale Serve(HTTPS + MagicDNS) | ★★★★★ | 低 | 推荐首选,手机日常使用 | +| SSH 隧道 | ★★★★★ | 低 | Linux/macOS 单次使用 | +| Tailscale 直连 IP | ★★★★☆ | 低 | Tailscale Serve 不可用时 | +| 公网/LAN 直接暴露 | ★★☆☆☆ | 高 | 不推荐,见文末安全边界 | + +--- + +## 一、SSH 隧道(Linux / macOS / VPS) + +服务器保持默认回环绑定不变,从本地机器开一条 SSH 隧道: + +```bash +ssh -N -L <本地端口>:127.0.0.1:<远程端口> <用户>@<服务器地址> +``` + +示例(远程 8787 → 本地 8787): + +```bash +ssh -N -L 8787:127.0.0.1:8787 user@your.server.com +``` + +然后浏览器打开 `http://localhost:8787`。 + +`start.sh` 检测到你在 SSH 会话中运行时会自动打印这条命令。 + +--- + +## 二、Tailscale 私有网络访问(推荐) + +[Tailscale](https://tailscale.com) 是基于 WireGuard 的零配置组网工具: +服务器和手机装上后自动进入同一个私有网络,**无需端口转发、无需公网暴露**。 +Hermes WebUI 自带移动端适配布局(汉堡侧栏、抽屉式顶部标签、触控友好的控件), +作为手机的日常 AI 助手界面非常合适。 + +### 方案 A:Tailscale Serve(首选) + +WebUI 保持绑定回环,只把端口"发布"到你的 tailnet: + +1. 在服务器和手机上都安装 [Tailscale](https://tailscale.com/download) 并登录同一账号。 +2. 启用密码认证后启动 WebUI(保持回环绑定): + + ```bash + HERMES_WEBUI_PASSWORD=你的密码 ./start.sh + ``` + + Windows 原生环境见下文"三、Windows 原生部署"。 + +3. 通过 Tailscale Serve 发布本地端口: + + ```bash + tailscale serve --bg 8787 + ``` + +4. 用 Tailscale 打印的 HTTPS MagicDNS 地址在手机浏览器打开。 + +要点: + +- Tailscale Serve 让 WebUI 继续待在回环上,同时为你的 tailnet 提供 HTTPS + MagicDNS 域名,**不需要改 `HERMES_WEBUI_HOST`,也不需要开防火墙端口**。 +- Linux 上修改 Serve 配置可能需要提权。若报 `Access denied: serve config denied`, + 用 sudo 执行: + + ```bash + sudo -S -p '' tailscale serve --bg 8787 + ``` + + 或者把运行 WebUI 的非 root 用户设为 Tailscale operator: + + ```bash + sudo -S -p '' tailscale set --operator=$USER + tailscale serve --bg 8787 + ``` + +### 方案 B:直连 Tailscale IP(备选) + +Serve 不可用/被禁用时,直接走 tailnet IP。因为这会超出回环绑定,**必须启用密码认证**: + +```bash +HERMES_WEBUI_HOST=0.0.0.0 HERMES_WEBUI_PASSWORD=你的密码 ./start.sh +``` + +然后在手机浏览器打开 `http://<服务器Tailscale-IP>:8787`(服务器上执行 +`tailscale ip -4` 查看自己的 Tailscale IP)。 + +流量全程由 WireGuard 端到端加密,密码认证在应用层保护界面。 +可以把它添加到手机主屏,获得近似原生 App 的体验。 + +--- + +## 三、Windows 原生部署 + +Windows 用户不要直接 `python server.py`——请用仓库自带的原生启动器 +[`start.ps1`](../start.ps1),它会自动完成:加载 `.env`、发现 Python、 +定位 hermes-agent 安装目录、设置默认环境变量,然后拉起 `server.py`。 + +### 1. 首次准备 + +原生 Windows 路径要求 Python 3.11+ 与 hermes-agent 已安装。推荐在 +PowerShell 中创建 Windows 虚拟环境并安装依赖: + +```powershell +python -m venv venv +venv\Scripts\pip install -r requirements.txt +``` + +> 注意:在 WSL2 里创建的虚拟环境是 Linux 格式(`venv/bin/python`), +> 原生 Windows Python 无法使用,必须用 Windows 原生 `python -m venv` 重建。 + +### 2. 带密码启动(前台) + +```powershell +$env:HERMES_WEBUI_PASSWORD = "你的密码" +.\start.ps1 +``` + +默认绑定 `127.0.0.1:8787`。常用参数: + +| 参数 / 环境变量 | 作用 | 示例 | +|---|---|---| +| `-Port` | 端口覆盖(优先于环境变量) | `.\start.ps1 -Port 9000` | +| `-BindHost` | 绑定地址覆盖 | `.\start.ps1 -BindHost 0.0.0.0` | +| `HERMES_WEBUI_HOST` | 绑定地址(默认 `127.0.0.1`) | `$env:HERMES_WEBUI_HOST = "0.0.0.0"` | +| `HERMES_WEBUI_PORT` | 端口(默认 `8787`) | `$env:HERMES_WEBUI_PORT = "8787"` | +| `HERMES_WEBUI_PASSWORD` | 界面密码认证 | 见上 | +| `HERMES_WEBUI_AGENT_DIR` | 显式指定 hermes-agent 目录 | `$env:HERMES_WEBUI_AGENT_DIR = "C:\path\to\hermes-agent"` | + +> **绑定 `0.0.0.0` 前先设密码**:一旦绑定所有接口,任何能路由到该端口的设备 +> 都能触达界面,密码认证是唯一防线。参见文末"安全边界"。 + +### 3. Windows 防火墙 + +**用 Tailscale Serve(方案 A)时不需要任何防火墙放行**——连接发生在 +Tailscale 虚拟网卡与回环之间,Windows 防火墙不会拦截回环流量。 + +用"直连 Tailscale IP(方案 B)"时,Windows 防火墙可能会拦下从手机进来的 +8787 连接。此时请只对 **Tailscale 接口/网络**放行,而不是对所有网络放行: + +```powershell +# 允许 Tailscale 网段访问 8787(按你的实际 tailnet CIDR 调整) +New-NetFirewallRule -DisplayName "Hermes WebUI (Tailscale only)" ` + -Direction Inbound -Protocol TCP -LocalPort 8787 ` + -RemoteAddress 100.64.0.0/10 -Action Allow +``` + +要点: + +- `100.64.0.0/10` 是 CGNAT 保留段,Tailscale 的虚拟 IP 都落在这个范围; + 这样规则只对 tailnet 生效,公网接口依然拒绝。 +- 不要写 `-Profile Any -RemoteAddress Any` 这类全放行规则——那是把服务 + 直接暴露到公网的行为,见"安全边界"。 + +### 4. Windows 开机自启 + +官方自启通道是任务计划程序 + WSL 启动脚本,配套脚本在 +[`scripts/windows/setup_webui_autostart.ps1`](../scripts/windows/setup_webui_autostart.ps1)。 +它注册一个"登录时启动"的计划任务,通过 `wsl.exe` 在 WSL 发行版内执行启动脚本。 +该脚本幂等:重复运行只更新已有任务,不会创建重复项。 + +前置:WSL2 内已能手动启动 WebUI(见 [`docs/wsl-autostart.md`](wsl-autostart.md)), +并确保启动脚本可执行: + +```bash +chmod +x /path/to/hermes-webui/scripts/wsl/hermes_webui_autostart.sh +``` + +然后回到 Windows PowerShell 注册计划任务(`-WslScriptPath` 是 **WSL 内部路径**): + +```powershell +Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass +.\scripts\windows\setup_webui_autostart.ps1 ` + -WslScriptPath "/home/你的用户名/hermes-webui/scripts/wsl/hermes_webui_autostart.sh" ` + -Distro "Ubuntu" +``` + +参数说明: + +- `-Distro` 可省略,省略时使用默认 WSL 发行版。 +- 任务默认名 `HermesWebUIAutoStart`,需要改名传 `-TaskName`。 +- 加 `-RunNow` 注册后立即启动;加 `-WhatIf` 只预览不注册; + 仅当 WSL 路径尚不存在时才需要 `-SkipValidation`。 + +事后查看或移除任务: + +```powershell +Get-ScheduledTask -TaskName HermesWebUIAutoStart +Unregister-ScheduledTask -TaskName HermesWebUIAutoStart -Confirm:$false +``` + +自启日志位置(WSL 内): + +```text +$HOME/.hermes/webui/logs/webui_autostart.log +$HOME/.hermes/webui/logs/hermes_webui.log +``` + +--- + +## 四、常见问题 + +| 症状 | 可能原因 | 处理 | +|---|---|---| +| 手机打不开地址 | WebUI 没绑定到可达接口 | 方案 A 检查 Serve 状态(`tailscale serve status`);方案 B 确认 `HERMES_WEBUI_HOST=0.0.0.0` 且防火墙已放行 Tailscale | +| 任务计划已建但 WebUI 没起来 | WSL 脚本路径写错/发行版不对 | 用正确的 `-WslScriptPath` 与 `-Distro` 重跑注册脚本 | +| 打开 WSL 才启动,登录时不启动 | 用的是会话级自启而非计划任务 | 安装 Windows 计划任务 | +| 健康检查失败但进程存在 | 端口不一致或仍在启动 | 核对 `HERMES_WEBUI_PORT` 与 `hermes_webui.log` | +| 提示需要密码但没设过 | 界面要求认证 | 设置 `HERMES_WEBUI_PASSWORD` 后重启 | + +--- + +## 五、安全边界(重要) + +**区分三种暴露范围:** + +1. **Tailscale 私有(推荐)**:仅你的 tailnet 设备可达。Serve 方案连端口都不用开; + 直连 IP 方案也只需对 `100.64.0.0/10` 放行。WireGuard 加密 + 应用层密码认证。 +2. **局域网(LAN)**:`HERMES_WEBUI_HOST=0.0.0.0` 且防火墙对私有网段放行。 + 同一 WiFi 下的其他设备都能触达——密码认证是必需品,且不建议用于公共 WiFi。 +3. **公网**:端口对互联网开放。**不推荐**。即便有密码,也缺乏传输加密与暴力破解 + 防护;若确有需要,应放在 HTTPS 反向代理(Caddy/Nginx)之后并启用强密码。 + +**所有超出回环的绑定,第一守则:先设 `HERMES_WEBUI_PASSWORD`,再改绑定地址。** + +--- + +*原文:[`docs/remote-access.md`](remote-access.md) · 部署相关:[`docs/docker.md`](docker.md) · Windows/WSL 自启:[`docs/wsl-autostart.md`](wsl-autostart.md)* From 24dd1fbd581693b75895265ae4e5e0c0b844b55f Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Thu, 24 Sep 2026 19:47:45 +0000 Subject: [PATCH 19/22] docs(zh): correct the public-exposure note (WebUI does rate-limit logins; the gap is plaintext HTTP) --- docs/remote-access-zh.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/remote-access-zh.md b/docs/remote-access-zh.md index d67f35ad..411f7c31 100644 --- a/docs/remote-access-zh.md +++ b/docs/remote-access-zh.md @@ -226,8 +226,9 @@ $HOME/.hermes/webui/logs/hermes_webui.log 直连 IP 方案也只需对 `100.64.0.0/10` 放行。WireGuard 加密 + 应用层密码认证。 2. **局域网(LAN)**:`HERMES_WEBUI_HOST=0.0.0.0` 且防火墙对私有网段放行。 同一 WiFi 下的其他设备都能触达——密码认证是必需品,且不建议用于公共 WiFi。 -3. **公网**:端口对互联网开放。**不推荐**。即便有密码,也缺乏传输加密与暴力破解 - 防护;若确有需要,应放在 HTTPS 反向代理(Caddy/Nginx)之后并启用强密码。 +3. **公网**:端口对互联网开放。**不推荐**。WebUI 本身只做简单的登录限速(每个 IP + 每 60 秒最多 5 次失败尝试),但直连时没有传输加密(HTTP 明文);若确有需要, + 应放在 HTTPS 反向代理(Caddy/Nginx)之后并启用强密码。 **所有超出回环的绑定,第一守则:先设 `HERMES_WEBUI_PASSWORD`,再改绑定地址。** From c13a2ecf640d4b25b3cc33b08cc06bcfce8a61c6 Mon Sep 17 00:00:00 2001 From: happy5318 <5318happy@users.noreply.github.com> Date: Sat, 26 Sep 2026 01:39:24 +0800 Subject: [PATCH 20/22] docs(zh): align Windows sections with start.ps1 / WSL-only autostart MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review feedback (nesquena 9/24, PR #7814): 1. §三.1 "首次准备" 原本让用户在 WebUI 仓库本地 `python -m venv venv` 再 `pip install -r requirements.txt`,但 `start.ps1`(line 132-136) 启动时优先用 `\venv\Scripts\python.exe`,找不到才 回退到 PATH 上的 `python`——WebUI 本地的 venv 永远不会被它挑中。 照原指南装的依赖会变成死代码,启动时仍然缺包。重写为:要求把 requirements.txt 装进 hermes-agent 的 venv,列出 hermes-agent 的 常见安装位置,保留 WebUI 本地 venv 作备选但明确警告。 2. §三.4 之前是 "Windows 开机自启",但内容(setup_webui_autostart.ps1 → wsl.exe → scripts/wsl/hermes_webui_autostart.sh)实际上**只** 启动 WSL 里的 WebUI,放在 native-Windows 章节里会误导用户以为 native 启动也有自启配套。把它从 §三 抽出,单独成节 "## 四、WSL 用户:Windows 开机自启",顶部加 WSL-only 提示, §四/§五 顺延为 §五/§六,FAQ 里的 cross-ref 同步更新,README 中文链接文案改为 "...Windows 原生部署(自启仅适用 WSL 用户)" 不再暗示 native 自启。 3. 顺手处理两条可选小项: a. start.sh / start.ps1 / bootstrap.py 都会从仓库 .env 加载 `HERMES_WEBUI_PASSWORD`,且 .env 里的值**覆盖**命令行内联 的 env var。在 §三.2 加 callout 提示:改完密码后打开登录页 确认真的要求输入密码;如未生效就清 .env 或显式 `HERMES_WEBUI_PRESERVE_ENV=1`。 b. §六.1 "Tailscale 私有" 原写法 "仅你的 tailnet 设备可达" 在 方案 B(`HERMES_WEBUI_HOST=0.0.0.0`)下不成立——`0.0.0.0` 绑的是所有接口,"仅 tailnet" 完全靠防火墙那条 `100.64.0.0/10` 规则兜底。改为:首选 `HERMES_WEBUI_HOST= $(tailscale ip -4)`(只绑 Tailscale 网卡,更严),次选 `0.0.0.0` + 防火墙,并明确标注"防火墙被改/被关就立刻 对 LAN/公网开放"。 保留 24dd1fbd 的 public-exposure note 修正。 --- README.md | 2 +- docs/remote-access-zh.md | 71 ++++++++++++++++++++++++++++++++-------- 2 files changed, 59 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 1b048f5c..d2c94c34 100644 --- a/README.md +++ b/README.md @@ -684,7 +684,7 @@ The WebUI is still coupled to Hermes Agent internals for runtime execution, prov **Deploying & operating** - [`docs/remote-access.md`](docs/remote-access.md) — SSH tunnel, Tailscale, and phone access (incl. a community ARM64-Android field report) -- [`docs/remote-access-zh.md`](docs/remote-access-zh.md) — 中文远程访问指南:Tailscale、SSH 隧道、Windows 原生部署与开机自启 +- [`docs/remote-access-zh.md`](docs/remote-access-zh.md) — 中文远程访问指南:Tailscale、SSH 隧道、Windows 原生部署(自启仅适用 WSL 用户) - [`docs/advanced-chat-setup.md`](docs/advanced-chat-setup.md) — optional dynamic recall-prefill and Gateway-backed browser chat for self-hosted deployments - [`docs/docker.md`](docs/docker.md) — Docker compose setup, common failures, and bind-mount migration - [`docs/supervisor.md`](docs/supervisor.md) — launchd, systemd, supervisord, runit, and s6 process-supervisor setup diff --git a/docs/remote-access-zh.md b/docs/remote-access-zh.md index 411f7c31..b9d701c7 100644 --- a/docs/remote-access-zh.md +++ b/docs/remote-access-zh.md @@ -106,16 +106,38 @@ Windows 用户不要直接 `python server.py`——请用仓库自带的原生 ### 1. 首次准备 -原生 Windows 路径要求 Python 3.11+ 与 hermes-agent 已安装。推荐在 -PowerShell 中创建 Windows 虚拟环境并安装依赖: +原生 Windows 路径要求 Python 3.11+ 与 hermes-agent 已安装。 +**关键:依赖要装在 `start.ps1` 实际使用的那个解释器里。** `start.ps1` +启动时会优先用 `\venv\Scripts\python.exe`,找不到才回退 +到 PATH 上的 `python`——WebUI 仓库本地的 `venv\` 永远不会被它挑中。 +所以把 `requirements.txt` 装到 hermes-agent 的 venv,而不是在 WebUI +仓库下新建一个 venv。 -```powershell -python -m venv venv -venv\Scripts\pip install -r requirements.txt +hermes-agent 的常见安装位置(任选其一,取决于当初的安装方式): + +```text +%USERPROFILE%\.hermes\hermes-agent # 官方安装器默认 +%LOCALAPPDATA%\hermes\hermes-agent # 备选 +%ProgramW6432%\hermes\hermes-agent # 全局安装 ``` -> 注意:在 WSL2 里创建的虚拟环境是 Linux 格式(`venv/bin/python`), -> 原生 Windows Python 无法使用,必须用 Windows 原生 `python -m venv` 重建。 +在该目录下创建 venv 并装依赖(下面以默认的 `%USERPROFILE%\.hermes\hermes-agent` +为例): + +```powershell +cd $env:USERPROFILE\.hermes\hermes-agent +python -m venv venv +.\venv\Scripts\pip.exe install -r C:\path\to\hermes-webui\requirements.txt +``` + +如果 hermes-agent 装在其它位置,把第一行 `cd` 换成对应目录即可。 + +> 备选:在 WebUI 仓库本地建 venv 也可以(`python -m venv venv` + +> `venv\Scripts\pip install -r requirements.txt`),但这只在 +> `\venv\Scripts\python.exe` 不存在、且 PATH 上的 +> `python.exe` 恰好指向你装依赖时用的那个解释器时,`start.ps1` 才 +> 会用上你装的依赖——非常容易踩坑。最稳的路径仍然是装到 hermes-agent +> 的 venv。 ### 2. 带密码启动(前台) @@ -137,6 +159,14 @@ $env:HERMES_WEBUI_PASSWORD = "你的密码" > **绑定 `0.0.0.0` 前先设密码**:一旦绑定所有接口,任何能路由到该端口的设备 > 都能触达界面,密码认证是唯一防线。参见文末"安全边界"。 +> +> **改完密码先验证一下**:`start.sh` / `start.ps1` / `bootstrap.py` 都会 +> 加载仓库根目录的 `.env`,且 `.env` 里的值会**覆盖**命令行内联的 +> `HERMES_WEBUI_PASSWORD=`。如果你以前在 `.env` 里写过旧密码、现在又 +> 想换成新的,单纯在命令行里 export 是没用的——`.env` 里的旧值会盖掉它。 +> 改完密码后用浏览器打开登录页确认**真的要求输入密码**,没有跳过直接 +> 进入主界面就说明 `.env` 还在用旧值,需要先清理或显式 +> `HERMES_WEBUI_PRESERVE_ENV=1`(仅 `bootstrap.py` 支持)再启动。 ### 3. Windows 防火墙 @@ -160,9 +190,14 @@ New-NetFirewallRule -DisplayName "Hermes WebUI (Tailscale only)" ` - 不要写 `-Profile Any -RemoteAddress Any` 这类全放行规则——那是把服务 直接暴露到公网的行为,见"安全边界"。 -### 4. Windows 开机自启 +## 四、WSL 用户:Windows 开机自启 -官方自启通道是任务计划程序 + WSL 启动脚本,配套脚本在 +> **本节只适用于在 WSL2 内运行 WebUI 的用户。** 原生 Windows 启动请看 +> 上一节"三、Windows 原生部署"——`start.ps1` 本身不自带开机自启 +> (Windows 登录时不会自动拉起 PowerShell 脚本),需要自启的话请改 +> 用 WSL 路径或自行配置 NSSM / Task Scheduler 启动 `start.ps1`。 + +官方 WSL 自启通道是 **Windows 任务计划程序 + WSL 启动脚本**,配套脚本在 [`scripts/windows/setup_webui_autostart.ps1`](../scripts/windows/setup_webui_autostart.ps1)。 它注册一个"登录时启动"的计划任务,通过 `wsl.exe` 在 WSL 发行版内执行启动脚本。 该脚本幂等:重复运行只更新已有任务,不会创建重复项。 @@ -206,24 +241,34 @@ $HOME/.hermes/webui/logs/hermes_webui.log --- -## 四、常见问题 +## 五、常见问题 | 症状 | 可能原因 | 处理 | |---|---|---| | 手机打不开地址 | WebUI 没绑定到可达接口 | 方案 A 检查 Serve 状态(`tailscale serve status`);方案 B 确认 `HERMES_WEBUI_HOST=0.0.0.0` 且防火墙已放行 Tailscale | | 任务计划已建但 WebUI 没起来 | WSL 脚本路径写错/发行版不对 | 用正确的 `-WslScriptPath` 与 `-Distro` 重跑注册脚本 | -| 打开 WSL 才启动,登录时不启动 | 用的是会话级自启而非计划任务 | 安装 Windows 计划任务 | +| 打开 WSL 才启动,登录时不启动 | 用的是会话级自启而非计划任务 | 按"四、WSL 用户:Windows 开机自启"装任务计划程序 | | 健康检查失败但进程存在 | 端口不一致或仍在启动 | 核对 `HERMES_WEBUI_PORT` 与 `hermes_webui.log` | | 提示需要密码但没设过 | 界面要求认证 | 设置 `HERMES_WEBUI_PASSWORD` 后重启 | --- -## 五、安全边界(重要) +## 六、安全边界(重要) **区分三种暴露范围:** 1. **Tailscale 私有(推荐)**:仅你的 tailnet 设备可达。Serve 方案连端口都不用开; - 直连 IP 方案也只需对 `100.64.0.0/10` 放行。WireGuard 加密 + 应用层密码认证。 + 直连 IP 方案("二、方案 B")则要看 `HERMES_WEBUI_HOST` 设的是什么: + + - **首选:** `HERMES_WEBUI_HOST=$(tailscale ip -4)`——只绑 Tailscale + 虚拟网卡,其他接口根本不开 8787,连防火墙关了也安全。 + - **次选:** `HERMES_WEBUI_HOST=0.0.0.0` + Windows 防火墙**只对 + `100.64.0.0/10` 放行**("三、3 防火墙")。"仅 tailnet 可达" + 这条结论完全靠这条防火墙规则兜底——**一旦放行规则被改回 + `Any`、或防火墙被关闭,WebUI 立刻对 LAN/公网开放**,因为 + `0.0.0.0` 是所有接口,不是只绑 Tailscale 网卡。 + + WireGuard 端到端加密 + 应用层密码认证是双层保险。 2. **局域网(LAN)**:`HERMES_WEBUI_HOST=0.0.0.0` 且防火墙对私有网段放行。 同一 WiFi 下的其他设备都能触达——密码认证是必需品,且不建议用于公共 WiFi。 3. **公网**:端口对互联网开放。**不推荐**。WebUI 本身只做简单的登录限速(每个 IP From 94539cf074488759a500efe4402ae0b47da2eadb Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Sat, 26 Sep 2026 05:01:24 +0000 Subject: [PATCH 21/22] docs(zh): correct the .env precedence note for start.ps1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The new "改完密码先验证一下" note said start.sh, start.ps1 and bootstrap.py all let repo-.env values override an inline HERMES_WEBUI_PASSWORD. That holds for start.sh (`set -a; source .env` runs before anything else), but start.ps1 skips any .env key that is already set in the environment (start.ps1:70-72), so on native Windows the `$env:HERMES_WEBUI_PASSWORD` shown in this section wins. start.ps1 runs server.py directly, so the suggested HERMES_WEBUI_PRESERVE_ENV=1 did not apply to it either. Keep the useful advice (check the login page actually asks for the new password, and look for a stale value in .env), state the correct precedence for each launcher, and drop the PRESERVE_ENV suggestion. Co-authored-by: happy5318 <34227488+happy5318@users.noreply.github.com> --- docs/remote-access-zh.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/remote-access-zh.md b/docs/remote-access-zh.md index b9d701c7..d4b37260 100644 --- a/docs/remote-access-zh.md +++ b/docs/remote-access-zh.md @@ -160,13 +160,13 @@ $env:HERMES_WEBUI_PASSWORD = "你的密码" > **绑定 `0.0.0.0` 前先设密码**:一旦绑定所有接口,任何能路由到该端口的设备 > 都能触达界面,密码认证是唯一防线。参见文末"安全边界"。 > -> **改完密码先验证一下**:`start.sh` / `start.ps1` / `bootstrap.py` 都会 -> 加载仓库根目录的 `.env`,且 `.env` 里的值会**覆盖**命令行内联的 -> `HERMES_WEBUI_PASSWORD=`。如果你以前在 `.env` 里写过旧密码、现在又 -> 想换成新的,单纯在命令行里 export 是没用的——`.env` 里的旧值会盖掉它。 -> 改完密码后用浏览器打开登录页确认**真的要求输入密码**,没有跳过直接 -> 进入主界面就说明 `.env` 还在用旧值,需要先清理或显式 -> `HERMES_WEBUI_PRESERVE_ENV=1`(仅 `bootstrap.py` 支持)再启动。 +> **改完密码先验证一下**:`start.ps1` 会加载仓库根目录的 `.env`,但 +> **已经设置的环境变量优先**——上面用 `$env:HERMES_WEBUI_PASSWORD` +> 设的值会生效,`.env` 里的同名项被跳过。Linux / WSL 下的 `start.sh` +> 恰好相反:它会先 `source` 一遍 `.env`,**`.env` 里的值会覆盖**命令行 +> 内联的 `HERMES_WEBUI_PASSWORD=`。无论用哪个启动脚本,改完密码后都用 +> 浏览器打开登录页,确认**真的要求输入密码**且新密码能登录;不对的话, +> 先检查仓库根目录的 `.env` 里是否还留着旧值。 ### 3. Windows 防火墙 From bac8a5205b6a98fbec77a380b0e4958abcde5df8 Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Sat, 26 Sep 2026 05:11:46 +0000 Subject: [PATCH 22/22] docs(changelog): stamp #7420 + #7814 --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fc39cd9..68ca289a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -447,6 +447,14 @@ ### Documentation +- **The README's remote-access paragraph now leads with Tailscale Serve.** It sent users straight to a + `HERMES_WEBUI_HOST=0.0.0.0` bind, which contradicted the guide it links to. It now recommends Serve, which + keeps WebUI on loopback behind tailnet-only HTTPS, and keeps the authenticated direct-IP bind as the + fallback when Serve is unavailable. (#7420 by @taljeon) +- **A Chinese remote-access guide.** `docs/remote-access-zh.md` covers Tailscale Serve, the direct tailnet-IP + fallback, SSH tunnels, a native-Windows setup with `start.ps1` (dependencies installed into the agent venv + that `start.ps1` actually uses, plus a Tailscale-only firewall rule), WSL-only login autostart, and the + security boundaries of each exposure level. The README links it. (#7814 by @happy5318) - **`AGENTS.md` now routes contributors to the references that match their change.** The old "read first" list asked for four files up front regardless of what was being changed, and carried a compressed copy of the ten change guidelines that `docs/GUIDELINES.md` owns. It now maps each reference to the kind of work it applies to and states explicit completion/verification criteria instead. No information is lost — the ten rules remain in `docs/GUIDELINES.md`, which the new version still points to. Thanks @steveafrost. (#7593) - **The `/api/models` cache invalidation contract is documented.** `#7556` shipped a change to the catalog cache's source fingerprint, and its review flagged the surrounding contract as undocumented runtime behavior. `docs/architecture/models-cache-invalidation.md` now records what is cached (in-memory snapshot, per-profile `models_cache.json`, cold vs hot path), the three source axes (`config_yaml` stat identity, `auth_json` content hash with a volatile-key deny-list, baked-in plus Codex catalog hashes) and why each is fingerprinted the way it is, and the invariant that both volatile-key sets are deny-lists that may only remove fields which provably do not gate the provider/model set. Changes no runtime behavior. Thanks @webtecnica. (#7560, #7556)