Skip to content

fix(gateway): add canonical cache paths to MEDIA_DELIVERY_SAFE_ROOTS#31764

Open
tvy14 wants to merge 1 commit into
NousResearch:mainfrom
tvy14:fix/31733-media-delivery-safe-roots
Open

fix(gateway): add canonical cache paths to MEDIA_DELIVERY_SAFE_ROOTS#31764
tvy14 wants to merge 1 commit into
NousResearch:mainfrom
tvy14:fix/31733-media-delivery-safe-roots

Conversation

@tvy14

@tvy14 tvy14 commented May 25, 2026

Copy link
Copy Markdown

Summary

image_generate saves generated images under ~/.hermes/cache/images/, but the gateway's MEDIA_DELIVERY_SAFE_ROOTS only validated paths under the legacy ~/.hermes/image_cache/ and the IMAGE_CACHE_DIR variable — which resolves to the legacy path when it exists on disk, creating a mismatch.

This adds the canonical new paths (cache/images, cache/audio, cache/videos, cache/documents, cache/screenshots) alongside the existing legacy paths so generated media is deliverable regardless of cache layout.

Changes

  • gateway/platforms/base.py: Added 5 canonical paths to MEDIA_DELIVERY_SAFE_ROOTS
  • tests/gateway/test_platform_base.py: Added 3 regression tests

Testing

8 passed in 0.38s

Fixes #31733

image_generate saves generated images under ~/.hermes/cache/images/,
but MEDIA delivery on Telegram only validated paths under the legacy
~/.hermes/image_cache/ and the IMAGE_CACHE_DIR variable (which resolves
to the legacy path when it exists on disk).

Add the canonical new paths explicitly so generated images are deliverable
regardless of which cache layout the user has.

Fixes NousResearch#31733
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/gateway Gateway runner, session dispatch, delivery labels May 25, 2026
@GodsBoy

GodsBoy commented May 26, 2026

Copy link
Copy Markdown
Contributor

I hit this same bug today on a Linux Telegram DM flow, and the fix direction here looks right.

Additional repro context from #31733:

  • Generated image was saved under /root/.hermes/cache/images/....
  • Final-response MEDIA:/root/.hermes/cache/images/... did not show as an attachment in Telegram.
  • Retrying the same path also did not show.
  • Sending the same path through send_message returned success, but still did not display for the user.
  • Copying the original PNG to /root/.hermes/image_cache/... and sending from there worked reliably, including as a PNG file attachment.

So explicitly adding the canonical cache/images root to the gateway media allowlist is the right core fix. I also agree with adding the sibling canonical cache roots for audio, video, documents, and screenshots so this does not become the same bug in another media path.

One thing I would tighten before merge: the tests added here mostly monkeypatch MEDIA_DELIVERY_SAFE_ROOTS to include the path under test. That proves the existing generic validation behaviour, but it does not strongly prove the real default safe-root list now includes ~/.hermes/cache/images when ~/.hermes/image_cache also exists.

A stronger regression test would assert the default/media-root construction includes the canonical cache dirs, or simulate the actual failure shape:

  1. legacy image_cache/ exists
  2. canonical cache/images/ also has a generated file
  3. validate_media_delivery_path() accepts the generated file under cache/images/

That distinction matters because the original failure is caused by get_hermes_dir("cache/images", "image_cache") resolving to the legacy dir when it exists, while image_generate writes directly to cache/images.

Also worth rebasing this against current main; GitHub reports the PR as conflicting. There is related security work around denying the broader Hermes home tree for media delivery, so the final shape should keep explicit cache-dir allowlisting checked before any broader ~/.hermes denylist.

Net: this is the right fix direction, but I would strengthen the regression tests and resolve the conflicts before merge.

@GodsBoy

GodsBoy commented May 27, 2026

Copy link
Copy Markdown
Contributor

Quick follow-up: I have opened #33251 as a complementary draft PR covering the same MEDIA_DELIVERY_SAFE_ROOTS direction as this one, plus the broader operator-diagnosability bug surface I hit on Linux Telegram DM (a valid PDF under /tmp/pool_evidence/... failing to deliver from the final-response path while send_message succeeded, with no actionable signal in the gateway log).

What #33251 adds on top of the canonical-roots change:

  • Kebab-string rejection reasons (outside-allowlist-no-recency, outside-allowlist-stale-mtime, outside-allowlist-denied-prefix, does-not-resolve, etc.) surfaced from _validate_media_delivery_path_with_reason and logged by filter_media_delivery_paths / filter_local_delivery_paths with a redacted path. "MEDIA didn't attach" is currently diagnosable only by source-diving.
  • Control-character sanitisation in the redacted log so a model-emitted path with an embedded newline cannot forge a second log line.
  • The strengthened legacy-and-canonical co-exist regression test the review thread here asked for (legacy image_cache/ exists on disk, canonical cache/images/ also has a generated file, the canonical file is deliverable through the default safe-roots tuple).
  • PDF / document delivery regression tests under canonical cache/documents and legacy document_cache.
  • send_message tool / final-response extraction parity via source introspection so a future split surfaces in tests.

Happy to fold whatever subset of #33251 is useful into this PR (or close #33251 in favour of this one) — whichever path you and the maintainers prefer. If #31764 lands first, the safe-roots block in #33251 is a clean merge against it.

GodsBoy added a commit to GodsBoy/hermes-agent that referenced this pull request May 29, 2026
…sResearch#31733)

Builds on the direction in NousResearch#31764 with a broader cut at the same bug
surface that surfaces in operator reports as "MEDIA tag did not attach":

* MEDIA_DELIVERY_SAFE_ROOTS now lists the canonical
  ~/.hermes/cache/{images,audio,videos,documents,screenshots} explicitly
  alongside the legacy ~/.hermes/{type}_cache/ entries. get_hermes_dir
  returns the legacy path when it exists, so IMAGE_CACHE_DIR alone drops
  the canonical layout on co-existing installs (root cause of NousResearch#31733).

* validate_media_delivery_path now delegates to a private
  _validate_media_delivery_path_with_reason helper that returns
  (resolved_or_None, kebab_reason). filter_media_delivery_paths and
  filter_local_delivery_paths log the reason plus a redacted path
  (_redact_path_for_log), so operators chasing rejected MEDIA tags can
  tell allowlist-miss from stale-mtime from denied-prefix from
  does-not-resolve without rebuilding the bot with debug logging.

* _redact_path_for_log neutralises control characters (newline, NUL,
  carriage return) so a model-emitted path containing an embedded
  newline cannot forge a second log line.

* Hoisted _FINAL_RESPONSE_IMAGE_EXTS / _FINAL_RESPONSE_VIDEO_EXTS to
  module scope so the regression test imports the same partition the
  final-response dispatch actually uses, instead of snapshotting a
  local copy that could drift.

* Regression coverage added in tests/gateway/test_platform_base.py:
  - default safe-roots tuple contains both legacy and canonical layouts
  - legacy and canonical image caches co-exist (the strengthened
    failure-shape test NousResearch#31764 review asked for)
  - PDF deliverable from canonical ~/.hermes/cache/documents and
    legacy ~/.hermes/document_cache
  - filter_*_delivery_paths logs the redacted path and the kebab
    reason tag (using the imported _MEDIA_REASON_* constants, not raw
    strings) for stale-mtime, no-recency, does-not-resolve,
    denied-prefix, and newline-injection cases
  - validate_media_delivery_path delegation invariant: public API
    matches the resolved path returned by the reason-bearing helper
  - send_message tool and final-response paths share extract_media +
    filter_media_delivery_paths (asserted via source introspection so
    a future split surfaces here)
  - PDF MEDIA tag routes to send_document via the production
    extension partition (no image / video misroute)

Scope: gateway media delivery only. Does not duplicate NousResearch#32472's
extension-derivation refactor or NousResearch#31561 / NousResearch#33206's markdown attachment
recognition work; if any of those land first, the diagnosable-logging
contract here still applies and the canonical-root entries become a
clean merge against NousResearch#31764.

Deferred follow-up (out of scope for this PR):
- Surfacing the rejection reason to the calling agent (send_message
  tool currently returns {success: true} regardless of dropped
  attachments)
- Unifying the three dispatch-site extension sets across
  _process_message_background, _deliver_media_from_response, and the
  send_message tool path
- Pre-existing residual: symlink-swap of a cache root before
  image_generate writes; TOCTOU between resolve() and upload;
  /var/tmp / /var/cache not in denylist
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Generated images under cache/images may not deliver via Telegram MEDIA, while image_cache works

3 participants