Skip to content

bug(gateway): post-stream media delivery can upload bare local paths not intentionally present in the visible reply #20834

@TheEnigmaticT

Description

@TheEnigmaticT

Bug Description

In the gateway post-stream media delivery path, Hermes can upload a local file even when the final visible assistant reply did not intentionally request an attachment.

The failure mode I hit on Slack was:

  • the visible reply text sent to the user contained no MEDIA: directive,
  • but after the text message was sent, the gateway still uploaded an image based on a stale local path that had appeared in inspected content/tool output.

This is distinct from issues where a literal visible MEDIA:/... example in the assistant text gets treated as an attachment. In this case, the visible final reply was clean, but the post-stream helper rescanned the response and promoted a bare local filesystem path into a real upload.

Environment

  • Platform: Slack gateway
  • Repo area: gateway/run.py
  • Observed in a local git checkout after hermes update

Steps to Reproduce

  1. Run Hermes via the Slack gateway.
  2. Have the agent inspect content that contains a local path or archived MEDIA: reference to an existing image/file, e.g. tool output or searched content containing something like /absolute/path/to/file.png.
  3. Let the agent produce a normal streamed text reply that does not intentionally include an attachment directive in the visible final user-facing message.
  4. Observe gateway post-processing after the text has already been sent.

Expected Behavior

  • Post-stream media delivery should only send attachments that were explicitly intended for user delivery.
  • A normal streamed reply should not cause bare local paths from inspected/tool-output content to be uploaded as attachments.
  • If the visible final reply has no explicit attachment directive, no attachment should be sent just because a local path string appeared somewhere in the assembled response context.

Actual Behavior

The gateway can attach a file after streaming the text reply.

In my case:

  • Slack logs showed the normal text reply being sent first,
  • then files_upload_v2 ran afterward,
  • and the uploaded image came from stale inspected content rather than an intentional attachment request in the visible reply.

Representative log pattern:

INFO gateway.run: response ready: platform=slack ... response=11235 chars
INFO gateway.platforms.base: [Slack] Sending response (11164 chars) to ...
INFO gateway.platforms.slack: [Slack] Sending 1 image(s) in single files_upload_v2 (chunk 1/1)

In a later reproduction, the gateway also logged:

WARNING gateway.platforms.slack: [Slack] Skipping missing image: /.../jonathan_brill_mockup.png
INFO gateway.platforms.slack: [Slack] Sending 1 image(s) in single files_upload_v2 (chunk 1/1)

Root Cause

GatewayRunner._deliver_media_from_response() rescans the response after streaming and currently mixes two behaviors:

  • explicit attachment extraction (extract_media(response))
  • implicit local-path extraction from cleaned text (extract_local_files(cleaned))

The problematic flow was:

media_files, _ = adapter.extract_media(response)
_, cleaned = adapter.extract_images(response)
local_files, _ = adapter.extract_local_files(cleaned)

That extract_local_files(cleaned) step lets bare local filesystem paths found in post-processed response text become real attachments, even when the final visible reply did not intentionally request upload.

This is especially dangerous in streaming mode because:

  • the user-visible text has already been streamed/sanitized,
  • then the post-stream helper reparses the full response,
  • and hidden/internal/tool-derived path strings can be promoted into uploads.

Local Fix That Worked

I patched gateway/run.py so the post-stream helper stays explicit-only:

  • keep explicit MEDIA: extraction,
  • stop auto-attaching bare local paths from the post-stream response text.

Specifically, I removed extract_local_files(cleaned) and the follow-on post-stream send logic for non-explicit local files from _deliver_media_from_response().

Conceptually:

 media_files, _ = adapter.extract_media(response)
-_, cleaned = adapter.extract_images(response)
-local_files, _ = adapter.extract_local_files(cleaned)
+adapter.extract_images(response)

and removed the later block that sent non_image_local via send_video() / send_document().

Regression Test Added

I added a focused regression test file:

  • tests/gateway/test_post_stream_media_delivery.py

Coverage:

  1. post-stream delivery ignores a bare local path in the response text
  2. explicit MEDIA: directives still work

Targeted test results on my checkout:

  • tests/gateway/test_post_stream_media_delivery.py: 2 passed
  • tests/gateway/test_extract_local_files.py: 37 passed
  • combined targeted run with tests/gateway/test_run_progress_topics.py: 63 passed

Why This Matters

This is more than log noise:

  • it can leak internal/local file references into user-facing attachments,
  • it can upload the wrong file after an otherwise normal reply,
  • and it breaks the mental model that only explicit attachment directives produce uploads.

Related Issue

This seems related to #16434, but not identical.

A robust fix may want both:

  1. stricter MEDIA: extraction semantics
  2. no implicit bare-local-path attachment behavior in the post-stream delivery helper

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Medium — degraded but workaround existscomp/gatewayGateway runner, session dispatch, deliveryplatform/slackSlack app adaptertype/bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions