Skip to content

fix: serve raw chat attachments from inbox - #2382

Merged
1 commit merged into
nesquena:masterfrom
Michaelyklam:fix/issue-2380-attachment-file-raw
May 16, 2026
Merged

1 commit merged into
nesquena:masterfrom
Michaelyklam:fix/issue-2380-attachment-file-raw

Conversation

@Michaelyklam

@Michaelyklam Michaelyklam commented May 16, 2026

Copy link
Copy Markdown
Contributor

Thinking Path

  • Chat uploads were intentionally moved out of workspaces into a per-session attachment inbox.
  • The transcript renderer still emits stable api/file/raw?session_id=...&path=<filename> URLs for uploaded images.
  • _handle_file_raw only checked session.workspace, so inbox-backed uploads rendered as broken images.
  • The narrow fix is to preserve the URL surface and add a session-scoped inbox fallback on the server.
  • The fallback must stay scoped to the current session so one session cannot probe another session's attachments.

What Changed

  • Added _file_raw_target() to resolve /api/file/raw first from the workspace, then from _session_attachment_dir(session_id).
  • Kept traversal failures as not-found responses instead of exposing filesystem paths or falling through to another root.
  • Added regression coverage for serving a chat-upload filename from the attachment inbox and rejecting traversal through the fallback.
  • Updated an existing raw-file traversal expectation and the inline-HTML CSP source-string guard to reflect the refactor.
  • Added an Unreleased changelog entry.

Why It Matters

Uploaded chat images stored under ~/.hermes/webui/attachments/<session_id>/ now load through the URL the frontend already generates, including docker-compose setups where the workspace and attachment root are separate.

Verification

  • env -u HERMES_CONFIG_PATH -u HERMES_WEBUI_HOST /home/michael/.hermes/hermes-agent/venv/bin/python -m pytest tests/test_sprint6.py::test_file_raw_serves_session_attachment_inbox tests/test_sprint6.py::test_file_raw_attachment_fallback_rejects_traversal tests/test_sprint6.py::test_file_raw_requires_session_id tests/test_sprint6.py::test_file_raw_unknown_session -q — 4 passed
  • env -u HERMES_CONFIG_PATH -u HERMES_WEBUI_HOST /home/michael/.hermes/hermes-agent/venv/bin/python -m pytest tests/test_sprint6.py tests/test_sprint1.py::test_upload_text_file tests/test_sprint1.py::test_upload_respects_attachment_dir_env tests/test_native_image_attachments.py -q — 56 passed
  • env -u HERMES_CONFIG_PATH -u HERMES_WEBUI_HOST /home/michael/.hermes/hermes-agent/venv/bin/python -m pytest tests/test_779_html_preview.py::test_inline_html_response_sets_csp_sandbox tests/test_sprint2.py::test_raw_endpoint_path_traversal_blocked tests/test_sprint6.py::test_file_raw_serves_session_attachment_inbox tests/test_sprint6.py::test_file_raw_attachment_fallback_rejects_traversal -q — 4 passed
  • env -u HERMES_CONFIG_PATH -u HERMES_WEBUI_HOST /home/michael/.hermes/hermes-agent/venv/bin/python -m pytest tests/ -q — 5734 passed, 6 skipped, 3 xpassed, 8 subtests passed
  • python -m py_compile api/routes.py
  • git diff --check

Risks / Follow-ups

  • This intentionally does not expose _attachment_root() to the browser or switch the frontend to absolute media URLs.
  • The attachment fallback is scoped to _session_attachment_dir(session_id), not the attachment root, to avoid cross-session probing.

Closes #2380

Model Used

AI-assisted change with repository inspection, targeted editing, and shell-based test verification.

@Michaelyklam
Michaelyklam force-pushed the fix/issue-2380-attachment-file-raw branch from 75ee009 to 59b72fd Compare May 16, 2026 12:08
@Michaelyklam
Michaelyklam force-pushed the fix/issue-2380-attachment-file-raw branch from 59b72fd to e4dad1c Compare May 16, 2026 12:14
@nesquena-hermes nesquena-hermes closed this pull request by merging all changes into nesquena:master in 3de4338 May 16, 2026
eleboucher pushed a commit to eleboucher/homelab that referenced this pull request May 16, 2026
… 0.51.75) (#527)

This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [ghcr.io/nesquena/hermes-webui](https://github.com/nesquena/hermes-webui) | patch | `0.51.74` → `0.51.75` |

---

### Release Notes

<details>
<summary>nesquena/hermes-webui (ghcr.io/nesquena/hermes-webui)</summary>

### [`v0.51.75`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v05175--2026-05-16--Release-AY-stage-368--11-PR-safe-lane-batch--storage--i18n--run-journal-parity--attachments--compression-sidebar--restart-recovery--text-mode-images--tables--settings-i18n--German-labels)

[Compare Source](nesquena/hermes-webui@v0.51.74...v0.51.75)

##### Test infrastructure

- Stage-368 maintainer fix — pytest no longer self-loops on the `_schedule_restart` daemon thread. Several existing tests in `tests/test_update_banner_fixes.py` call `api.updates._schedule_restart()`, which spawns a daemon thread that eventually calls `os.execv()`. Those tests monkeypatch `os.execv` for the test scope, but monkeypatch teardown can win the race against the daemon thread, restoring the real `os.execv` before the thread fires it — at which point the daemon re-execs the entire pytest process with the original argv, looking from the outside like pytest hangs at 99 % then restarts the suite from 0 % in an infinite loop. `tests/conftest.py` now installs a permanent no-op wrapper on `os.execv` at module-import time so late-firing daemon threads cannot re-exec pytest. New `tests/test_pytest_execv_guard.py` pins the guard against future regressions.

##### Added

- **PR [#&#8203;2377](nesquena/hermes-webui#2377 by [@&#8203;franksong2702](https://github.com/franksong2702) (refs [#&#8203;2283](nesquena/hermes-webui#2283), refs [#&#8203;2363](nesquena/hermes-webui#2363), refs [#&#8203;1925](nesquena/hermes-webui#1925)) — Run-journal replay timeline parity checks. After [#&#8203;2283](nesquena/hermes-webui#2283) shipped the first run-journal replay slice and [#&#8203;2363](nesquena/hermes-webui#2363) documented the cross-layer state consistency contract, this PR adds explicit parity assertions over the replayed timeline so divergences between the journal and the visible transcript (Thinking → tool calls → assistant text) surface as test failures instead of silent drift.

##### Fixed

- **PR [#&#8203;2391](nesquena/hermes-webui#2391 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (fixes [#&#8203;2389](nesquena/hermes-webui#2389)) — Reduce browser storage pressure during service-worker updates and over long-running sessions. `static/sw.js` now calls `deleteOldShellCaches()` BEFORE `caches.open(CACHE_NAME)` in the install handler so the new \~2.2 MB shell cache no longer overlaps the old one during a version bump (especially painful on shared-origin quota accounting). A new `_clearSessionViewedCount()` helper plus extended `_clearHandoffStorageForSession()` prune `hermes-session-viewed-counts`, `hermes-session-completion-unread`, and `hermes-session-observed-streaming` on every single-session delete and batch-delete so per-session tracking maps no longer grow unbounded.

- **PR [#&#8203;2387](nesquena/hermes-webui#2387 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (fixes [#&#8203;2386](nesquena/hermes-webui#2386)) — Guard `localStorage.setItem('hermes-webui-session', ...)` and workspace-panel runtime-state writes with `try { … } catch (_) {}` across `static/boot.js`, `static/sessions.js`, `static/commands.js`, and `static/messages.js`. These convenience writes were previously fatal UI operations on quota-exhausted browsers (especially Firefox public-domain setups where shared quota fills up after a service-worker shell rotation).

- **PR [#&#8203;2368](nesquena/hermes-webui#2368 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) — Hybridize background profile env routing so background title generation, manual compression, and update-summary workers honor a session's non-default profile. The pure thread-local refactor for [#&#8203;2321](nesquena/hermes-webui#2321) was reverted because `hermes_cli.config.load_config()` still reads `HERMES_HOME` from process env. This PR keeps the thread-local layer for WebUI helpers and adds an `os.environ.update(runtime_env)` mirror under a narrow `_ENV_LOCK` for the worker body, with proper restore of prior values. New test asserts `OPENROUTER_API_KEY` is visible from the worker against a non-default profile.

- **PR [#&#8203;2382](nesquena/hermes-webui#2382 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (fixes [#&#8203;2380](nesquena/hermes-webui#2380)) — Serve raw chat attachments from the per-session inbox in addition to the session workspace. Chat uploads were intentionally moved out of workspaces into a per-session attachment inbox in an earlier release; the transcript renderer still emits stable `api/file/raw?session_id=...&path=<filename>` URLs, but `_handle_file_raw` only checked `session.workspace` so inbox-backed uploads rendered as broken images. The URL surface is preserved and a session-attachment fallback is added with path-traversal guards intact.

- **PR [#&#8203;2385](nesquena/hermes-webui#2385 by [@&#8203;franksong2702](https://github.com/franksong2702) — Keep fuller compression snapshots reachable in the sidebar. The default behavior hides `pre_compression_snapshot: true` rows so archived compression segments do not duplicate the active continuation. A real long Kanban session exposed a narrower failure: the fuller transcript was still present on disk but remained marked as `pre_compression_snapshot`, so the sidebar surfaced a shorter row and the fuller transcript became unreachable. The fix preserves discoverability without re-introducing duplication in normal cases.

- **PR [#&#8203;2371](nesquena/hermes-webui#2371 by [@&#8203;franksong2702](https://github.com/franksong2702) — Clarify interrupted turn recovery after a WebUI restart. WebUI executes browser-originated agent turns inside the WebUI process; if that process restarts mid-turn, the worker dies with it. Run journal replay can only replay events that were already emitted, so the stale-pending repair path is now annotated and refined to make the post-restart state explicit (interrupted, recoverable, or terminal) instead of leaving the user with a half-rendered turn and no signal.

- **PR [#&#8203;2378](nesquena/hermes-webui#2378 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) — Strip historical images in text-only mode. Current-turn uploads already respect `agent.image_input_mode: text`, but saved conversation history still passed native `image_url` content parts back into later provider calls, breaking text-only providers on replayed turns. `_sanitize_messages_for_api()` gains a `cfg=` keyword argument so the API-history sanitizer can strip historical native image parts when the mode is text. Default `cfg=None` preserves prior behavior for callers that don't pass the new argument.

- **PR [#&#8203;2375](nesquena/hermes-webui#2375 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) — Keep Markdown tables block-level. Pipe tables were already converted to `<table>` markup, but the final paragraph pass did not treat generated tables as block-level output, occasionally wrapping them in `<p>` and breaking the surrounding layout. The fix isolates generated tables and adds `table` to the paragraph-wrap skip list so valid CommonMark tables render predictably.

- **PR [#&#8203;2372](nesquena/hermes-webui#2372 by [@&#8203;mccxj](https://github.com/mccxj) — Settings → Conversation page action buttons now respect locale selection. Pre-fix, the JSON export, MD export, and Copy buttons had hardcoded English labels/titles. Adds `data-i18n` / `data-i18n-title` attributes plus the missing translation keys so non-English locales no longer see English labels stuck in the middle of a translated screen.

- **PR [#&#8203;2381](nesquena/hermes-webui#2381 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (fixes [#&#8203;2379](nesquena/hermes-webui#2379)) — German relative session-time labels now interpolate the elapsed value instead of rendering the literal `{n}` placeholder in the sidebar/header. The German locale now uses function-valued translations for minutes, hours, and days, matching the other locale bundles.

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these updates again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMDEuMSIsInVwZGF0ZWRJblZlciI6IjQzLjEwMS4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJyZW5vdmF0ZS9jb250YWluZXIiLCJ0eXBlL3BhdGNoIl19-->

Reviewed-on: https://git.erwanleboucher.dev/eleboucher/homelab/pulls/527
pull Bot pushed a commit to soitun/hermes-webui that referenced this pull request May 16, 2026
SysAdminDoc pushed a commit to SysAdminDoc/hermes-webui that referenced this pull request Jun 26, 2026
bernyforce pushed a commit to bernyforce/hermes-webui that referenced this pull request Jul 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

api/file/raw returns 404 for uploaded chat attachments (images stored in attachment dir, not workspace)

2 participants