#3220: redesign generated-image rendering from a permanent bordered card
(filename + Open/Download buttons) to a clean inline image with click-to-zoom
lightbox + a hover/focus-revealed Download overlay, matching ChatGPT/Claude/
Gemini. Drops redundant Open (lightbox already covers it). Tests updated.
#3337: fix two-tone code background — Prism's prism-tomorrow theme styled the
parent <pre> gray while the <code> was navy var(--code-bg), so dark theme showed
a gray frame around navy code. Override BOTH .preview-code[class*=language-] and
its <code> to var(--code-bg) (mirrors the chat code-block fix at .msg-body pre).
#3223: fix action-menu clipping — the new Regenerate-title row made the 9-item
menu tall enough to overflow the viewport bottom when opened on a top-anchored
row at short viewports (e.g. 1280x720). _positionSessionActionMenu now clamps
the menu within both viewport edges and caps max-height with scroll when the
menu is taller than the viewport.
Per-profile WebUI state lives at <root>/webui_state (api/workspace.py), so
<base>/profiles/<name>/webui_state/sessions/*.json was reachable — it is not a
direct child of the profile root, so the prior deny-subdir loop missed it. Add
<root>/webui_state/<state-subdir> to the deny dirs for every Hermes root. Adds a
regression assertion (profile webui_state/sessions/*.json → 403).
Denying the whole <root>/profiles tree 403'd legitimate named-profile workspace
media (<base>/profiles/p1/workspace/shot.png). Fix: remove 'profiles' from
_DENY_SUBDIRS and instead enumerate each <root>/profiles/<name> directory as its
own Hermes root — so each profile's sensitive subdirs (sessions/memories/cron/
logs/checkpoints/backups) + secret filenames are denied, while that profile's
workspace/ is allowed via the carve-out. Adds a regression test: named-profile
workspace media serves, profile + sibling-profile auth.json stay 403.
The default workspace lives at STATE_DIR/workspace, so denying STATE_DIR itself
403'd legitimate workspace media. STATE_DIR is already in _hermes_roots, so its
sensitive subdirs (STATE_DIR/sessions, /memories, /profiles, etc.) are still
covered by the per-root subdir loop; direct sensitive files are still caught by
the filename denies. Drop the wholesale _state_dir deny. Adds a regression test
proving STATE_DIR/workspace/shot.png serves while STATE_DIR/sessions/*.json 403s.
Codex round-4: the carve-out could re-open the hole if the active workspace is
pathologically set to a broad/internal root ($HOME, ~/.hermes, a profile root)
— get_last_workspace only checks is_dir(), so workspace=~/.hermes would serve
state.db. Gate the carve-out: disable it when the active workspace IS, CONTAINS,
or is CONTAINED BY any Hermes root, or is $HOME / a */profiles dir / a named
profile root / an internal state subdir. Adds a unit test proving state.db stays
403 when the active workspace is the Hermes home. Widen CSP-slice test window.
Codex round-3 found the prior multi-profile hardening OVER-blocked: denying
STATE_DIR + base/profiles wholesale 403'd legitimate active-workspace media.
Redesign around a single principle: the ACTIVE WORKSPACE is the user's own
content (never deny), Hermes INTERNAL STATE lives outside any workspace (deny).
If target is inside the active workspace -> allow; else deny known secret/config
basenames + internal state subdirs across all Hermes roots. Also folds in Opus
defense-in-depth: adds cron/logs/checkpoints/backups subdirs +
gateway_state.json/channel_directory.json/jobs.json basenames. Adds an
over-block regression test (a /tmp artifact named settings_* still serves 200).
Pre-release dual-gate (Codex + Opus) on #3219 surfaced that /api/media serves
files under the allowlisted Hermes home, including settings.json / state.db /
auth.json / config.yaml. #3219 makes this materially worse: pre-#3219 a bare
file:// URL in agent output rendered as inert text, but #3219 turns it into an
auto-loading <img src=/api/media?path=...> that fetches on render. Rather than
weaken #3219, harden the boundary at the route: hard-deny known secret/config
filenames and the WebUI state subdirs (sessions/memories/profiles + STATE_DIR)
before the allow/serve decision, covering every entry path (bare file://,
markdown anchors, MEDIA: tokens, session-token grants). Adds a live-server
regression test. Closes#3234.
Co-authored-by: AJV20 <24819659+AJV20@users.noreply.github.com>
The /api/media endpoint only serves files from ~/.hermes, /tmp, and the
active workspace. Power users with media in custom directories (models,
Downloads, Pictures, ComfyUI outputs) have no way to serve those files
inline without copying or symlinking.
Add MEDIA_ALLOWED_ROOTS env var — a colon-separated list of absolute
paths — that extends the allowed roots at runtime. Each entry is resolved
and validated as an existing directory before being appended. Non-existent
or invalid paths are silently skipped.
This is purely additive: the built-in security whitelist is unchanged,
and if MEDIA_ALLOWED_ROOTS is unset, behavior is identical to before.
* feat: attention state for broken cron jobs + Korean i18n (#1133, @franksong2702)
* fix: pytest state isolation for direct session saves (#1136, @franksong2702)
* fix(#1095): image thumbnails in composer + lightbox in chat (#1135)
* fix(css): restore cron attention + detail-alert rules overwritten by style.css merge (absorb)
* docs: v0.50.225 release notes and version bump
---------
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Squash-merges feature from PR #588 by @vcavichini. Dynamic <base href> injection + api() helper slash-stripping enables deploying hermes-webui behind a reverse proxy at any subpath without configuration. Also fixes pre-existing bug: api/upload was using location.origin instead of location.href (closes#596). Co-authored-by: vcavichini <vcavichini@users.noreply.github.com>
Fixes two SKIP_ONBOARDING bugs and eliminates 10 permanently-skipped integration tests.
- SKIP_ONBOARDING=1 now honoured unconditionally (no longer gated on chat_ready)
- apply_onboarding_setup refuses to write config/env files when SKIP_ONBOARDING is set
- TestMediaEndpointIntegration (6) and TestOnboardingGateIntegration (4): collection-time
skip guards removed; server reachability checked at runtime with fail() not skip()
Tests: 1327 passed, 0 skipped.
Admin merge — self-built PR, Nathan authorized full merge process in session.