Merge pull request #3302 from nesquena/docs/markdown-refresh

docs: refresh markdown, regenerate contributor tally (137→194), README IA pass
This commit is contained in:
nesquena-hermes
2026-05-31 19:40:01 -07:00
committed by GitHub
8 changed files with 587 additions and 489 deletions
+46 -32
View File
@@ -7,10 +7,12 @@
>
> Keep this document updated as architecture changes are made.
> Current shipped build: `v0.51.54` (May 13, 2026).
> Automated coverage: 5303 tests via `pytest tests/ --collect-only -q`. CI runs on Python 3.11, 3.12, and 3.13 against every PR.
> Current shipped build: `v0.51.192` (May 31, 2026).
> Automated coverage: ~7,150 tests via `pytest tests/ --collect-only -q`. CI runs on
> Python 3.11, 3.12, and 3.13 (3 parallel shards each) against every PR, plus a ruff
> lint gate, a headless browser smoke test, and a Docker smoke test.
>
> Notable architecture state as of v0.51.54: the bootstrap and first-run onboarding flow own setup discovery; the default WebUI state directory is `~/.hermes/webui`; `ctl.sh` provides a daemon wrapper for homelab installs; chat streaming is still WebUI-owned SSE with stream-ownership guards, cancellation, async manual compression, and turn-journal audit plumbing; provider/model discovery is profile-aware with live-model cache invalidation and custom-provider scoping.
> Notable architecture state: the bootstrap and first-run onboarding flow own setup discovery; the default WebUI state directory is `~/.hermes/webui`; `ctl.sh` provides a daemon wrapper for homelab installs; chat streaming is still WebUI-owned SSE with stream-ownership guards, cancellation, async manual compression, and turn-journal audit plumbing; provider/model discovery is profile-aware with live-model cache invalidation and custom-provider scoping. (Version/test-count numbers above are a periodic snapshot — the authoritative source is the latest git tag and `pytest --collect-only`.)
---
@@ -43,51 +45,63 @@ actions. The topbar remains focused on conversation context and the workspace/fi
## 2. File Inventory
<repo>/
server.py Thin routing shell + HTTP Handler + auth middleware. ~446 lines.
server.py Thin routing shell + HTTP Handler + auth middleware.
Delegates all route handling to api/routes.py.
bootstrap.py One-shot launcher: optional agent install, deps, health wait, browser open.
start.sh Thin wrapper around bootstrap.py for shell-based startup.
Dockerfile python:3.12-slim container image (~89 lines)
docker-compose.yml Compose config with named volume and optional auth (~57 lines)
ctl.sh Daemon lifecycle wrapper (start/stop/restart/status/logs) for homelab installs.
pyproject.toml Tooling config (ruff lint gate). NOT a packaged distribution.
Dockerfile python:3.12-slim container image
docker-compose.yml Compose config with named volume and optional auth
.dockerignore Excludes .git, tests/, .env* from Docker builds
api/
__init__.py Package marker
auth.py Optional password authentication, signed cookies (~366 lines)
config.py Discovery, globals, model detection, reloadable config (~4139 lines)
helpers.py HTTP helpers: j(), bad(), require(), safe_resolve(), security headers (~302 lines)
models.py Session model + CRUD, per-session profile tracking (~1927 lines)
profiles.py Profile state management, hermes_cli wrapper (~1056 lines)
onboarding.py First-run onboarding status, real provider config writes, OAuth linking, and readiness detection (~1002 lines)
routes.py All GET + POST route handlers (~9772 lines)
startup.py Startup helpers: auto_install_agent_deps() (~128 lines)
streaming.py SSE engine, run_agent, cancel, HERMES_HOME save/restore (~4420 lines)
upload.py Multipart parser, file upload handler (~284 lines)
workspace.py File ops: list_dir, read_file_content, workspace helpers (~810 lines)
auth.py Optional password authentication, signed cookies, passkeys/WebAuthn
config.py Discovery, globals, model detection, reloadable config
helpers.py HTTP helpers: j(), bad(), require(), safe_resolve(), security headers
models.py Session model + CRUD, per-session profile tracking, CLI/state.db bridge
profiles.py Profile state management, hermes_cli wrapper
onboarding.py First-run onboarding status, real provider config writes, OAuth linking, readiness detection
routes.py All GET + POST route handlers (if/elif dispatch, no decorators)
startup.py Startup helpers: auto_install_agent_deps()
state_sync.py /insights sync — message_count to the agent's state.db
streaming.py SSE engine, run_agent, cancel, compression, HERMES_HOME save/restore
updates.py Self-update check and release notes
upload.py Multipart parser, file upload handler
workspace.py File ops: list_dir, read_file_content, git detection, workspace helpers
static/
index.html HTML template (~1323 lines)
style.css All CSS incl. mobile responsive (~3767 lines)
ui.js DOM helpers, renderMd, tool cards, model dropdown, file tree (~7216 lines)
workspace.js File preview, file ops, loadDir, clearPreview (~369 lines)
sessions.js Session CRUD, list rendering, search, SVG icons, dropdown actions (~3517 lines)
messages.js send(), SSE event handlers, approval, transcript (~2301 lines)
panels.js Cron, skills, memory, workspace, profiles, todo, settings (~6480 lines)
commands.js Slash command registry, parser, autocomplete dropdown (~1302 lines)
onboarding.js First-run wizard overlay, provider setup flow, and settings/workspace orchestration.
boot.js Event wiring, mobile sidebar/workspace nav, voice input, boot IIFE (~1607 lines)
index.html HTML template
style.css All CSS incl. mobile responsive, themes + skins, KaTeX
ui.js DOM helpers, renderMd, tool cards, context indicator, file tree
workspace.js File preview, file ops, git badge, central api() fetch wrapper
sessions.js Session CRUD, list rendering, collapsible groups, search, SSE sync
messages.js send(), SSE event handlers, approval/clarify, transcript, recovery
panels.js Cron, skills, memory, profiles, todo, settings (Control Center)
commands.js Slash command registry, parser, autocomplete dropdown
boot.js Event wiring, mobile nav, voice input, theme/skin boot, bfcache handler
onboarding.js First-run wizard overlay, provider setup flow
i18n.js Localization catalog (en, es, de, zh, zh-Hant, ru, …)
login.js Login page + open-redirect guard
icons.js Lucide icon path registry
sw.js Service worker: offline shell cache, version-pinned assets
tests/
conftest.py Isolated test server/state fixtures (~644 lines)
488 test files 5303 tests collected via pytest
test_regressions.py Permanent regression gate (~976 lines)
conftest.py Isolated test server/state fixtures
~700 test files ~7,150 tests collected via pytest (run `pytest --collect-only -q` for exact)
test_regressions.py Permanent regression gate
CONTRIBUTING.md Contributor workflow and PR expectations.
ROADMAP.md Feature and product roadmap document.
SPRINTS.md Forward sprint plan with CLI + Claude parity targets.
ARCHITECTURE.md THIS FILE.
TESTING.md Manual browser test plan and automated coverage reference.
CHANGELOG.md Release notes per sprint.
BUGS.md Bug backlog and fixed items tracker.
CHANGELOG.md Release notes per version.
CONTRIBUTORS.md Community credit roll (regenerated via the maintainer workspace script).
requirements.txt Python dependencies.
.env.example Sample environment variable overrides.
> Per-file line counts intentionally omitted — they drift every release. Use
> `git ls-files | xargs wc -l` (or your editor) for current sizes; the role of
> each file above is the durable part.
State directory (runtime data, separate from source):
~/.hermes/webui/
+48 -41
View File
File diff suppressed because one or more lines are too long
+315 -403
View File
@@ -45,6 +45,20 @@ This gives you nearly **1:1 parity with Hermes CLI from a convenient web UI** wh
---
## Contents
- [Why Hermes](#why-hermes) — what it is and how it compares
- [Quick start](#quick-start) — clone + `bootstrap.py` / `start.sh` / `ctl.sh`
- [Features](#features) — chat, sessions, workspace, voice, profiles, security, themes, panels, mobile
- [Configuration & access](#configuration--access) — auto-discovery, overrides, remote/Tailscale/phone, manual launch
- [Docker](#docker) — single- and multi-container deploys
- [Running tests](#running-tests)
- [Architecture](#architecture) — backend/frontend layout, state dir
- [Docs](#docs) — the full documentation index
- [Contributors](#contributors)
---
## Why Hermes
Most AI tools reset every session. They don't know who you are, what you worked on, or what
@@ -121,85 +135,9 @@ For self-hosted VM or homelab installs, `ctl.sh` wraps the common daemon lifecyc
`ctl.sh start` runs the bootstrap in foreground/no-browser mode behind the daemon wrapper, writes logs to `~/.hermes/webui.log`, and respects `.env` plus inline overrides such as `HERMES_WEBUI_HOST=0.0.0.0 ./ctl.sh start`.
### Optional session recall prefill
### Advanced: dynamic recall prefill & Gateway-backed chat
WebUI can attach ephemeral prefill messages to new browser-originated
agent turns. This is useful when a deployment already has a local recall or
router script for Joplin, Obsidian, Notion, llm-wiki, or another third-party
notes source and wants browser chat to know where durable context lives.
Prefer a compact router-style prefill (for example, "Joplin has the durable
project context; use the available notes/search tools before answering
detail-dependent questions") instead of dumping the full note corpus into every
new browser session. The prefill should point the agent toward retrieval; the
notes/search tools should provide the specific facts on demand.
Static JSON remains supported through `prefill_messages_file` or
`HERMES_PREFILL_MESSAGES_FILE`. For dynamic recall, opt in explicitly with a
WebUI-specific script hook:
```yaml
webui_prefill_messages_script:
- python3
- /path/to/notes_recall.py
webui_prefill_messages_script_timeout: 5
```
or:
```bash
HERMES_WEBUI_PREFILL_MESSAGES_SCRIPT="python3 /path/to/notes_recall.py" \
HERMES_WEBUI_PREFILL_MESSAGES_SCRIPT_TIMEOUT=5 \
./ctl.sh restart
```
The script may print either an OpenAI-style JSON message list, a JSON object with
a `messages` list, or plain text; plain text is wrapped as one `user` prefill
message so dynamic recall text becomes ordinary context instead of an extra
system instruction. If the hook must provide system-level guidance, emit JSON
messages with an explicit `role: "system"` entry instead. Script output is capped
at 256 KiB before parsing. Parsed prefill context is then bounded by
`webui_prefill_context_max_chars` or `HERMES_WEBUI_PREFILL_CONTEXT_MAX_CHARS`
(default: 12,000 characters; set to `0` to disable). When a dynamic script
exceeds the budget and a compact static prefill file is configured, WebUI falls
back to that file. If no compact fallback is available, WebUI injects a short
retrieval instruction instead of sending the oversized note/body payload with
every new browser turn. The browser only receives a compact status event
(`source`, `label`, message count, compaction metadata, and redacted errors),
never the prefill message bodies.
### Optional Gateway-backed browser chat
By default, browser chat runs through WebUI's in-process legacy runtime. Advanced
self-hosted deployments can opt into routing new browser turns through a running
Hermes Gateway API server while preserving the existing WebUI `/api/chat/start`
and `/api/chat/stream` browser contract:
```bash
HERMES_WEBUI_CHAT_BACKEND=gateway \
HERMES_WEBUI_GATEWAY_BASE_URL=http://127.0.0.1:8642 \
HERMES_WEBUI_GATEWAY_API_KEY=... \
./ctl.sh restart
```
`HERMES_WEBUI_CHAT_BACKEND` is intentionally strict: only `gateway`,
`api_server`, or `api-server` enable the bridge. Generic truthy values such as
`1` or `true` are ignored so existing deployments do not change execution
ownership accidentally. If `HERMES_WEBUI_GATEWAY_API_KEY` is omitted, WebUI falls
back to `API_SERVER_KEY` when present. When Gateway returns HTTP 401, WebUI
reports a `gateway_auth_error` that points at this WebUI↔Gateway key mismatch
rather than showing the Gateway's generic provider-style "Invalid API key" body.
`/api/health/agent` also includes a redacted `gateway_chat` block so operators can
see whether gateway mode, base URL, and API-key presence are configured without
exposing the key value. That `gateway_chat` field is an operator diagnostic
payload only; it is not currently rendered as a user-facing health banner in the
browser UI.
The bridge is best used by operators who already run Hermes Gateway/API Server
locally and want browser-originated chat to use the same runtime/tool path as
messaging surfaces. Attachments, cancellation, approvals, and clarify prompts
still follow WebUI's current compatibility path and may not match every messaging
surface until the runtime-adapter migration is complete.
Two optional, self-hosted-deployment features — attaching dynamic **session-recall prefill** to browser turns (Joplin/Obsidian/Notion/llm-wiki routers), and routing browser chat through a running **Hermes Gateway** — are documented in [`docs/advanced-chat-setup.md`](docs/advanced-chat-setup.md). Most users need neither.
The bootstrap will:
@@ -226,264 +164,6 @@ If an AI assistant is helping with install, reinstall, bootstrap, provider setup
---
## Docker
**Pre-built images** (amd64 + arm64) are published to GHCR on every release.
For a comprehensive setup guide covering all 3 compose files, common failure modes, and bind-mount migration, see [`docs/docker.md`](docs/docker.md). The README covers the 5-minute happy path.
### 5-minute quickstart (single container)
The simplest setup: one WebUI container that runs the agent in-process.
```bash
git clone https://github.com/nesquena/hermes-webui
cd hermes-webui
cp .env.docker.example .env
# Edit .env if your host UID isn't 1000 (e.g. macOS where UIDs start at 501)
docker compose up -d
# Open http://localhost:8787
```
Run Compose as the user who owns your Hermes home. `sudo docker compose up -d` can make `${HOME}` expand to the root user's home, so Docker mounts the wrong `.hermes` directory instead of your real `~/.hermes` and the WebUI starts with `config.yaml (not found, using defaults)`. Prefer adding your user to the Docker group and running `docker compose up -d`; if you must use sudo, set absolute paths first, for example `HERMES_HOME=/home/you/.hermes HERMES_WORKSPACE=/home/you/workspace sudo -E docker compose up -d`, then verify with `docker compose config`.
The container auto-detects your UID/GID from the mounted `~/.hermes` volume so files written by the agent stay readable by you on the host.
To enable password protection (required if you expose the port outside `127.0.0.1`):
```bash
echo "HERMES_WEBUI_PASSWORD=change-me-to-something-strong" >> .env
docker compose up -d --force-recreate
```
### Manual `docker run` (no compose)
```bash
docker pull ghcr.io/nesquena/hermes-webui:latest
docker run -d \
-e WANTED_UID=$(id -u) -e WANTED_GID=$(id -g) \
-v ~/.hermes:/home/hermeswebui/.hermes \
-e HERMES_WEBUI_STATE_DIR=/home/hermeswebui/.hermes/webui \
-v ~/workspace:/workspace \
-p 127.0.0.1:8787:8787 \
ghcr.io/nesquena/hermes-webui:latest
```
### Build locally
```bash
docker build -t hermes-webui .
docker run -d \
-e WANTED_UID=$(id -u) -e WANTED_GID=$(id -g) \
-v ~/.hermes:/home/hermeswebui/.hermes \
-e HERMES_WEBUI_STATE_DIR=/home/hermeswebui/.hermes/webui \
-v ~/workspace:/workspace \
-p 127.0.0.1:8787:8787 \
hermes-webui
```
### Multi-container setups
If you want the agent and WebUI in separate containers (for isolation, or because you're already running an agent gateway elsewhere):
```bash
# Agent + WebUI
docker compose -f docker-compose.two-container.yml up -d
# Agent + Dashboard + WebUI
docker compose -f docker-compose.three-container.yml up -d
```
Both compose files use **named Docker volumes** by default, which solves the UID/GID problem by construction. If you need bind mounts to share an existing host directory, see [`docs/docker.md`](docs/docker.md) for the full migration recipe.
> **Known limitation (#681)**: in the two-container setup, tools triggered from the WebUI run in the **WebUI container**, not the agent container. If you need git/node/etc. on the WebUI's filesystem, either use the single-container setup, extend the WebUI Dockerfile, or use the community [all-in-one image](https://github.com/sunnysktsang/hermes-suite).
>
> **Source boundary note (#2453)**: the multi-container setup mounts `hermes-agent-src` read-only into the WebUI by default. This prevents WebUI-side source rewrites but is still an implementation-coupling bridge, not a stable Agent API boundary. See [`docs/rfcs/agent-source-boundary.md`](docs/rfcs/agent-source-boundary.md) for the current source/API decoupling inventory.
### Common failure modes
| Symptom | Likely cause | Fix |
|---|---|---|
| `PermissionError` at startup | UID mismatch on bind mount | Set `UID=$(id -u)` in `.env` |
| `.env: permission denied` (#1389) | `fix_credential_permissions()` enforced 0600 | Set `HERMES_SKIP_CHMOD=1` in `.env` |
| Workspace appears empty | UID mismatch on `/workspace` mount | Set `UID=$(id -u)` in `.env` |
| `git: command not found` in chat | Two-container architectural limit (#681) | Use single-container or extend Dockerfile |
| WebUI can't find agent source | `hermes-agent-src` volume misconfigured | Use the named volumes from compose files as-is |
| Podman shared `.hermes` fails | Podman 3.4 `keep-id` limitation | Use Podman 4+ or single-container |
| Host API at `localhost` fails from WebUI | Container `localhost` means the container, not your host (#3012) | Use `http://host.docker.internal:<port>` on Docker Desktop, or `http://host.containers.internal:<port>` on Podman |
| WebUI can't see `~/.hermes` after `sudo docker compose` | `${HOME}` expanded to the root user's home (#3006) | Run Compose as your user, or pass absolute `HERMES_HOME`/`HERMES_WORKSPACE` with `sudo -E` |
For the deep dive on each of these, see [`docs/docker.md`](docs/docker.md).
> **Note:** By default, Docker Compose binds to `127.0.0.1` (localhost only).
> To expose on a network, change the port to `"8787:8787"` in `docker-compose.yml`
> and set `HERMES_WEBUI_PASSWORD` to enable authentication.
---
## What start.sh discovers automatically
| Thing | How it finds it |
|---|---|
| Hermes agent dir | `HERMES_WEBUI_AGENT_DIR` env, then `$HERMES_HOME/hermes-agent` (Windows default `%LOCALAPPDATA%\hermes\hermes-agent`, POSIX default `~/.hermes/hermes-agent`), then sibling `../hermes-agent` |
| Python executable | Agent venv first, then `.venv` in this repo, then system `python3` |
| State directory | `HERMES_WEBUI_STATE_DIR` env, then `$HERMES_HOME/webui` (Windows default `%LOCALAPPDATA%\hermes\webui`, POSIX default `~/.hermes/webui`) |
| Default workspace | `HERMES_WEBUI_DEFAULT_WORKSPACE` env, then `~/workspace`, then state dir |
| Port | `HERMES_WEBUI_PORT` env or first argument, default `8787` |
If discovery finds everything, nothing else is required.
---
## Overrides (only needed if auto-detection misses)
```bash
export HERMES_WEBUI_AGENT_DIR=/path/to/hermes-agent
export HERMES_WEBUI_PYTHON=/path/to/python
export HERMES_WEBUI_PORT=9000
export HERMES_WEBUI_AUTO_INSTALL=1 # enable auto-install of agent deps (disabled by default)
./start.sh
```
Or inline:
```bash
HERMES_WEBUI_AGENT_DIR=/custom/path ./start.sh 9000
```
Full list of environment variables:
| Variable | Default | Description |
|---|---|---|
| `HERMES_WEBUI_AGENT_DIR` | auto-discovered | Path to the hermes-agent checkout |
| `HERMES_WEBUI_PYTHON` | auto-discovered | Python executable |
| `HERMES_WEBUI_HOST` | `127.0.0.1` | Bind address (`0.0.0.0` for all IPv4, `::` for all IPv6, `::1` for IPv6 loopback) |
| `HERMES_WEBUI_PORT` | `8787` | Port |
| `HERMES_WEBUI_STATE_DIR` | `$HERMES_HOME/webui` (Windows default `%LOCALAPPDATA%\hermes\webui`, POSIX default `~/.hermes/webui`) | Where sessions and state are stored |
| `HERMES_WEBUI_DEFAULT_WORKSPACE` | `~/workspace` | Default workspace |
| `HERMES_WEBUI_DEFAULT_MODEL` | *(provider default)* | Optional model override; leave unset to use the active Hermes provider default |
| `HERMES_WEBUI_PASSWORD` | *(unset)* | Set to enable password authentication |
| `HERMES_WEBUI_CSP_CONNECT_EXTRA` | *(unset)* | Optional space-separated `http(s)://` or `ws(s)://` origins to append to the report-only CSP `connect-src` directive for reverse-proxy or tunnel deployments |
| `HERMES_WEBUI_EXTENSION_DIR` | *(unset)* | Optional local directory served at `/extensions/`; must point to an existing directory before extension injection is enabled |
| `HERMES_WEBUI_EXTENSION_SCRIPT_URLS` | *(unset)* | Optional comma-separated same-origin script URLs to inject; see [WebUI Extensions](docs/EXTENSIONS.md) |
| `HERMES_WEBUI_EXTENSION_STYLESHEET_URLS` | *(unset)* | Optional comma-separated same-origin stylesheet URLs to inject; see [WebUI Extensions](docs/EXTENSIONS.md) |
| `HERMES_HOME` | Windows: `%LOCALAPPDATA%\hermes`; POSIX: `~/.hermes` | Base directory for Hermes state (affects all paths) |
| `HERMES_CONFIG_PATH` | `$HERMES_HOME/config.yaml` | Path to Hermes config file |
---
## Accessing from a remote machine
The server binds to `127.0.0.1` by default (loopback only). If you are running
Hermes on a VPS or remote server, use an SSH tunnel from your local machine:
```bash
ssh -N -L <local-port>:127.0.0.1:<remote-port> <user>@<server-host>
```
Example:
```bash
ssh -N -L 8787:127.0.0.1:8787 user@your.server.com
```
Then open `http://localhost:8787` in your local browser.
`start.sh` will print this command for you automatically when it detects you
are running over SSH.
---
## Accessing on your phone with Tailscale
[Tailscale](https://tailscale.com) is a zero-config mesh VPN built on
WireGuard. Install it on your server and your phone, and they join the same
private network -- no port forwarding, no SSH tunnels, no public exposure.
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:**
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:
```bash
HERMES_WEBUI_HOST=0.0.0.0 HERMES_WEBUI_PASSWORD=your-secret ./start.sh
```
3. Open `http://<server-tailscale-ip>:8787` in your phone's browser
(find your server's Tailscale 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.
### Community field report: ARM64 Android via AVF
A community report in [#2364](https://github.com/nesquena/hermes-webui/issues/2364)
documents Hermes Agent + WebUI running on a mid-range ARM64 Android phone inside
a Debian 12 VM via Android Virtualization Framework (AVF). The reported setup
used a Xiaomi Redmi Note 13 Pro 4G, 3.8 GiB RAM allocated to the VM, 8 visible
CPU cores, Chrome on Android at `localhost:8787`, and cloud-hosted inference.
This is not an official support baseline or provider/model benchmark, but it is
a useful compatibility signal for mobile ARM64 experiments: the WebUI rendered
smoothly in Chrome, ARM64 Debian worked for the agent stack, and the total local
footprint was about 1.7 GB. Practical caveats from the report: first install can
take longer when dependencies compile from source, Android browser tabs may
reload when switching apps, and disabling battery optimization for the terminal
or VM host may be needed for longer-running sessions.
> **Tip:** If using Docker, set `HERMES_WEBUI_HOST=0.0.0.0` in your
> `docker-compose.yml` environment (already the default) and set
> `HERMES_WEBUI_PASSWORD`.
---
## Manual launch (without start.sh)
If you prefer to launch the server directly:
```bash
cd /path/to/hermes-agent # or wherever sys.path can find Hermes modules
HERMES_WEBUI_PORT=8787 venv/bin/python /path/to/hermes-webui/server.py
```
Note: use the agent venv Python (or any Python environment that has the Hermes agent dependencies installed). System Python will be missing `openai`, `httpx`, and other required packages.
Health check:
```bash
curl http://127.0.0.1:8787/health
```
---
## Running tests
Tests discover the repo and the Hermes agent dynamically -- no hardcoded paths.
```bash
cd hermes-webui
pytest tests/ -v --timeout=60
```
Or using the agent venv explicitly:
```bash
/path/to/hermes-agent/venv/bin/python -m pytest tests/ -v
```
Tests run against an isolated server with a separate state directory.
Production data and real cron jobs are never touched. Current snapshot:
**5303 tests collected** across **488 test files**.
---
## Features
### Chat and agent
@@ -605,110 +285,340 @@ Production data and real cron jobs are never touched. Current snapshot:
---
## Configuration & access
`start.sh` auto-detects almost everything; the subsections below cover the knobs for when it can't, and how to reach the UI remotely.
### What start.sh discovers automatically
| Thing | How it finds it |
|---|---|
| Hermes agent dir | `HERMES_WEBUI_AGENT_DIR` env, then `$HERMES_HOME/hermes-agent` (Windows default `%LOCALAPPDATA%\hermes\hermes-agent`, POSIX default `~/.hermes/hermes-agent`), then sibling `../hermes-agent` |
| Python executable | Agent venv first, then `.venv` in this repo, then system `python3` |
| State directory | `HERMES_WEBUI_STATE_DIR` env, then `$HERMES_HOME/webui` (Windows default `%LOCALAPPDATA%\hermes\webui`, POSIX default `~/.hermes/webui`) |
| Default workspace | `HERMES_WEBUI_DEFAULT_WORKSPACE` env, then `~/workspace`, then state dir |
| Port | `HERMES_WEBUI_PORT` env or first argument, default `8787` |
If discovery finds everything, nothing else is required.
---
### Overrides (only needed if auto-detection misses)
```bash
export HERMES_WEBUI_AGENT_DIR=/path/to/hermes-agent
export HERMES_WEBUI_PYTHON=/path/to/python
export HERMES_WEBUI_PORT=9000
export HERMES_WEBUI_AUTO_INSTALL=1 # enable auto-install of agent deps (disabled by default)
./start.sh
```
Or inline:
```bash
HERMES_WEBUI_AGENT_DIR=/custom/path ./start.sh 9000
```
Full list of environment variables:
| Variable | Default | Description |
|---|---|---|
| `HERMES_WEBUI_AGENT_DIR` | auto-discovered | Path to the hermes-agent checkout |
| `HERMES_WEBUI_PYTHON` | auto-discovered | Python executable |
| `HERMES_WEBUI_HOST` | `127.0.0.1` | Bind address (`0.0.0.0` for all IPv4, `::` for all IPv6, `::1` for IPv6 loopback) |
| `HERMES_WEBUI_PORT` | `8787` | Port |
| `HERMES_WEBUI_STATE_DIR` | `$HERMES_HOME/webui` (Windows default `%LOCALAPPDATA%\hermes\webui`, POSIX default `~/.hermes/webui`) | Where sessions and state are stored |
| `HERMES_WEBUI_DEFAULT_WORKSPACE` | `~/workspace` | Default workspace |
| `HERMES_WEBUI_DEFAULT_MODEL` | *(provider default)* | Optional model override; leave unset to use the active Hermes provider default |
| `HERMES_WEBUI_PASSWORD` | *(unset)* | Set to enable password authentication |
| `HERMES_WEBUI_CSP_CONNECT_EXTRA` | *(unset)* | Optional space-separated `http(s)://` or `ws(s)://` origins to append to the report-only CSP `connect-src` directive for reverse-proxy or tunnel deployments |
| `HERMES_WEBUI_EXTENSION_DIR` | *(unset)* | Optional local directory served at `/extensions/`; must point to an existing directory before extension injection is enabled |
| `HERMES_WEBUI_EXTENSION_SCRIPT_URLS` | *(unset)* | Optional comma-separated same-origin script URLs to inject; see [WebUI Extensions](docs/EXTENSIONS.md) |
| `HERMES_WEBUI_EXTENSION_STYLESHEET_URLS` | *(unset)* | Optional comma-separated same-origin stylesheet URLs to inject; see [WebUI Extensions](docs/EXTENSIONS.md) |
| `HERMES_HOME` | Windows: `%LOCALAPPDATA%\hermes`; POSIX: `~/.hermes` | Base directory for Hermes state (affects all paths) |
| `HERMES_CONFIG_PATH` | `$HERMES_HOME/config.yaml` | Path to Hermes config file |
---
### 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://<server-tailscale-ip>: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).
### Manual launch (without start.sh)
If you prefer to launch the server directly:
```bash
cd /path/to/hermes-agent # or wherever sys.path can find Hermes modules
HERMES_WEBUI_PORT=8787 venv/bin/python /path/to/hermes-webui/server.py
```
Note: use the agent venv Python (or any Python environment that has the Hermes agent dependencies installed). System Python will be missing `openai`, `httpx`, and other required packages.
Health check:
```bash
curl http://127.0.0.1:8787/health
```
---
## Docker
**Pre-built images** (amd64 + arm64) are published to GHCR on every release.
For a comprehensive setup guide covering all 3 compose files, common failure modes, and bind-mount migration, see [`docs/docker.md`](docs/docker.md). The README covers the 5-minute happy path.
### 5-minute quickstart (single container)
The simplest setup: one WebUI container that runs the agent in-process.
```bash
git clone https://github.com/nesquena/hermes-webui
cd hermes-webui
cp .env.docker.example .env
# Edit .env if your host UID isn't 1000 (e.g. macOS where UIDs start at 501)
docker compose up -d
# Open http://localhost:8787
```
Run Compose as the user who owns your Hermes home. `sudo docker compose up -d` can make `${HOME}` expand to the root user's home, so Docker mounts the wrong `.hermes` directory instead of your real `~/.hermes` and the WebUI starts with `config.yaml (not found, using defaults)`. Prefer adding your user to the Docker group and running `docker compose up -d`; if you must use sudo, set absolute paths first, for example `HERMES_HOME=/home/you/.hermes HERMES_WORKSPACE=/home/you/workspace sudo -E docker compose up -d`, then verify with `docker compose config`.
The container auto-detects your UID/GID from the mounted `~/.hermes` volume so files written by the agent stay readable by you on the host.
To enable password protection (required if you expose the port outside `127.0.0.1`):
```bash
echo "HERMES_WEBUI_PASSWORD=change-me-to-something-strong" >> .env
docker compose up -d --force-recreate
```
### Manual `docker run` (no compose)
```bash
docker pull ghcr.io/nesquena/hermes-webui:latest
docker run -d \
-e WANTED_UID=$(id -u) -e WANTED_GID=$(id -g) \
-v ~/.hermes:/home/hermeswebui/.hermes \
-e HERMES_WEBUI_STATE_DIR=/home/hermeswebui/.hermes/webui \
-v ~/workspace:/workspace \
-p 127.0.0.1:8787:8787 \
ghcr.io/nesquena/hermes-webui:latest
```
### Build locally
```bash
docker build -t hermes-webui .
docker run -d \
-e WANTED_UID=$(id -u) -e WANTED_GID=$(id -g) \
-v ~/.hermes:/home/hermeswebui/.hermes \
-e HERMES_WEBUI_STATE_DIR=/home/hermeswebui/.hermes/webui \
-v ~/workspace:/workspace \
-p 127.0.0.1:8787:8787 \
hermes-webui
```
### Multi-container setups
If you want the agent and WebUI in separate containers (for isolation, or because you're already running an agent gateway elsewhere):
```bash
# Agent + WebUI
docker compose -f docker-compose.two-container.yml up -d
# Agent + Dashboard + WebUI
docker compose -f docker-compose.three-container.yml up -d
```
Both compose files use **named Docker volumes** by default, which solves the UID/GID problem by construction. If you need bind mounts to share an existing host directory, see [`docs/docker.md`](docs/docker.md) for the full migration recipe.
> **Known limitation (#681)**: in the two-container setup, tools triggered from the WebUI run in the **WebUI container**, not the agent container. If you need git/node/etc. on the WebUI's filesystem, either use the single-container setup, extend the WebUI Dockerfile, or use the community [all-in-one image](https://github.com/sunnysktsang/hermes-suite).
>
> **Source boundary note (#2453)**: the multi-container setup mounts `hermes-agent-src` read-only into the WebUI by default. This prevents WebUI-side source rewrites but is still an implementation-coupling bridge, not a stable Agent API boundary. See [`docs/rfcs/agent-source-boundary.md`](docs/rfcs/agent-source-boundary.md) for the current source/API decoupling inventory.
### Common failure modes
| Symptom | Likely cause | Fix |
|---|---|---|
| `PermissionError` at startup | UID mismatch on bind mount | Set `UID=$(id -u)` in `.env` |
| `.env: permission denied` (#1389) | `fix_credential_permissions()` enforced 0600 | Set `HERMES_SKIP_CHMOD=1` in `.env` |
| Workspace appears empty | UID mismatch on `/workspace` mount | Set `UID=$(id -u)` in `.env` |
| `git: command not found` in chat | Two-container architectural limit (#681) | Use single-container or extend Dockerfile |
| WebUI can't find agent source | `hermes-agent-src` volume misconfigured | Use the named volumes from compose files as-is |
| Podman shared `.hermes` fails | Podman 3.4 `keep-id` limitation | Use Podman 4+ or single-container |
| Host API at `localhost` fails from WebUI | Container `localhost` means the container, not your host (#3012) | Use `http://host.docker.internal:<port>` on Docker Desktop, or `http://host.containers.internal:<port>` on Podman |
| WebUI can't see `~/.hermes` after `sudo docker compose` | `${HOME}` expanded to the root user's home (#3006) | Run Compose as your user, or pass absolute `HERMES_HOME`/`HERMES_WORKSPACE` with `sudo -E` |
For the deep dive on each of these, see [`docs/docker.md`](docs/docker.md).
> **Note:** By default, Docker Compose binds to `127.0.0.1` (localhost only).
> To expose on a network, change the port to `"8787:8787"` in `docker-compose.yml`
> and set `HERMES_WEBUI_PASSWORD` to enable authentication.
---
## Running tests
Tests discover the repo and the Hermes agent dynamically -- no hardcoded paths.
```bash
cd hermes-webui
pytest tests/ -v --timeout=60
```
Or using the agent venv explicitly:
```bash
/path/to/hermes-agent/venv/bin/python -m pytest tests/ -v
```
Tests run against an isolated server with a separate state directory.
Production data and real cron jobs are never touched. Current snapshot:
**~7,150 tests collected** across **~700 test files**, run in CI on Python 3.11,
3.12, and 3.13 (3 parallel shards each).
---
## Architecture
No build step, no framework, no bundler — a Python standard-library HTTP server
and vanilla JS. The backend lives in `api/`, the frontend in `static/`.
**Backend (`api/`)**
```
server.py HTTP routing shell + auth middleware (~446 lines)
server.py HTTP routing shell + auth middleware
api/
auth.py Optional password authentication, signed cookies (~366 lines)
config.py Discovery, globals, model detection, reloadable config (~4139 lines)
helpers.py HTTP helpers, security headers (~302 lines)
models.py Session model + CRUD + CLI bridge (~1927 lines)
onboarding.py First-run onboarding wizard, OAuth provider support (~1002 lines)
profiles.py Profile state management, hermes_cli wrapper (~1056 lines)
routes.py All GET + POST route handlers (~9772 lines)
state_sync.py /insights sync — message_count to state.db (~118 lines)
streaming.py SSE engine, run_agent, cancel support (~4420 lines)
updates.py Self-update check and release notes (~545 lines)
upload.py Multipart parser, file upload handler (~284 lines)
workspace.py File ops, workspace helpers, git detection (~810 lines)
static/
index.html HTML template (~1323 lines)
style.css All CSS incl. mobile responsive, themes (~3767 lines)
ui.js DOM helpers, renderMd, tool cards, context indicator (~7216 lines)
workspace.js File preview, file ops, git badge (~369 lines)
sessions.js Session CRUD, collapsible groups, search, reload recovery (~3517 lines)
messages.js send(), SSE handlers, live streaming, session recovery (~2301 lines)
panels.js Cron, skills, memory, profiles, settings (~6480 lines)
commands.js Slash command autocomplete (~1302 lines)
boot.js Mobile nav, voice input, boot IIFE (~1607 lines)
tests/
conftest.py Isolated test server/state fixtures
488 test files 5303 tests collected
Dockerfile python:3.12-slim container image
docker-compose.yml Compose with named volume and optional auth
.github/workflows/ CI: multi-arch Docker build + GitHub Release on tag
auth.py Optional password authentication, signed cookies, passkeys
config.py Discovery, globals, model detection, reloadable config
helpers.py HTTP helpers, security headers
models.py Session model + CRUD + CLI/state.db bridge
onboarding.py First-run onboarding wizard, OAuth provider support
profiles.py Profile state management, hermes_cli wrapper
routes.py All GET + POST route handlers (if/elif dispatch, no decorators)
state_sync.py /insights sync — message_count to state.db
streaming.py SSE engine, run_agent, cancellation, compression
updates.py Self-update check and release notes
upload.py Multipart parser, file upload handler
workspace.py File ops, workspace helpers, git detection
```
**Frontend (`static/`)**
```
index.html HTML template
style.css All CSS incl. mobile responsive, themes + skins
ui.js DOM helpers, renderMd, tool cards, context indicator
workspace.js File preview, file ops, git badge, central api() fetch wrapper
sessions.js Session CRUD, collapsible groups, search, reload recovery
messages.js send(), SSE handlers, live streaming, session recovery
panels.js Cron, skills, memory, profiles, settings (Control Center)
commands.js Slash command autocomplete
boot.js Mobile nav, voice input, theme/skin boot, bfcache handler
```
**Tests + packaging**
```
tests/ Pytest suite (~7,150 tests; isolated server/state fixtures)
pyproject.toml Tooling config (ruff lint gate) — not a packaged distribution
Dockerfile python:3.12-slim container image
docker-compose.yml Compose with named volume and optional auth
.github/workflows/ CI: ruff + sharded pytest, browser smoke, Docker smoke,
multi-arch Docker build + GitHub Release on tag
```
State lives outside the repo at `~/.hermes/webui/` by default
(sessions, workspaces, settings, projects, last_workspace). Override with `HERMES_WEBUI_STATE_DIR`.
Full design notes and the endpoint catalog are in [`ARCHITECTURE.md`](ARCHITECTURE.md).
---
## Docs
- `docs/why-hermes.md` -- why Hermes, mental model, and detailed comparison to Claude Code / Codex / OpenCode / Cursor
- `ROADMAP.md` -- feature roadmap and sprint history
- `ARCHITECTURE.md` -- system design, all API endpoints, implementation notes
- `TESTING.md` -- manual browser test plan and automated coverage reference
- `CHANGELOG.md` -- release notes per sprint
- `SPRINTS.md` -- forward sprint plan with CLI + Claude parity targets
- `THEMES.md` -- theme system documentation, custom theme guide
- `docs/CONTRACTS.md` -- project contract/RFC/design index for contributors and agents
- `docs/UIUX-GUIDE.md` -- UI/UX principles sourced from existing design docs and visual inventories
- `docs/docker.md` -- Docker compose setup, common failures, and bind-mount migration
- `docs/supervisor.md` -- launchd, systemd, supervisord, runit, and s6 process-supervisor setup
- `docs/onboarding.md` -- first-run wizard, provider setup, local model server Base URLs, and safe re-runs
- `docs/onboarding-agent-checklist.md` -- safety rules, evidence commands, and pass/fail checks for assistant-led install or reinstall support
- `docs/troubleshooting.md` -- diagnostic flows for common failures (e.g. "AIAgent not available")
- `docs/wsl-autostart.md` -- WSL2 auto-start at Windows login
- `docs/EXTENSIONS.md` -- administrator-controlled WebUI extension injection
- `docs/rfcs/README.md` -- RFC index for larger architecture and durability proposals
**Start here**
- [`docs/why-hermes.md`](docs/why-hermes.md) — why Hermes, the mental model, and a detailed comparison to Claude Code / Codex / OpenCode / Cursor
- [`docs/onboarding.md`](docs/onboarding.md) — first-run wizard, provider setup, local model server Base URLs, and safe re-runs
- [`docs/troubleshooting.md`](docs/troubleshooting.md) — diagnostic flows for common failures (e.g. "AIAgent not available")
**Using & customizing**
- [`THEMES.md`](THEMES.md) — theme + skin system, custom theme guide
- [`docs/workspace-git.md`](docs/workspace-git.md) — the workspace Git controls
- [`docs/EXTENSIONS.md`](docs/EXTENSIONS.md) — administrator-controlled WebUI extension injection
**Deploying & operating**
- [`docs/remote-access.md`](docs/remote-access.md) — SSH tunnel, Tailscale, and phone access (incl. a community ARM64-Android field report)
- [`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
- [`docs/wsl-autostart.md`](docs/wsl-autostart.md) — WSL2 auto-start at Windows login
- [`docs/onboarding-agent-checklist.md`](docs/onboarding-agent-checklist.md) — safety rules and pass/fail checks for assistant-led install/reinstall support
**Contributing & design**
- [`CONTRIBUTING.md`](CONTRIBUTING.md) — contribution style, PR expectations, and local verification
- [`ARCHITECTURE.md`](ARCHITECTURE.md) — system design, all API endpoints, implementation notes
- [`TESTING.md`](TESTING.md) — manual browser test plan and automated coverage reference
- [`DESIGN.md`](DESIGN.md) — design tokens and the calm-console direction
- [`docs/UIUX-GUIDE.md`](docs/UIUX-GUIDE.md) — UI/UX principles sourced from the design docs and visual inventories
- [`docs/CONTRACTS.md`](docs/CONTRACTS.md) — project contract/RFC/design index for contributors and agents
- [`docs/rfcs/README.md`](docs/rfcs/README.md) — RFC index for larger architecture and durability proposals
**Release history & plan**
- [`CHANGELOG.md`](CHANGELOG.md) — release notes per version
- [`ROADMAP.md`](ROADMAP.md) — feature roadmap and sprint history
- [`SPRINTS.md`](SPRINTS.md) — forward sprint plan with CLI + Claude parity targets
- [`CONTRIBUTORS.md`](CONTRIBUTORS.md) — the full community credit roll
---
## Contributors
Hermes WebUI is built with help from the open-source community. Every PR — whether merged directly, absorbed into a batch release, or salvaged from a larger proposal — shapes the project, and we're grateful to everyone who has taken the time to contribute.
**137 contributors have shipped code that landed in a release tag** as of v0.51.58. The full credit roll lives in [`CONTRIBUTORS.md`](CONTRIBUTORS.md). The highlights:
Over **190 contributors** have shipped code that landed in a release tag. The full,
continuously-updated credit roll — including everyone with one or two PRs and the
special-thanks roll for design and architectural work — lives in
[`CONTRIBUTORS.md`](CONTRIBUTORS.md). A snapshot of the most prolific contributors:
### Top contributors (by PR count, including absorbed/batch-released work)
| # | Contributor | PRs | First → latest release |
|---|---|---:|---|
| 1 | [@franksong2702](https://github.com/franksong2702) | 117 | `v0.49.3``v0.51.58` |
| 2 | [@Michaelyklam](https://github.com/Michaelyklam) | 92 | `v0.50.240``v0.51.57` |
| 3 | [@bergeouss](https://github.com/bergeouss) | 62 | `v0.48.0``v0.51.46` |
| 4 | [@ai-ag2026](https://github.com/ai-ag2026) | 55 | `v0.50.279``v0.51.47` |
| 5 | [@dso2ng](https://github.com/dso2ng) | 23 | `v0.50.227``v0.51.51` |
| 6 | [@jasonjcwu](https://github.com/jasonjcwu) | 16 | `v0.50.227``v0.51.55` |
| 7 | [@Jordan-SkyLF](https://github.com/Jordan-SkyLF) | 12 | `v0.50.18``v0.51.58` |
| 8 | [@aronprins](https://github.com/aronprins) | 10 | `v0.44.0``v0.50.233` |
| 9 | [@JKJameson](https://github.com/JKJameson) | 10 | `v0.50.233``v0.51.31` |
| 10 | [@starship-s](https://github.com/starship-s) | 10 | `v0.50.128``v0.51.58` |
| 1 | [@franksong2702](https://github.com/franksong2702) | 148 | `v0.49.3``v0.51.153` |
| 2 | [@Michaelyklam](https://github.com/Michaelyklam) | 117 | `v0.50.240``v0.51.139` |
| 3 | [@bergeouss](https://github.com/bergeouss) | 70 | `v0.48.0``v0.51.46` |
| 4 | [@ai-ag2026](https://github.com/ai-ag2026) | 67 | `v0.50.279``v0.51.190` |
| 5 | [@dso2ng](https://github.com/dso2ng) | 25 | `v0.50.227``v0.51.153` |
| 6 | [@AJV20](https://github.com/AJV20) | 24 | `v0.51.93``v0.51.188` |
| 7 | [@starship-s](https://github.com/starship-s) | 19 | `v0.50.123``v0.51.153` |
| 8 | [@jasonjcwu](https://github.com/jasonjcwu) | 16 | `v0.50.227``v0.51.132` |
| 9 | [@dobby-d-elf](https://github.com/dobby-d-elf) | 15 | `v0.51.38``v0.51.161` |
| 10 | [@Jordan-SkyLF](https://github.com/Jordan-SkyLF) | 12 | `v0.50.18``v0.51.66` |
See [`CONTRIBUTORS.md`](CONTRIBUTORS.md) for the full ranked list of all 137 contributors, including everyone with one or two PRs and the special-thanks roll for design and architectural contributions.
See [`CONTRIBUTORS.md`](CONTRIBUTORS.md) for the full ranked list of all 194 contributors, including everyone with one or two PRs and the special-thanks roll for design and architectural contributions.
### Notable contributions
**[@franksong2702](https://github.com/franksong2702)** — Most prolific external contributor (117 PRs, `v0.49.3``v0.51.58`)
**[@franksong2702](https://github.com/franksong2702)** — Most prolific external contributor (148 PRs, `v0.49.3``v0.51.153`)
Across the longest tenure of any external contributor: the session title guard (#301), breadcrumb workspace navigation (#302), embedded workspace terminal (#1099), worktree-backed session creation (#2053), onboarding documentation (#2052), composer footer container queries, streaming-session sidebar exemption (#1327), session sidecar repair, cron output preservation (#1295), profile default workspace persistence, manual `/compress` async start/status endpoints (#2128), worktree status surface (#2109) + guarded remove (#2156) for the lifecycle umbrella #2057, session post-render dedup (#2166), native-WebUI fast path (#2170), tail-window response trim (#2171), stale-stream guard extension (#2158), CSP report collector (#2160), and a long tail of polish across mobile/responsive, the session sidebar, and the workspace state machine.
**[@Michaelyklam](https://github.com/Michaelyklam)** — Most prolific contributor of recent releases (92 PRs, `v0.50.240``v0.51.57`)
**[@Michaelyklam](https://github.com/Michaelyklam)** — Most prolific contributor of recent releases (117 PRs, `v0.50.240``v0.51.139`)
Production Docker hardening (#1921, drops sudo-capable staging user), profile-scoped skills endpoints (#1903), gateway PID resolution under profile-scoped HERMES_HOME (#1901), profile-aware AIAgent cache (#1898/#1904), backslash LaTeX delimiters (#1848), Codex quota error surfacing (#1770), shell-route HTML 503 (#1836), stale Kanban client recovery (#1828), context auto-compression toast lifetime (#1988), `/goal` command (#1866), Kanban detail-view scrolling (#1916), CLI session tool metadata preservation (#1778), Traditional Chinese kanban locale backfill (#1979), v0.51.51 mobile Insights bucketing/layout (#2120/#2121), Hermes run adapter RFC (#2105 for #1925), fork-from-here absolute index (#2198 for #2184), opencode-go custom-provider overlap routing (#2204 for #1894).
**[@bergeouss](https://github.com/bergeouss)** — Provider management UI + Docker hardening (62 PRs, `v0.48.0``v0.51.46`)
**[@bergeouss](https://github.com/bergeouss)** — Provider management UI + Docker hardening (70 PRs, `v0.48.0``v0.51.46`)
Provider management UI for adding/editing custom providers from Settings, OAuth provider status detection (#1552), two-container Docker setup, profile isolation hardening (per-profile `.env` secrets), the bulk of what users see when they touch Settings → Providers, Reveal-in-Finder context menu (#1551), gateway status card (#1552), auto-assign session to active project filter (#1550), "What's new?" link in update banner (#1549), OpenRouter free-tier live fetch (#1548), credential pool 401 self-heal (#1553), inline provider chip + group model count in model picker (#1644).
**[@ai-ag2026](https://github.com/ai-ag2026)** — Session recovery + audit infrastructure (55 PRs, `v0.50.279``v0.51.47`)
**[@ai-ag2026](https://github.com/ai-ag2026)** — Session recovery + audit infrastructure (67 PRs, `v0.50.279``v0.51.190`)
Autonomous-AI contributor (Hermes Agent-driven) focused on durability: `state.db`-backed sidecar reconciliation (#2041), orphan `.json.bak` recovery on startup (#2035), read-only session recovery audit endpoints (#2036, #2040), active run lifecycle in `/health` (#2039), crash-safe turn-journal RFC at `docs/rfcs/turn-journal.md` (#2042), append-only turn-journal helper (#2059), lifecycle events layer (#2062), `Content-Security-Policy-Report-Only` header (#2084), per-cron toast toggle (#2100), fork-session compression lineage isolation (#2014).
**[@dso2ng](https://github.com/dso2ng)** — Session lineage + diagnostics (23 PRs, `v0.50.227``v0.51.51`)
**[@dso2ng](https://github.com/dso2ng)** — Session lineage + diagnostics (25 PRs, `v0.50.227``v0.51.153`)
`/api/session/lineage-report/<sid>` endpoint for bounded session graph diagnostics (#2012), stale Mermaid render error cleanup (#1337), `session_source="fork"` continuation-chain isolation (#2063), lazy lineage-report fetch on sidebar badge expand (#2130), and a long tail of frontend reliability fixes around session loading.
**[@jasonjcwu](https://github.com/jasonjcwu)** — Composer + transcript polish (16 PRs, `v0.50.227``v0.51.55`)
**[@jasonjcwu](https://github.com/jasonjcwu)** — Composer + transcript polish (16 PRs, `v0.50.227``v0.51.132`)
Sidebar collapse via active-rail click (#2054, fuses #1884 + #1924), composer chip lightbox (#1758), title fixes for tool-heavy first turns, silent compress-status during session switch (#2185), concurrent-send loss fix (#2186), in-transcript steer message badges (#2187), and a string of frontend polish fixes.
**[@Jordan-SkyLF](https://github.com/Jordan-SkyLF)** — Live streaming + UX polish (12 PRs, `v0.50.18``v0.51.58`)
**[@Jordan-SkyLF](https://github.com/Jordan-SkyLF)** — Live streaming + UX polish (12 PRs, `v0.50.18``v0.51.66`)
Original sprint of workspace fallback resolution, live reasoning cards (#366, #367, #394#397), then a recent burst: manual "Refresh usage" button on the Provider quota card (#2150), cancelled-turn status classification (#2151), Firefox sidebar scroll stabilization (#2200), early provisional session titles (#2202), target-aware "What's new?" update-banner links (#2207), and MCP tools overflow fix in Settings (#2210).
**[@aronprins](https://github.com/aronprins)** — `v0.50.0` UI overhaul (PR #242, plus 9 follow-ups)
@@ -720,10 +630,10 @@ Six consecutive, focused security PRs: session memory leak fix (expired token pr
**[@lucasrc](https://github.com/lucasrc)** — Auth-hardening trilogy (PRs #2191, #2192, #2193)
Three coordinated security PRs that all landed in v0.51.57: thread-safe login rate limiter with PBKDF2 key separation, password-hash cache invalidation on Settings save, and the full 64-char HMAC-SHA256 session signature with a backwards-compatible migration bridge. The kind of cleanly-decomposed security work that's reviewable as three independent pieces.
**[@LumenYoung](https://github.com/LumenYoung)** — Streaming hot-path correctness (4 PRs, `v0.51.47``v0.51.55`)
**[@LumenYoung](https://github.com/LumenYoung)** — Streaming hot-path correctness (8 PRs, `v0.51.47``v0.51.99`)
The original stale-stream writeback guard (#2136 — the bug class the next two releases extended), gateway-state alive-null classification (#2075), compression-banner anchor alignment (#2182), and context-progress ring auto-refresh on compression complete (#2188). Each PR opened a small surgical fix in one of the most fragile subsystems in the codebase.
**[@dobby-d-elf](https://github.com/dobby-d-elf)** — Frontend reliability + motion polish (6 PRs, `v0.51.38``v0.51.58`)
**[@dobby-d-elf](https://github.com/dobby-d-elf)** — Frontend reliability + motion polish (15 PRs, `v0.51.38``v0.51.161`)
Workspace fallback on deleted directories (#2138), iPhone PWA bottom-scroll fix (#2143), the new "Activity: X tools" composer footer shimmer animation (#2203), and follow-up animation tuning (#2212).
**[@JKJameson](https://github.com/JKJameson)** — Composer + session polish (10 PRs)
@@ -786,6 +696,8 @@ A comprehensive CSRF / SSRF / XSS / env-race-condition audit that shipped in v0.
**[@TaraTheStar](https://github.com/TaraTheStar)** — Bot name + thinking blocks + login refactor (PRs #132, #176, #181)
Configurable assistant display name, thinking/reasoning block display, and a login page refactor.
---
## Repo
```
+9 -7
View File
@@ -2,7 +2,7 @@
> Web companion to the Hermes Agent CLI. Same workflows, browser-native.
>
> Last updated: v0.51.31 (May 9, 2026) — 5028 tests collected — Release H 12-PR contributor batch (image-mode fix + race fixes + composer drafts + locale parity + custom-provider dedup + TTL config + heartbeat polish)
> Last updated: v0.51.192 (May 31, 2026) — ~7,150 tests collected. Recent themes: forward-looking ruff lint gate (#3273), Windows upgrade state-stranding hotfix (#2905), per-model context_length default-only guard (#3256), gateway-configured banner + tooltip i18n, and an ongoing concentric PR-triage cadence.
> Test source: `pytest tests/ --collect-only -q`
> Per-version detail: see [CHANGELOG.md](./CHANGELOG.md)
@@ -18,7 +18,7 @@
| **Sessions + projects + search** | ✅ CRUD, content search, projects, tags, archive, fork, import |
| **Mobile + Docker + auth** | ✅ Hamburger nav, slide-overs, password auth, GHCR images |
| **Auxiliary surfaces** | ✅ Workspace tree + edit, cron CRUD, skills CRUD, memory write, MCP server UI |
| **Visual polish** | ✅ 8 themes (incl. light/system/OLED/Sienna), Mermaid, KaTeX, syntax highlighting |
| **Visual polish** | ✅ Light/dark/system themes × 11 skins (default, ares, mono, slate, poseidon, sisyphus, charizard, sienna, catppuccin, nous, geist-contrast), Mermaid, KaTeX, syntax highlighting |
| **Native distribution** | ✅ macOS desktop app (universal arm64+x86_64 DMG, signed) — separate repo |
Remaining gaps and forward work live in [Forward Work](#forward-work) below.
@@ -29,15 +29,17 @@ Remaining gaps and forward work live in [Forward Work](#forward-work) below.
| Layer | Files | Status |
|---|---|---|
| Python server | `server.py` (~165 lines) + `api/` modules (~20k lines) | Thin shell + auth middleware + business logic |
| HTML template | `static/index.html` (~600 lines) | Served from disk |
| CSS | `static/style.css` (~3k lines) | Themes, mobile responsive, KaTeX, table styles |
| JavaScript | `static/{ui,sessions,messages,workspace,panels,boot,commands,icons,i18n,login,onboarding}.js` (~26k lines) | 11 modules served as static files |
| Python server | `server.py` + `api/` modules | Thin HTTP shell + auth middleware over the `api/` business logic (config, sessions, streaming, profiles, routes, onboarding, workspace, updates, upload) |
| HTML template | `static/index.html` | Served from disk |
| CSS | `static/style.css` | Themes + skins, mobile responsive, KaTeX, table styles |
| JavaScript | `static/{ui,sessions,messages,workspace,panels,boot,commands,icons,i18n,login,onboarding}.js` | Vanilla-JS modules served as static files — no bundler |
| Service worker | `static/sw.js` | Offline shell cache, version-pinned assets |
| Docker | `Dockerfile`, `docker-compose.yml` | `python:3.12-slim`, multi-arch (amd64+arm64), HEALTHCHECK |
| CI/CD | `.github/workflows/release.yml` | Auto-release + GHCR publish on tag push |
| CI/CD | `.github/workflows/` | ruff lint + sharded pytest + browser smoke + Docker smoke on every PR; auto-release + GHCR publish on tag push |
| Test isolation | `tests/_pytest_port.py` | Per-worktree port + state-dir derivation, no collisions |
> Per-file line counts drift every release; see `ARCHITECTURE.md` for the current module map and `git ls-files` for exact sizes.
---
## Feature parity checklist
+4 -4
View File
@@ -1,14 +1,14 @@
# Hermes Web UI: Browser Testing Plan
> This document is for manual browser testing by you or by a Claude browser agent.
> It covers user-facing features of the UI through v0.50.21 and later releases.
> It covers user-facing features of the UI across current releases.
> Each section is written as a step-by-step test procedure with expected outcomes.
> A browser agent (e.g. Claude with Chrome access) can execute this plan directly.
>
> Prerequisites: SSH tunnel is active on port 8787. Open http://localhost:8787 in browser.
> Server health check: curl http://127.0.0.1:8787/health should return {"status":"ok"}.
>
> Automated coverage: 5303 tests collected via `pytest tests/ --collect-only -q`. Tests run on every PR via GitHub Actions on Python 3.11, 3.12, and 3.13. The suite covers the bootstrap/static wizard, real provider config persistence (`config.yaml` + `.env`), the `/api/onboarding/*` backend, the onboarding skip/existing-config guard, CSS regression coverage for thinking/tool card animation, streaming session persistence, mobile layout breakpoints, locale parity across 11 languages, and hundreds of issue/PR-pinned regression tests.
> Automated coverage: ~7,150 tests collected via `pytest tests/ --collect-only -q`. Tests run on every PR via GitHub Actions on Python 3.11, 3.12, and 3.13 (3 parallel shards each), alongside a ruff lint gate, a headless browser smoke test, and a Docker smoke test. The suite covers the bootstrap/static wizard, real provider config persistence (`config.yaml` + `.env`), the `/api/onboarding/*` backend, the onboarding skip/existing-config guard, CSS regression coverage for thinking/tool card animation, streaming session persistence, mobile layout breakpoints, locale parity across 11 languages, and hundreds of issue/PR-pinned regression tests.
> Run: `pytest tests/ -v --timeout=60`
>
> Local regression focus: verify that a previously closed workspace panel stays visually closed from first paint through boot completion on desktop refresh; there should be no brief open-then-close flash.
@@ -1925,8 +1925,8 @@ Bridged CLI sessions:
---
*Last updated: v0.51.54, May 13, 2026*
*Total automated tests collected: 5303*
*Last updated: v0.51.192, May 31, 2026*
*Total automated tests collected: ~7,150 (run `pytest tests/ --collect-only -q` for the exact current count)*
*Regression gate: tests/test_regressions.py*
*Run: pytest tests/ -v --timeout=60*
*Source: <repo>/*
+83
View File
@@ -0,0 +1,83 @@
# Advanced chat setup
Two optional features for self-hosted Hermes WebUI deployments. **Most users need neither** — the defaults (in-process chat, no prefill) work out of the box.
## Session recall prefill
WebUI can attach ephemeral prefill messages to new browser-originated
agent turns. This is useful when a deployment already has a local recall or
router script for Joplin, Obsidian, Notion, llm-wiki, or another third-party
notes source and wants browser chat to know where durable context lives.
Prefer a compact router-style prefill (for example, "Joplin has the durable
project context; use the available notes/search tools before answering
detail-dependent questions") instead of dumping the full note corpus into every
new browser session. The prefill should point the agent toward retrieval; the
notes/search tools should provide the specific facts on demand.
Static JSON remains supported through `prefill_messages_file` or
`HERMES_PREFILL_MESSAGES_FILE`. For dynamic recall, opt in explicitly with a
WebUI-specific script hook:
```yaml
webui_prefill_messages_script:
- python3
- /path/to/notes_recall.py
webui_prefill_messages_script_timeout: 5
```
or:
```bash
HERMES_WEBUI_PREFILL_MESSAGES_SCRIPT="python3 /path/to/notes_recall.py" \
HERMES_WEBUI_PREFILL_MESSAGES_SCRIPT_TIMEOUT=5 \
./ctl.sh restart
```
The script may print either an OpenAI-style JSON message list, a JSON object with
a `messages` list, or plain text; plain text is wrapped as one `user` prefill
message so dynamic recall text becomes ordinary context instead of an extra
system instruction. If the hook must provide system-level guidance, emit JSON
messages with an explicit `role: "system"` entry instead. Script output is capped
at 256 KiB before parsing. Parsed prefill context is then bounded by
`webui_prefill_context_max_chars` or `HERMES_WEBUI_PREFILL_CONTEXT_MAX_CHARS`
(default: 12,000 characters; set to `0` to disable). When a dynamic script
exceeds the budget and a compact static prefill file is configured, WebUI falls
back to that file. If no compact fallback is available, WebUI injects a short
retrieval instruction instead of sending the oversized note/body payload with
every new browser turn. The browser only receives a compact status event
(`source`, `label`, message count, compaction metadata, and redacted errors),
never the prefill message bodies.
## Gateway-backed browser chat
By default, browser chat runs through WebUI's in-process legacy runtime. Advanced
self-hosted deployments can opt into routing new browser turns through a running
Hermes Gateway API server while preserving the existing WebUI `/api/chat/start`
and `/api/chat/stream` browser contract:
```bash
HERMES_WEBUI_CHAT_BACKEND=gateway \
HERMES_WEBUI_GATEWAY_BASE_URL=http://127.0.0.1:8642 \
HERMES_WEBUI_GATEWAY_API_KEY=... \
./ctl.sh restart
```
`HERMES_WEBUI_CHAT_BACKEND` is intentionally strict: only `gateway`,
`api_server`, or `api-server` enable the bridge. Generic truthy values such as
`1` or `true` are ignored so existing deployments do not change execution
ownership accidentally. If `HERMES_WEBUI_GATEWAY_API_KEY` is omitted, WebUI falls
back to `API_SERVER_KEY` when present. When Gateway returns HTTP 401, WebUI
reports a `gateway_auth_error` that points at this WebUI↔Gateway key mismatch
rather than showing the Gateway's generic provider-style "Invalid API key" body.
`/api/health/agent` also includes a redacted `gateway_chat` block so operators can
see whether gateway mode, base URL, and API-key presence are configured without
exposing the key value. That `gateway_chat` field is an operator diagnostic
payload only; it is not currently rendered as a user-facing health banner in the
browser UI.
The bridge is best used by operators who already run Hermes Gateway/API Server
locally and want browser-originated chat to use the same runtime/tool path as
messaging surfaces. Attachments, cancellation, approvals, and clarify prompts
still follow WebUI's current compatibility path and may not match every messaging
surface until the runtime-adapter migration is complete.
+75
View File
@@ -0,0 +1,75 @@
# Remote access
How to reach a self-hosted Hermes WebUI from another machine or your phone.
## Accessing from a remote machine
The server binds to `127.0.0.1` by default (loopback only). If you are running
Hermes on a VPS or remote server, use an SSH tunnel from your local machine:
```bash
ssh -N -L <local-port>:127.0.0.1:<remote-port> <user>@<server-host>
```
Example:
```bash
ssh -N -L 8787:127.0.0.1:8787 user@your.server.com
```
Then open `http://localhost:8787` in your local browser.
`start.sh` will print this command for you automatically when it detects you
are running over SSH.
---
## Accessing on your phone with Tailscale
[Tailscale](https://tailscale.com) is a zero-config mesh VPN built on
WireGuard. Install it on your server and your phone, and they join the same
private network -- no port forwarding, no SSH tunnels, no public exposure.
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:**
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:
```bash
HERMES_WEBUI_HOST=0.0.0.0 HERMES_WEBUI_PASSWORD=your-secret ./start.sh
```
3. Open `http://<server-tailscale-ip>:8787` in your phone's browser
(find your server's Tailscale 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.
### Community field report: ARM64 Android via AVF
A community report in [#2364](https://github.com/nesquena/hermes-webui/issues/2364)
documents Hermes Agent + WebUI running on a mid-range ARM64 Android phone inside
a Debian 12 VM via Android Virtualization Framework (AVF). The reported setup
used a Xiaomi Redmi Note 13 Pro 4G, 3.8 GiB RAM allocated to the VM, 8 visible
CPU cores, Chrome on Android at `localhost:8787`, and cloud-hosted inference.
This is not an official support baseline or provider/model benchmark, but it is
a useful compatibility signal for mobile ARM64 experiments: the WebUI rendered
smoothly in Chrome, ARM64 Debian worked for the agent stack, and the total local
footprint was about 1.7 GB. Practical caveats from the report: first install can
take longer when dependencies compile from source, Android browser tabs may
reload when switching apps, and disabling battery optimization for the terminal
or VM host may be needed for longer-running sessions.
> **Tip:** If using Docker, set `HERMES_WEBUI_HOST=0.0.0.0` in your
> `docker-compose.yml` environment (already the default) and set
> `HERMES_WEBUI_PASSWORD`.
---
+7 -2
View File
@@ -201,9 +201,14 @@ def test_gateway_auth_label_i18n_key_exists_for_every_locale():
def test_gateway_chat_health_payload_is_documented_as_operator_diagnostic_only():
readme = Path("README.md").read_text(encoding="utf-8")
# The Gateway-backed-chat operator docs moved out of the README into
# docs/advanced-chat-setup.md during the v0.51.192 README IA pass (it's a
# niche self-hosted feature). The contract — that gateway_chat is documented
# as an operator-only diagnostic, not a user-facing banner — now lives there.
# CHANGELOG keeps its release-note entry. (Contract test moved with content.)
advanced = Path("docs/advanced-chat-setup.md").read_text(encoding="utf-8")
changelog = Path("CHANGELOG.md").read_text(encoding="utf-8")
for text in (readme, changelog):
for text in (advanced, changelog):
assert "gateway_chat" in text
assert "operator diagnostic" in text
assert "not currently rendered as a user-facing health banner" in text