Files
serverAppConfigs/serverAppConfigs-docs/Dankclaw/Hermes-WebUI.md
T
Frank a5fe045b58 docs(vault): one fact, one owner — repair the doubled pages, single-source the facts
Structural repair:
- 13 pages were the same document twice (each exactly 2x its HEAD size),
  a second frontmatter block whose closing fence had been glued onto a body
  line by a frontmatter rewrite that re-read its own output. Un-doubled by
  cutting at the second frontmatter and keeping the copy whose body is a
  superset; the orphan census reports 0 lost tokens against HEAD.
- log.md: one entry per date, bare `## YYYY-MM-DD` headings, newest first —
  66 dated headings merged into 34, titles demoted to bold leads, every
  content line verified present. The file had been descending at the head
  and ascending at the tail.

Ownership moves:
- New `LLM/frank-medium.md` is the locator for the retired Dankserver
  endpoint: era windows quoted from each era page's own banner, no numbers
  restated, and the two unresolved date conflicts recorded instead of
  silently resolved. `frank-archon.md` links to it instead of restating the
  pre-rename window.
- The four Pipeline-Parallelism stubs (frontmatter only, no body) are gone;
  `Multi-Node Runtimes.md` owns runtime construction, launch, mixed-vendor
  verdicts and cross-split spec decode, and the four links that pointed at
  the stubs were repointed at it or at the drafter page.
- Authentik's three stub pages fold into `Authentik.md`, with the process
  detail and the outpost reference split out so the parent stays under the
  length hint.

Verified corrections (host census + live probe, 2026-10-19):
- The MXFP4 checkpoint two pages called the "on-disk rollback" is gone:
  /home/dank/models/ holds StillDeadcode, rad and tpl.jinja. Stated once, on
  the era page that owns that payload; the swap page now links to it.
- frank-medium-radiance-mxfp4: the 4.24/7 accept rate belongs to the DFlash2
  arm measured on the FP8 target, not to the MXFP4 era; the 3-family soak run
  census is kept once (155 requests) instead of twice (~120 and 155).
- Fleet shapes re-confirmed against the live endpoints before touching any
  page: 3x200000 fp8 KV, 2x200704, 2x262144.

Index rows are regenerated, not hand-carved. Net -1,849 vault lines.
2026-10-19 05:00:43 -07:00

11 KiB

type, title, description, tags, timestamp
type title description tags timestamp
reference Hermes-WebUI (Hermex server) The iPhone companion app for Hermes on Dankclaw and the ways it is reached from the LAN.
hermes
hermes-webui
hermex
dankclaw
ssh-tunnel
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.py creates /webui/.venv on first boot, pip-installs the WebUI and agent requirements, then execs server.py as one foreground process (--foreground). systemd-free inside the container: supervision is the unit's Restart=always / RestartSec=5 (hermes-webui.container:60-61) — the compose-era restart: unless-stopped is retired with dankclaw/compose.yaml.
  • HERMES_WEBUI_SKIP_ONBOARDING=1 keeps the first-run wizard off (:45; live env confirms it, podman inspect hermes-webui 2026-10-18); a profile already exists in /data from the gateway.
  • The healthcheck start_period is 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 tunnel account 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_keys is not readable without sudo); the account can do nothing except authenticate — ForceCommand /usr/sbin/nologin kills 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) scopes AllowTcpForwarding yes to that user only, plus PermitTTY no, X11Forwarding no and ForceCommand /usr/sbin/nologin — all four lines are present under Match User tunnel (cat 2026-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 to localhost:8787, never act as a general SOCKS/SSH proxy (unverified 2026-10-18: the authorized_keys file needs sudo to 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 then http://localhost:8787 in 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/8 through it); Termius works but only while its session is connected — iOS allows no background SSH daemon.
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 /webui mount is rw on purpose: bootstrap.py creates .venv in 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's localhost) and Caddy, which fronts hermes.franksdank.cloud with reverse_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, and 192.168.0.143:8787 is REFUSED from Caddy's bridge netns while host.containers.internal:8787 answers (measured inside the Caddy container 2026-09-16, recorded at dankclaw/Caddyfile:178-185 and pinned by the seam tests/hermes_webui.bats:52-64, which fails any Caddyfile that reverts to the LAN IP). The dashboard's 9119 answers 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 to 192.168.0.143 reaches the API and needs only HERMES_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 set HERMES_WEBUI_HOST=127.0.0.1 and 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.ts now 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 the 192.168.0.143:8787 upstream and calls this "the only guarded vhost" — both are stale against dankclaw/Caddyfile:186-188.
  • The webui and the gateway share one data root (/data is 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.py auto-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

See Also