Skip to content

fix(gateway): allow source code/log extensions in MEDIA: delivery - #36060

Closed
Randimt wants to merge 1 commit into
NousResearch:mainfrom
Randimt:fix/media-delivery-source-code-extensions
Closed

fix(gateway): allow source code/log extensions in MEDIA: delivery#36060
Randimt wants to merge 1 commit into
NousResearch:mainfrom
Randimt:fix/media-delivery-source-code-extensions

Conversation

@Randimt

@Randimt Randimt commented May 31, 2026

Copy link
Copy Markdown
Contributor

Summary

The MEDIA: extension allowlist (MEDIA_DELIVERY_EXTS, introduced by #34844 / #34517) covered docs, spreadsheets, archives, and rendered web output but not source code, build manifests, or logs. A response of MEDIA:/path/to/script.py was silently dropped — extract_media() didn't match it, MEDIA_TAG_CLEANUP_RE left the tag in the body, and the file never reached the user. Same story for .ts/.tsx, .go, .rs, .sh, .sol, .toml, .log, etc. — exactly the file types most likely to be shared in a chat workflow (code review, snippet sharing, log triage).

This is a follow-up to #34844 that closes the remaining gap.

Changes

  • Extend MEDIA_DELIVERY_EXTS with source code (Python / JS / TS / shell / systems / JVM / scripting / Solidity / web), build metadata (.toml, .css), and .log.

    Sensitive single-purpose configs (.env, .conf, .ini) are deliberately excluded. The credential-path denylist in _media_delivery_denied_paths is the runtime backstop, but keeping these out of the extension allowlist is the cheaper, earlier layer of defence — a malicious prompt-injection that emits MEDIA:~/some/.env from a non-denied location is now ignored at the extractor instead of relying solely on the path check.

  • Replace two duplicated extension regexes in gateway/run.py (history scan + post-run scan) with imports of _MEDIA_EXT_ALTERNATION from gateway/platforms/base.py. They had been drifting from the source of truth that fix(gateway): unify MEDIA: extraction extension set + close the unknown-ext black hole (#34517) #34844 introduced; pulling them through the shared constant closes the gap and means future extension additions only need to land in one place.

Test plan

Two new regression tests in tests/gateway/test_platform_base.py::TestSourceCodeMediaDelivery:

  • Every newly-added extension extracts via MEDIA: tag.
  • .env / .conf / .ini stay out of the allowlist (anti-regression for the credential-leak guardrail).
$ scripts/run_tests.sh tests/gateway/test_platform_base.py
129 tests passed, 0 failed

Adjacent gateway tests (test_send_image_file.py, test_dingtalk.py) also still pass — no behaviour change on the existing allowed types.

Manual verification

Tested live on a private Discord-connected gateway: a reply containing MEDIA:/home/user/snippet.py now delivers as a native file attachment. Before the patch, the same reply showed only the surrounding prose with the path silently stripped.

The MEDIA: extension allowlist (MEDIA_DELIVERY_EXTS, follow-up to NousResearch#34517)
covered docs/spreadsheets/archives/web output but not source code, build
manifests, or logs. A response of 'MEDIA:/path/to/script.py' was silently
dropped — extract_media() didn't match it, MEDIA_TAG_CLEANUP_RE left the
tag in the body, and the file never reached the user. Same for .ts/.tsx,
.go, .rs, .sh, .sol, .toml, .log, etc. — the very file types most likely
to be shared in a chat workflow (code review, snippet sharing, log triage).

- Extend MEDIA_DELIVERY_EXTS with source code (Python/JS/TS/shell/systems
  /JVM/scripting/Solidity/web), build metadata (.toml, .css), and .log.
  Sensitive single-purpose configs (.env, .conf, .ini) are deliberately
  excluded — the credential-path denylist is the runtime backstop, but
  keeping these out of the extension allowlist is the cheaper layer of
  defence against accidental delivery from non-denied locations.

- Replace two duplicated extension regexes in gateway/run.py (history
  scan + post-run scan) with imports of _MEDIA_EXT_ALTERNATION from
  base.py. They were drifting from the source of truth that NousResearch#34517
  introduced; pulling them through the shared constant closes the gap.

- Regression tests: every new extension extracts via MEDIA: tag, and
  .env/.conf/.ini stay out of the allowlist (anti-regression for the
  credential-leak guardrail).
@alt-glitch alt-glitch added type/bug Something isn't working comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists labels May 31, 2026

@mxnstrexgl mxnstrexgl left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — automated review passed. No security, quality, or test coverage issues detected.

@teknium1

Copy link
Copy Markdown
Contributor

Thanks for identifying the explicit MEDIA: gap. The current implementation still rejects source-file tags, but the proposed shared-list change collides with a later deliberate delivery policy.

Problems

  • MEDIA_DELIVERY_EXTS drives both explicit tags and bare-path auto-delivery (gateway/platforms/base.py:1432-1436, 3745). Adding .py/.log here would auto-attach ordinary source paths, contrary to the documented policy in website/docs/user-guide/features/deliverable-mode.md:51-53 and its regression test at tests/gateway/test_extract_local_files.py:322-332.
  • The gateway/run.py hunks are stale: current main centralizes this in _TOOL_MEDIA_RE at gateway/run.py:1061-1066, reused by collection at lines 1134, 1141, and 1180. The PR is currently conflicting, so those old sites cannot be salvaged directly.

Suggested changes

  • Please first confirm the intended distinction between explicit MEDIA: delivery and bare-path auto-delivery. If explicit source delivery is desired, preserve the existing bare-path exclusion with a separate explicit-tag mechanism and test both behaviors.
  • Port any tool-result parsing changes to the current collector rather than the removed local regex sites.

Automated hermes-sweeper review.

@teknium1 teknium1 added sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 13, 2026
teknium1 added a commit that referenced this pull request Jul 16, 2026
…gress

MEDIA: tags whose path had an unknown extension (.py, .log, .toml,
.weirdext, ...) fell between both extraction passes: the anchored
extension allowlist (MEDIA_TAG_CLEANUP_RE) did not match them, and the
extension-less pass explicitly skipped any path that HAD a suffix. The
file was never delivered even though the intended design (universal
ingress/egress) says any non-credential file should ship.

Widen _path_lacks_deliverable_extension() so the validated delivery
pass (MEDIA_EXTENSIONLESS_TAG_RE + validate_media_delivery_path)
covers every path the extension allowlist does not — unknown
extensions and extension-less files alike. Security posture is
unchanged: unknown-extension paths only deliver after full validation
(exists, symlinks resolved, credential/system denylist, strict-mode
allowlist+recency), and unvalidated tags stay visible in the text
instead of being silently dropped. Known extensions keep their
unconditional pre-existing behavior.

Because extract_media, _strip_media_tag_directives (non-streaming
dispatch), and strip_media_directives_for_display (streaming) all
share the same two regexes + predicate, all delivery paths pick up the
widened behavior with no per-site changes. Dispatch partition in
gateway/run.py already routes non-image/video extensions through
send_document.

Closes the gap reported in PR #36060; supersedes the allowlist-append
approach there (an extension allowlist can never enumerate every file
type a user asks the agent to produce).

Co-authored-by: Randimt <randimt@users.noreply.github.com>
@teknium1

Copy link
Copy Markdown
Contributor

Merged via #65510 (rebase-merged onto current main, head commit 2fd36b1) — your extension-list idea was widened into universal validated egress — any MEDIA: path now delivers after passing the credential/system-path validation, so no allowlist maintenance is ever needed again. You're credited as Co-authored-by on the commit. Thanks for the contribution!

@teknium1 teknium1 closed this Jul 16, 2026
exiao added a commit to exiao/hermes-agent that referenced this pull request Jul 20, 2026
Third baseline failure (test_platform_base.py::TestUniversalMediaEgress x3).

The egress redesign (2fd36b1, NousResearch#36060) intends unknown/code extensions to
deliver ONLY via the validated pass (validate_media_delivery_path: exists on
disk, safe root, not denylisted) so a prompt-injection MEDIA:/etc/x.py stays
visible instead of silently exfiltrating. But the code still folded
MEDIA_TAG_EXTRA_EXTS (.py/.log/.toml/...) into _MEDIA_TAG_EXT_ALTERNATION, the
UNCONDITIONAL extract grammar — contradicting its own design comment and the
egress tests. Point _MEDIA_TAG_EXT_ALTERNATION at MEDIA_DELIVERY_EXTS only, so
code/config/log tags route through the validated pass. MEDIA_TAG_EXTRA_EXTS is
retained for the separate producer-tool detector in run.py (_TOOL_MEDIA_RE).

test_send_file_tool's expanded-ext test used nonexistent /tmp paths, which
under the validated design correctly no longer extract. Fixed the fixture to
create a real file in a monkeypatched safe root, exercising the true intended
delivery path for every extension.

Verified: TestUniversalMediaEgress 13/13, test_send_file_tool 85/85, full
affected set 426 passed. The 5 unrelated red tests (SSRF, url_safety,
cross_profile_guard) are pre-existing on clean live-config and untouched here.

Approach A per Eric: egress design authoritative.
exiao added a commit to exiao/hermes-agent that referenced this pull request Jul 20, 2026
…e shadow handlers) (#139)

* fix(gateway): delete stale shadow slash-handlers + raise KANBAN_GUIDANCE ceiling

Consolidates the live-config CI baseline repair (supersedes #136, #137, #138).

15 tests were failing on live-config across 4 modules. This fixes 12 via the
correct root cause; 3 egress tests are a separate product decision (see patch note).

1. Stale shadow handlers (11 tests: test_fast_command x3, test_choice_picker x8).
   The June slash-command extraction moved _handle_reasoning_command and
   _handle_fast_command into GatewaySlashCommandsMixin with the new choice-picker
   logic, but left the OLD copies in the GatewayRunner class body in run.py. A
   method defined on the subclass overrides an inherited mixin method, so the
   stale copies shadowed the picker-enabled versions, making the choice picker
   unreachable and /fast --global unrecognized. Delete the two stale defs (166
   lines) so the mixin versions activate. #137's re-implement-into-the-shadow
   approach was wrong; the shadow must be deleted.

2. KANBAN_GUIDANCE size ceiling (1 test). Upstream b5bd0ef added legitimate
   attachment guidance (5749 chars vs the <5500 cap). Raise cap to 6500.

219 tests pass across the affected modules; send_file's 57 expanded-ext tests
still pass (unlike #138 which broke them). ruff clean.

Patch note: ~/.hermes/plans/hermes-patches/live-config-baseline-consolidated.md

* fix(gateway): route MEDIA-tag code/config exts through validated egress

Third baseline failure (test_platform_base.py::TestUniversalMediaEgress x3).

The egress redesign (2fd36b1, NousResearch#36060) intends unknown/code extensions to
deliver ONLY via the validated pass (validate_media_delivery_path: exists on
disk, safe root, not denylisted) so a prompt-injection MEDIA:/etc/x.py stays
visible instead of silently exfiltrating. But the code still folded
MEDIA_TAG_EXTRA_EXTS (.py/.log/.toml/...) into _MEDIA_TAG_EXT_ALTERNATION, the
UNCONDITIONAL extract grammar — contradicting its own design comment and the
egress tests. Point _MEDIA_TAG_EXT_ALTERNATION at MEDIA_DELIVERY_EXTS only, so
code/config/log tags route through the validated pass. MEDIA_TAG_EXTRA_EXTS is
retained for the separate producer-tool detector in run.py (_TOOL_MEDIA_RE).

test_send_file_tool's expanded-ext test used nonexistent /tmp paths, which
under the validated design correctly no longer extract. Fixed the fixture to
create a real file in a monkeypatched safe root, exercising the true intended
delivery path for every extension.

Verified: TestUniversalMediaEgress 13/13, test_send_file_tool 85/85, full
affected set 426 passed. The 5 unrelated red tests (SSRF, url_safety,
cross_profile_guard) are pre-existing on clean live-config and untouched here.

Approach A per Eric: egress design authoritative.

* fix(gateway): honor effective model for fast mode
Gravezzz pushed a commit to Gravezzz/hermes-agent that referenced this pull request Jul 21, 2026
…gress

MEDIA: tags whose path had an unknown extension (.py, .log, .toml,
.weirdext, ...) fell between both extraction passes: the anchored
extension allowlist (MEDIA_TAG_CLEANUP_RE) did not match them, and the
extension-less pass explicitly skipped any path that HAD a suffix. The
file was never delivered even though the intended design (universal
ingress/egress) says any non-credential file should ship.

Widen _path_lacks_deliverable_extension() so the validated delivery
pass (MEDIA_EXTENSIONLESS_TAG_RE + validate_media_delivery_path)
covers every path the extension allowlist does not — unknown
extensions and extension-less files alike. Security posture is
unchanged: unknown-extension paths only deliver after full validation
(exists, symlinks resolved, credential/system denylist, strict-mode
allowlist+recency), and unvalidated tags stay visible in the text
instead of being silently dropped. Known extensions keep their
unconditional pre-existing behavior.

Because extract_media, _strip_media_tag_directives (non-streaming
dispatch), and strip_media_directives_for_display (streaming) all
share the same two regexes + predicate, all delivery paths pick up the
widened behavior with no per-site changes. Dispatch partition in
gateway/run.py already routes non-image/video extensions through
send_document.

Closes the gap reported in PR NousResearch#36060; supersedes the allowlist-append
approach there (an extension allowlist can never enumerate every file
type a user asks the agent to produce).

Co-authored-by: Randimt <randimt@users.noreply.github.com>
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
…gress

MEDIA: tags whose path had an unknown extension (.py, .log, .toml,
.weirdext, ...) fell between both extraction passes: the anchored
extension allowlist (MEDIA_TAG_CLEANUP_RE) did not match them, and the
extension-less pass explicitly skipped any path that HAD a suffix. The
file was never delivered even though the intended design (universal
ingress/egress) says any non-credential file should ship.

Widen _path_lacks_deliverable_extension() so the validated delivery
pass (MEDIA_EXTENSIONLESS_TAG_RE + validate_media_delivery_path)
covers every path the extension allowlist does not — unknown
extensions and extension-less files alike. Security posture is
unchanged: unknown-extension paths only deliver after full validation
(exists, symlinks resolved, credential/system denylist, strict-mode
allowlist+recency), and unvalidated tags stay visible in the text
instead of being silently dropped. Known extensions keep their
unconditional pre-existing behavior.

Because extract_media, _strip_media_tag_directives (non-streaming
dispatch), and strip_media_directives_for_display (streaming) all
share the same two regexes + predicate, all delivery paths pick up the
widened behavior with no per-site changes. Dispatch partition in
gateway/run.py already routes non-image/video extensions through
send_document.

Closes the gap reported in PR NousResearch#36060; supersedes the allowlist-append
approach there (an extension allowlist can never enumerate every file
type a user asks the agent to produce).

Co-authored-by: Randimt <randimt@users.noreply.github.com>
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 sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants