Commit Graph

10 Commits

Author SHA1 Message Date
nesquena-hermes 57c71e89f3 fix(docker): salvage operational hardening from #1686 (env readonly + apt deps)
Three independent operational hardening fixes salvaged from PR #1686
(@binhpt310) after the parent PR was deferred over a separate sibling-repo
build-context concern unrelated to these fixes:

1. start.sh's .env loader now filters readonly bash vars (UID, GID, EUID,
   EGID, PPID) before `source`-ing.  docker-compose.yml's macOS instructions
   document `echo "UID=$(id -u)" >> .env` to set host UID/GID for bind-mount
   permission fixing — that .env was crashing start.sh with
   `UID: readonly variable` when `set -a; source ...; set +a` tried to
   assign to those names.  Replaced with
   `source <(grep -vE '^[[:space:]]*(export[[:space:]]+)?(UID|GID|EUID|EGID|PPID)=' "${REPO_ROOT}/.env")`.
   The bootstrap regression guard at tests/test_bootstrap_dotenv.py:181
   still passes — both `source` and `.env` are still on the modified line.

2. start.sh now defensively re-execs as the unprivileged hermeswebui user
   when invoked as root.  Fires only when EUID==0 AND a hermeswebui user
   actually exists AND sudo is on PATH — so it's a no-op on host machines
   without the container user setup.  The production image's entrypoint
   (docker_init.bash) already drops to hermeswebui before invoking start.sh,
   so this is a no-op on the canonical container path; it only matters for
   `sudo ./start.sh` or accidental root shells inside the container during
   interactive debugging.

3. Dockerfile installs xz-utils + git apt packages.  xz-utils is required
   to decompress .tar.xz archives (e.g. Node.js distribution tarballs);
   git is needed for `git describe` (powers WEBUI_VERSION resolution at
   api/updates.py:_detect_webui_version) and any clone-based agent install
   path.  Both are tiny apt packages on top of python:3.12-slim with no
   measurable image-size impact.

What's NOT in this commit (deferred from #1686):

- Pre-baking hermes-agent source into the image via
  `COPY hermes-agent-desktop/hermes-agent /opt/hermes/` plus a build-context
  flip to `..`.  Requires a sibling-repo layout that breaks the canonical
  `git clone hermes-webui && cd hermes-webui && docker compose build` flow.
  The right shape is a build arg gating the COPY behind
  --build-arg WITH_AGENT_SOURCE=1; left to a separate PR.
- Pre-installing Node.js 22 LTS system-wide.  Real motivation but worth
  evaluating the fix shape (full Node bake vs. opt-in vs. layer cache)
  separately from these three operational fixes.

Tests: tests/test_docker_env_readonly_vars.py — 11 tests (4 source-grep
on the start.sh filter pattern + 5 behavioral that actually run bash
against synthetic .env files containing readonly vars + 2 Dockerfile
package-presence tests).  All 11 pass.  Behavioral tests skip if bash
is not on PATH.

Full suite: 5028 → 5036 passing (+8 net new after pytest collection
counted some behavioral tests under skip), 0 regressions, 147.84s.

Closes the operational-hardening portion of #1686.

Co-authored-by: binhpt310 <binhpt310@users.noreply.github.com>
2026-05-09 19:17:34 +00:00
Michael Lam b1b0cedbe9 security: harden production Docker image 2026-05-08 20:48:39 +00:00
nesquena-hermes 9a6caa1e78 fix: add Docker HEALTHCHECK to Dockerfile
From PR #1332.

Co-authored-by: Leon.C <160379708+zichen0116@users.noreply.github.com>
2026-04-30 15:24:29 +00:00
Joe Maples 1a98f75005 fix(docker): add openssh-client to Docker image for SSH terminal backend — v0.50.157 (PR #868 by @frap129)
Adds openssh-client to apt-get install block so Docker users running the SSH terminal backend can connect to remote agents. Closes #868.
2026-04-22 22:39:41 +00:00
nesquena-hermes 3f484aec33 fix: add --chown to Dockerfile COPY so RUN can write api/_version.py (#793)
The v0.50.124 Docker build failed with:
  cannot create /apptoo/api/_version.py: Permission denied

Root cause: 'USER hermeswebuitoo' is set before 'COPY . /apptoo', but
COPY without --chown creates files owned by root. The subsequent RUN
step (which writes api/_version.py) runs as hermeswebuitoo and has no
write permission to the root-owned api/ directory.

Fix: COPY --chown=hermeswebuitoo:hermeswebuitoo so the unprivileged user
owns the app files and can write _version.py at build time.

Regression from #790.

Co-authored-by: nesquena-hermes <hermes@nesquena.com>
2026-04-20 21:03:41 -07:00
nesquena-hermes 38e215e8f8 fix: dynamic version badge — read from git tag, never hardcoded (#790)
* fix: dynamic version badge — read from git tag, never hardcoded

The settings panel showed v0.50.87 and the HTTP Server: header said
HermesWebUI/0.50.38 — both hardcoded strings that drift further behind
with every release because there was no mechanism to keep them in sync.

Changes:
- api/updates.py: add _run_git() (moved before _detect_webui_version),
  _detect_webui_version(), and WEBUI_VERSION module constant resolved
  once at import time via 'git describe --tags --always --dirty'.
  Fallback chain: git → api/_version.py → 'unknown'.
- api/routes.py: inject webui_version into GET /api/settings response
  so the frontend can read it without a separate API call.
- static/panels.js: loadSettingsPanel() populates .settings-version-badge
  from settings.webui_version — one line after the existing api() call.
- static/index.html: replace stale hardcoded 'v0.50.87' with '—'
  placeholder; JS overwrites it as soon as the settings panel opens.
- server.py: replace hardcoded 'HermesWebUI/0.50.38' server_version with
  'HermesWebUI/' + WEBUI_VERSION.lstrip('v') — stays in sync automatically.
- Dockerfile: add ARG HERMES_VERSION=unknown and write api/_version.py
  so Docker images (where .git is excluded) still show the correct tag.
- .github/workflows/release.yml: pass build-args: HERMES_VERSION=${{ github.ref_name }}
  to the Docker build step on tag pushes.
- .gitignore: exclude api/_version.py (generated by Docker/CI, never committed).

No manual 'update the version badge' step is required going forward.
Tagging is sufficient — the badge and HTTP header update automatically.

Tests: 18 new tests in tests/test_version_badge.py covering the full
resolution chain, /api/settings injection, HTML placeholder, JS wiring,
and server.py import. 1596 tests pass total.

* fix: address review feedback on PR #790

- api/updates.py: replace exec() with regex parse for api/_version.py
  (no supply-chain risk from build artifact; exec unnecessary for one assignment)
- api/updates.py: cap git describe timeout at 3s (was 10s — import-time
  stall on NFS/.git would block server startup unnecessarily)
- server.py: lstrip('v') → removeprefix('v') (lstrip strips chars not prefix)
- server.py: emit bare 'HermesWebUI' when version is 'unknown' rather than
  'HermesWebUI/unknown' (log aggregators expect semver-ish suffix or none)
- CHANGELOG.md: add v0.50.124 entry for this user-visible change
- tests: rename exec-error test to reflect regex behaviour; add tests for
  removeprefix usage and unknown-version header guard (1598 tests total)

---------

Co-authored-by: nesquena-hermes <hermes@nesquena.com>
2026-04-20 20:36:53 -07:00
nesquena-hermes a6484f69a8 fix: Docker uv pre-install at build time + workspace permissions (#365)
* fix: pre-install uv in Docker image + fix workspace dir permissions (#357)

Two fixes for Docker startup reliability:

1. Install uv at build time in the Dockerfile so the container works
   without internet access at runtime. The init script now skips the
   download when uv is already on PATH.

2. Use sudo mkdir/chown for the workspace directory, matching the
   pattern used for /app. Docker auto-creates bind-mount directories
   as root, leaving them unwritable by the hermeswebui user.

Fixes #357

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: Docker uv pre-install as root to /usr/local/bin + tests + CHANGELOG

Dockerfile: install uv as root with UV_INSTALL_DIR=/usr/local/bin so it
lands in /usr/local/bin (system PATH) rather than /home/hermeswebuitoo/.local/bin
which the hermeswebui runtime user can't see.

tests/test_issue357.py: 15 structural tests covering Dockerfile uv build-time
install (system-wide, as root, before COPY), init script skip-if-present
logic, and workspace sudo mkdir/chown.

CHANGELOG.md: v0.50.17 entry; 915 tests (up from 900)

---------

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 12:36:11 -07:00
nesquena-hermes 27c2fd6c08 v0.46.0: security, Docker UID/GID, model discovery, i18n, cancel fix
* fix: decode HTML entities before markdown processing + zh/zh-Hant translations (#239)

Adds decode() helper in renderMd() to fix double-escaping of HTML entities
from LLM output (e.g. &lt;code&gt; becoming &amp;lt;code&amp;gt; instead
of rendering). XSS-safe: decode runs before esc(), only 5 entity patterns.

Also adds 40+ missing zh (Simplified Chinese) translation keys and a new
zh-Hant (Traditional Chinese) locale with 163 keys.

Fix applied: removed duplicate settings_label_notifications key in both
zh and zh-Hant locales.

Fixes #240

* fix: restore custom model list discovery with config api key (#238)

get_available_models() now reads api_key from config.yaml before env vars:
  1. model.api_key
  2. providers.<active>.api_key / providers.custom.api_key
  3. env var fallbacks (HERMES_API_KEY, OPENAI_API_KEY, etc.)

Also adds OpenAI/Python User-Agent header and a regression test covering
authenticated /v1/models discovery.

Fixes users with LM Studio / Ollama custom endpoints configured in
config.yaml whose model picker silently collapsed to the default model.

* feat: Docker UID/GID matching to avoid root-owned .hermes files (#237)

Adds docker_init.bash with hermeswebuitoo/hermeswebui user pattern so
container files match the host user UID/GID. Prevents .hermes volume
mounts from being owned by root when using a non-root host user.

Configure via WANTED_UID and WANTED_GID env vars (default 1000/1000).
Readme updated with setup instructions.

Fix applied: removed duplicate WANTED_GID=1000 line in docker-compose.yml
that was overriding the ${GID:-1000} variable expansion.

* security: redact credentials from API responses and fix credential file permissions (#243)

Adds response-layer credential redaction to three endpoints:
  - GET /api/session — messages[], tool_calls[], and title
  - GET /api/session/export — download also redacted
  - SSE done event — session payload in stream
  - GET /api/memory — MEMORY.md and USER.md content

Adds api/startup.py with fix_credential_permissions() at server startup.
Adds 13 tests in tests/test_security_redaction.py.

Merged with #237 container detection changes in server.py.

* fix: cancel button now interrupts agent and cleans up UI state (#244)

Wires agent.interrupt() into cancel_stream() so the backend actually
stops tool execution when the user clicks Cancel, rather than only
stopping the SSE stream while the agent keeps running.

Changes:
  - api/config.py: adds AGENT_INSTANCES dict (stream_id -> AIAgent)
  - api/streaming.py: stores agent in AGENT_INSTANCES after creation,
    checks CANCEL_FLAGS immediately after store (race condition fix),
    calls agent.interrupt() in cancel_stream(), cleans up in finally block
  - static/boot.js: removes stale setStatus(cancelling) call
  - static/messages.js: setBusy(false)/setStatus('') unconditionally on cancel

Race condition fix: after storing agent in AGENT_INSTANCES, immediately
checks if CANCEL_FLAGS[stream_id] is already set (cancel arrived during
agent init) and interrupts before starting. Check is inside the same
STREAMS_LOCK acquisition, making it atomic.

New test file: tests/test_cancel_interrupt.py with 6 unit tests.

* docs: v0.46.0 release notes, bump version, update test counts

---------

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-11 10:17:52 -07:00
Nathan Esquenazi 574cd2cf70 fix(review): 5 issues found in agent review of PR #40
BUG-1 (critical): CSS cascade — .sidebar{position:relative} and
.rightpanel{position:relative} at line 528/530 appeared after the
@media(max-width:640px) block and silently overrode the position:fixed
overlay behavior needed for the mobile slide-in. Wrapped both in
@media(min-width:641px) so they only apply on desktop.

BUG-2 (medium): mobileSwitchPanel() in boot.js always reopened the
sidebar overlay after closing it, with a stale comment saying 'close
after a moment' but no actual auto-close. For the 'chat' panel, the
content lives in the main area — reopening the sidebar obstructs it.
Fixed: only open sidebar for non-chat panels; chat tap closes sidebar.

BUG-3 (medium): Dockerfile was missing 'pip install -r requirements.txt'.
pyyaml (required by api/config.py) is not in the python:3.12-slim base
image — the container would fail at startup with ImportError.

SEC-2 (medium): No .dockerignore — COPY . /app included .git/, tests/,
and .env* in every image. Added .dockerignore excluding these.

NIT-3: docker-compose.yml used ${HERMES_HOME:-~/.hermes} but Docker
Compose does not shell-expand ~ in default values. Changed to
${HERMES_HOME:-${HOME}/.hermes}.

Tests: 415 passed, 0 failed (same as pre-fix).
2026-04-03 17:21:42 +00:00
Nathan Esquenazi d278563e00 feat: Sprint 21 — mobile responsive layout + Docker support
Mobile responsive (Issue #21):
- Hamburger sidebar: slide-in overlay on mobile (<640px) with backdrop.
  Tap hamburger in topbar to open, tap outside to close. Full session
  list, project chips, all panel content accessible.
- Bottom navigation bar: 5-tab fixed bar (Chat, Tasks, Skills, Memory,
  Spaces) replaces sidebar nav tabs on mobile. iOS-style layout.
  Tapping a tab opens the sidebar overlay with that panel active.
- Right panel slide-over: Files button in topbar chips opens workspace
  panel as a slide-over from the right on mobile/tablet.
- Touch targets: all interactive elements get min 44x44px touch areas.
  Session items, approval buttons, composer buttons all sized for fingers.
- Composer positioned above bottom nav bar with proper spacing.
- Sidebar nav tabs and bottom section hidden on mobile (replaced by
  bottom nav + topbar chips).
- Clicking a session auto-closes the sidebar overlay.
- Desktop layout completely unchanged — all mobile elements are
  display:none by default, only shown inside @media(max-width:640px).

Docker (Issue #7):
- Dockerfile: python:3.12-slim, HERMES_WEBUI_HOST=0.0.0.0, port 8787.
- docker-compose.yml: named volume for state persistence, optional
  ~/.hermes mount for agent features, password env var documented.
- README: Docker quick start section with compose and manual commands.

Tests: 392 passed, 23 pre-existing failures, 0 regressions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 10:09:36 -07:00