The docs-fact census wave re-read the live hosts and re-keyed every page it could: runtime/payload identities, ports, sizes, ledger figures, unit state, and the retirement banners that drifted as the fleet moved. The wave was interrupted mid-run, so this commit carries only what passed the integrity gates: every touched page parses (front matter or H1+status convention intact, balanced fences, no truncated tables, trailing newline), the docs_completeness and link-integrity seams are green, and bats is 384/384. Numbers that this session proved on live hosts are keyed to the measured value, not the page's earlier claim.
11 KiB
type, title, description, tags, timestamp
| type | title | description | tags | timestamp | |||||
|---|---|---|---|---|---|---|---|---|---|
| configuration | Hermes-WebUI (Hermex server) | The hermes-webui container on Dankclaw — the API server that the Hermex iPhone app talks to, reachable over an SSH tunnel, from the whole LAN (it binds all interfaces), and from https://hermes.franksdank.cloud (internal-only vhost). |
|
2026-09-14 |
Hermes-WebUI (Hermex server)
hermes-webui is the hermes-webui
server (MIT, third-party) that the Hermex iPhone app connects to. It runs
the Hermes agent in-process from the shared HERMES_HOME, so it is the same
agent — same memory, skills, and sessions — as the Hermes gateway and the
Hermes-Dashboard.
Host: Dankclaw — Port: 0.0.0.0:8787 (all interfaces; ss -ltnp
2026-10-18 shows the webui listening there in the host netns) — Ingress: SSH
tunnel for the phone app; direct LAN access also works (re-measured 2026-10-18
from another LAN host: /health → 200, / → 302) — Vhost:
hermes.franksdank.cloud, exempt by construction — no Anubis, no Authentik;
HERMES_WEBUI_PASSWORD is the only gate
Facts
| Item | Value |
|---|---|
| Image | localhost/hermes-agent:latest with Pull=never (dankclaw/hermes/quadlet/hermes-webui.container:35-36) — the same image as the gateway and dashboard. The deployer keeps that alias fresh by tagging the upstream name into local storage (podman tag docker.io/nousresearch/hermes-agent:latest localhost/hermes-agent:latest, scripts/deploy-remote.sh:1056); the installed image is the 2026-09-24 build, revision 749220e (podman image inspect 2026-10-18). |
| Command | python3 /webui/bootstrap.py --foreground (hermes-webui.container:48) |
| Source checkout | /home/dank/Containers/hermes/webui on Dankclaw, bind-mounted at /webui (hermes-webui.container:47; live dir drwxrwxr-x dank dank) |
| Port | 8787, bound to 0.0.0.0 in the host network namespace (Network=host + HERMES_WEBUI_HOST=0.0.0.0, hermes-webui.container:37-40) — LAN-reachable, not loopback-only |
| Auth | HERMES_WEBUI_PASSWORD (from /home/dank/serverAppConfigs/dankclaw/.env, the unit's only EnvironmentFile; Hermex signs in with it) |
| Data | bind mount /home/dank/Containers/hermes/data → /data, shared with the gateway and dashboard; anonymous volume at /opt/data |
| Health | HealthCmd= in the Quadlet unit probes http://127.0.0.1:8787/health from inside the unit (hermes-webui.container:53; works because the webui runs in the host netns). Container state 2026-10-18: healthy. There is no host-side probe in the deployer — the contract is the seam tests/hermes_webui.bats, which reads the live unit and pins the Caddyfile upstream |
Why a container, not a host process
The gateway's state files are owned by the container-mapped uid (109999,
from rootless Podman's 1 → 100000/65536 subuid range — /etc/subuid →
dank:100000:65536, and the agent's runtime uid is 10000, so 100000 + 9999 =
109999; ls -ld /home/dank/Containers/hermes/data → uid 109999, 2026-10-18). A fresh rootless
namespace on Dankclaw maps identically (verified with podman unshare cat /proc/self/uid_map), so a second container using the same image reads
and writes the shared volume with exactly the ownership the gateway already
uses. A host-side process running as dank (uid 1000) could not read the
gateway's 0600 state files (a non-privileged read of the data dir is
EACCES, checked 2026-10-18).
How it works
bootstrap.pycreates/webui/.venvon first boot, pip-installs the WebUI and agent requirements, then execsserver.pyas one foreground process (--foreground). systemd-free inside the container: supervision is the unit'sRestart=always/RestartSec=5(hermes-webui.container:60-61) — the compose-erarestart: unless-stoppedis retired withdankclaw/compose.yaml.HERMES_WEBUI_SKIP_ONBOARDING=1keeps the first-run wizard off (:45; live env confirms it,podman inspect hermes-webui2026-10-18); a profile already exists in/datafrom the gateway.- The healthcheck
start_periodis 600 s (HealthStartPeriod=600s,:57) because the very first boot spends minutes pip-installing into a fresh venv.
Remote access (Hermex on iOS — persistent SSH tunnel)
Only port 22 is forwarded to Dankclaw at the router (unverified 2026-10-18:
router configuration is not readable from this host; sshd -T confirms sshd
itself listens on port 22), so the phone reaches the
webui through an SSH local forward — no extra router ports, no third-party
relay (no Tailscale, no Cloudflare). Implemented 2026-08-31. The tunnel is an
access path for the phone, not a containment boundary: the server binds
0.0.0.0:8787, so any host that can route to 192.168.0.143 can reach the API
directly and needs only HERMES_WEBUI_PASSWORD to get in (verified 2026-09-02:
an unauthenticated GET /health from another LAN machine returned HTTP 200;
re-measured from a different LAN host on 2026-10-18: /health → 200, / → 302).
See Container Network Namespaces for why the webui is reachable this way and
the sibling dashboard on 9119 is not.
- Dedicated
tunnelaccount on Dankclaw (getent passwd tunnel→ uid 994, shell/usr/sbin/nologin, 2026-10-18; password locked). Authentication is key-only (unverified 2026-10-18:/home/tunnel/.ssh/authorized_keysis not readable withoutsudo); the account can do nothing except authenticate —ForceCommand /usr/sbin/nologinkills any session channel while the transport stays up for port forwarding. - sshd drop-in
/etc/ssh/sshd_config.d/99-tunnel.conf(exists, 118 B, 2026-08-31) scopesAllowTcpForwarding yesto that user only, plusPermitTTY no,X11Forwarding noandForceCommand /usr/sbin/nologin— all four lines are present underMatch User tunnel(cat2026-10-18). - Key options on the authorized key pin the blast radius:
command="/usr/sbin/nologin",no-pty,restrict,port-forwarding,permitopen="localhost:8787"— a leaked tunnel key can only forward tolocalhost:8787, never act as a general SOCKS/SSH proxy (unverified 2026-10-18: the authorized_keys file needssudoto read). - On the phone (client side): create a key on-device (private key never
leaves the device), hand the public key to the admin who appends it to
/home/tunnel/.ssh/authorized_keys, then connect from the SSH client:- In-network target
192.168.0.143:22, off-network target the public hostname (router forwards only port 22 → Dankclaw) — same forward config otherwise. - Local forward
127.0.0.1:8787 → localhost:8787; Hermex server URL is thenhttp://localhost:8787in both locations. - For a persistent tunnel on iOS, Secure ShellFish's personal-VPN mode
(Network Extension keeps the tunnel up and routes
127.0.0.0/8through it); Termius works but only while its session is connected — iOS allows no background SSH daemon.
- In-network target
graph LR
A[Hermex app] -->|http://localhost:8787| B[SSH local forward on iPhone<br/>persistent via personal VPN]
B ==>|encrypted SSH to port 22| C[Dankclaw sshd<br/>tunnel user, forced nologin]
C --> D["hermes-webui 0.0.0.0:8787 (host netns)"]
D --> E["/home/dank/Containers/hermes/data (shared agent state)"]
Deployment
./scripts/deploy.sh dankclaw --service hermes-webui
deploy_hermes (scripts/deploy-remote.sh:1034-1071; any of
--service hermes|hermes-dashboard|hermes-webui routes to it,
scripts/deploy-remote.sh:1594) copies chrome-devtools-protocol.container
plus the three trio units into the quadlet unit directory, starts the CDP daemon
only if inactive (it is never restarted on deploy, so in-flight browser
sessions survive), re-tags the image alias, then restarts
hermes.service, hermes-dashboard.service and
hermes-webui.service in the user manager of dank (linger must be on for
them to come up at boot — loginctl show-user dank -p Linger → yes,
2026-10-18), so gateway/dashboard/webui restarts stay isolated from
inference deploys.
Before first deploy, set a strong HERMES_WEBUI_PASSWORD in the live
dankclaw/.env.
Gotchas
- The
/webuimount is rw on purpose:bootstrap.pycreates.venvin the repo root. Do not flip it to:ro. - The webui binds
0.0.0.0:8787, and two consumers depend on that: the SSH tunnel (which dials the host'slocalhost) and Caddy, which frontshermes.franksdank.cloudwithreverse_proxy host.containers.internal:8787(dankclaw/Caddyfile:186-188) — netavark's host address (169.254.1.2), not the host's LAN IP. That distinction is load-bearing: this unit is host-networked and publishes nothing, so netavark DNATs nothing, and192.168.0.143:8787is REFUSED from Caddy's bridge netns whilehost.containers.internal:8787answers (measured inside the Caddy container 2026-09-16, recorded atdankclaw/Caddyfile:178-185and pinned by the seamtests/hermes_webui.bats:52-64, which fails any Caddyfile that reverts to the LAN IP). The dashboard's9119answers on the LAN IP precisely because it IS published. So the SSH tunnel is an access path for the phone, not a security boundary: any LAN host that can route to192.168.0.143reaches the API and needs onlyHERMES_WEBUI_PASSWORD. The webui can run agent commands, so treat that password as a credential with command-execution blast radius. The vhost itself carries no guard chain — no Anubis challenge, no Authentik gate (measured 2026-10-18 from another LAN host:https://hermes.franksdank.cloud/→ 302 →/login?next=/and/health→ 200, with no authentik or bot-challenge bytes in the body); it is private only because no public DNS record exists for the name (dankclaw/pihole/addn-hosts:39, LAN-only). To make loopback-only binding true again you must setHERMES_WEBUI_HOST=127.0.0.1and give Caddy another way in (a host-gateway route or a bridge-network unit) — that is an exposure change to decide deliberately, not a silent fix. It was tried on 2026-09-16 as a silent fix and broke the vhost with a 502 while the whole e2e suite stayed green;tests/e2e/specs/17-hermes-vhost.spec.tsnow asserts the vhost reaches its app (no 5xx, no Caddy error page, no gate page), so a dead upstream fails a gate instead of hiding. Its file comment still names the192.168.0.143:8787upstream and calls this "the only guarded vhost" — both are stale againstdankclaw/Caddyfile:186-188. - The webui and the gateway share one data root (
/datais a bind mount of/home/dank/Containers/hermes/data). A config change made on one side takes effect when the other restarts (same caveat as the dashboard). bootstrap.pyauto-installs a Hermes Agent if it cannot find one; in this image the agent is already present, so that path should never fire. If the log shows an install attempt, the image changed — re-verify the entrypoint.
Citations
- hermes-webui README (env vars,
--foreground,/health): https://github.com/nesquena/hermes-webui - Hermex setup model (server URL + password; Cloudflare/Tailscale are optional, not required): https://hermexapp.com/
See Also
- Hermes — the agent and its Signal gateway
- Hermes-Dashboard — the LAN browser management UI
- Dankclaw Stack
- Container Network Namespaces — why this host-netns service is LAN-reachable