fix(gateway): allow native delivery of freshly-produced agent files#32060
Merged
Conversation
The gateway's media delivery allowlist required files live inside
`~/.hermes/cache/{documents,images,...}`, which is the wrong shape for
real agent usage. Agents naturally produce artifacts via terminal tools
(`pandoc -o /tmp/report.pdf`, `matplotlib savefig`, etc.) or
write_file into project directories — these never land under the cache.
Result: users got a raw file path in chat instead of an attachment.
This is doubly bad in deployment shapes where the cache directories
aren't writable by the agent at all: Hermes running in Docker with a
read-only mount, or with a Docker/Modal/SSH terminal backend whose
filesystem isn't the gateway host's filesystem.
Layered trust model:
1. Cache-dir allowlist (unchanged) — Hermes-managed roots always trusted.
2. Operator allowlist — `HERMES_MEDIA_ALLOW_DIRS` env var, now also
surfaced as `gateway.media_delivery_allow_dirs` in config.yaml.
3. Recency-based trust (new, default on) — files whose mtime is within
`gateway.trust_recent_files_seconds` (default 600s) of "now" are
trusted even outside the cache/operator allowlist. Old host files
(`/etc/passwd`, `~/.bashrc`, `~/.ssh/id_rsa`) have mtimes measured
in days/months, well outside the window — prompt-injection paths
pointing at pre-existing files are still rejected.
4. Hard denylist — `/etc`, `/proc`, `/sys`, `/dev`, `/root`, `/boot`,
`/var/{log,lib,run}`, plus `$HOME/.{ssh,aws,gnupg,kube,docker,config,
azure,gcloud}` and `Library/Keychains`. Denylist blocks delivery
even when recency would trust the file, in case an attacker
somehow refreshes a sensitive file's mtime.
Operators who want strict-allowlist behavior set
`gateway.trust_recent_files: false` and the system reverts to
pre-existing behavior.
Tests: 6 new cases in test_platform_base.py cover the recency window,
disabled mode, system-path denylist, and the motivating PDF-in-project
scenario. 3 existing tests (test_platform_base, test_tts_media_routing,
test_send_message_tool) that exercised the strict-allowlist path are
updated to disable recency trust explicitly.
E2E validation: real `validate_media_delivery_path()` accepts fresh
PDFs in /tmp and project dirs, rejects /etc/passwd, ~/.ssh/id_rsa, and
files older than the window; config.yaml `gateway.*` keys bridge
correctly to the env vars the validator reads.
Contributor
🔎 Lint report:
|
8 tasks
daletkc
pushed a commit
to daletkc/hermes-agent
that referenced
this pull request
May 25, 2026
…ousResearch#32060) The gateway's media delivery allowlist required files live inside `~/.hermes/cache/{documents,images,...}`, which is the wrong shape for real agent usage. Agents naturally produce artifacts via terminal tools (`pandoc -o /tmp/report.pdf`, `matplotlib savefig`, etc.) or write_file into project directories — these never land under the cache. Result: users got a raw file path in chat instead of an attachment. This is doubly bad in deployment shapes where the cache directories aren't writable by the agent at all: Hermes running in Docker with a read-only mount, or with a Docker/Modal/SSH terminal backend whose filesystem isn't the gateway host's filesystem. Layered trust model: 1. Cache-dir allowlist (unchanged) — Hermes-managed roots always trusted. 2. Operator allowlist — `HERMES_MEDIA_ALLOW_DIRS` env var, now also surfaced as `gateway.media_delivery_allow_dirs` in config.yaml. 3. Recency-based trust (new, default on) — files whose mtime is within `gateway.trust_recent_files_seconds` (default 600s) of "now" are trusted even outside the cache/operator allowlist. Old host files (`/etc/passwd`, `~/.bashrc`, `~/.ssh/id_rsa`) have mtimes measured in days/months, well outside the window — prompt-injection paths pointing at pre-existing files are still rejected. 4. Hard denylist — `/etc`, `/proc`, `/sys`, `/dev`, `/root`, `/boot`, `/var/{log,lib,run}`, plus `$HOME/.{ssh,aws,gnupg,kube,docker,config, azure,gcloud}` and `Library/Keychains`. Denylist blocks delivery even when recency would trust the file, in case an attacker somehow refreshes a sensitive file's mtime. Operators who want strict-allowlist behavior set `gateway.trust_recent_files: false` and the system reverts to pre-existing behavior. Tests: 6 new cases in test_platform_base.py cover the recency window, disabled mode, system-path denylist, and the motivating PDF-in-project scenario. 3 existing tests (test_platform_base, test_tts_media_routing, test_send_message_tool) that exercised the strict-allowlist path are updated to disable recency trust explicitly. E2E validation: real `validate_media_delivery_path()` accepts fresh PDFs in /tmp and project dirs, rejects /etc/passwd, ~/.ssh/id_rsa, and files older than the window; config.yaml `gateway.*` keys bridge correctly to the env vars the validator reads.
mathias3
pushed a commit
to mathias3/hermes-agent
that referenced
this pull request
May 28, 2026
…ousResearch#32060) The gateway's media delivery allowlist required files live inside `~/.hermes/cache/{documents,images,...}`, which is the wrong shape for real agent usage. Agents naturally produce artifacts via terminal tools (`pandoc -o /tmp/report.pdf`, `matplotlib savefig`, etc.) or write_file into project directories — these never land under the cache. Result: users got a raw file path in chat instead of an attachment. This is doubly bad in deployment shapes where the cache directories aren't writable by the agent at all: Hermes running in Docker with a read-only mount, or with a Docker/Modal/SSH terminal backend whose filesystem isn't the gateway host's filesystem. Layered trust model: 1. Cache-dir allowlist (unchanged) — Hermes-managed roots always trusted. 2. Operator allowlist — `HERMES_MEDIA_ALLOW_DIRS` env var, now also surfaced as `gateway.media_delivery_allow_dirs` in config.yaml. 3. Recency-based trust (new, default on) — files whose mtime is within `gateway.trust_recent_files_seconds` (default 600s) of "now" are trusted even outside the cache/operator allowlist. Old host files (`/etc/passwd`, `~/.bashrc`, `~/.ssh/id_rsa`) have mtimes measured in days/months, well outside the window — prompt-injection paths pointing at pre-existing files are still rejected. 4. Hard denylist — `/etc`, `/proc`, `/sys`, `/dev`, `/root`, `/boot`, `/var/{log,lib,run}`, plus `$HOME/.{ssh,aws,gnupg,kube,docker,config, azure,gcloud}` and `Library/Keychains`. Denylist blocks delivery even when recency would trust the file, in case an attacker somehow refreshes a sensitive file's mtime. Operators who want strict-allowlist behavior set `gateway.trust_recent_files: false` and the system reverts to pre-existing behavior. Tests: 6 new cases in test_platform_base.py cover the recency window, disabled mode, system-path denylist, and the motivating PDF-in-project scenario. 3 existing tests (test_platform_base, test_tts_media_routing, test_send_message_tool) that exercised the strict-allowlist path are updated to disable recency trust explicitly. E2E validation: real `validate_media_delivery_path()` accepts fresh PDFs in /tmp and project dirs, rejects /etc/passwd, ~/.ssh/id_rsa, and files older than the window; config.yaml `gateway.*` keys bridge correctly to the env vars the validator reads.
Bryce-huang
pushed a commit
to wbkunlun/hermes-agent
that referenced
this pull request
May 29, 2026
…ousResearch#32060) The gateway's media delivery allowlist required files live inside `~/.hermes/cache/{documents,images,...}`, which is the wrong shape for real agent usage. Agents naturally produce artifacts via terminal tools (`pandoc -o /tmp/report.pdf`, `matplotlib savefig`, etc.) or write_file into project directories — these never land under the cache. Result: users got a raw file path in chat instead of an attachment. This is doubly bad in deployment shapes where the cache directories aren't writable by the agent at all: Hermes running in Docker with a read-only mount, or with a Docker/Modal/SSH terminal backend whose filesystem isn't the gateway host's filesystem. Layered trust model: 1. Cache-dir allowlist (unchanged) — Hermes-managed roots always trusted. 2. Operator allowlist — `HERMES_MEDIA_ALLOW_DIRS` env var, now also surfaced as `gateway.media_delivery_allow_dirs` in config.yaml. 3. Recency-based trust (new, default on) — files whose mtime is within `gateway.trust_recent_files_seconds` (default 600s) of "now" are trusted even outside the cache/operator allowlist. Old host files (`/etc/passwd`, `~/.bashrc`, `~/.ssh/id_rsa`) have mtimes measured in days/months, well outside the window — prompt-injection paths pointing at pre-existing files are still rejected. 4. Hard denylist — `/etc`, `/proc`, `/sys`, `/dev`, `/root`, `/boot`, `/var/{log,lib,run}`, plus `$HOME/.{ssh,aws,gnupg,kube,docker,config, azure,gcloud}` and `Library/Keychains`. Denylist blocks delivery even when recency would trust the file, in case an attacker somehow refreshes a sensitive file's mtime. Operators who want strict-allowlist behavior set `gateway.trust_recent_files: false` and the system reverts to pre-existing behavior. Tests: 6 new cases in test_platform_base.py cover the recency window, disabled mode, system-path denylist, and the motivating PDF-in-project scenario. 3 existing tests (test_platform_base, test_tts_media_routing, test_send_message_tool) that exercised the strict-allowlist path are updated to disable recency trust explicitly. E2E validation: real `validate_media_delivery_path()` accepts fresh PDFs in /tmp and project dirs, rejects /etc/passwd, ~/.ssh/id_rsa, and files older than the window; config.yaml `gateway.*` keys bridge correctly to the env vars the validator reads. #AI commit#
mosaiq-systems
pushed a commit
to mosaiq-systems/hermes-agent
that referenced
this pull request
May 29, 2026
…ousResearch#32060) The gateway's media delivery allowlist required files live inside `~/.hermes/cache/{documents,images,...}`, which is the wrong shape for real agent usage. Agents naturally produce artifacts via terminal tools (`pandoc -o /tmp/report.pdf`, `matplotlib savefig`, etc.) or write_file into project directories — these never land under the cache. Result: users got a raw file path in chat instead of an attachment. This is doubly bad in deployment shapes where the cache directories aren't writable by the agent at all: Hermes running in Docker with a read-only mount, or with a Docker/Modal/SSH terminal backend whose filesystem isn't the gateway host's filesystem. Layered trust model: 1. Cache-dir allowlist (unchanged) — Hermes-managed roots always trusted. 2. Operator allowlist — `HERMES_MEDIA_ALLOW_DIRS` env var, now also surfaced as `gateway.media_delivery_allow_dirs` in config.yaml. 3. Recency-based trust (new, default on) — files whose mtime is within `gateway.trust_recent_files_seconds` (default 600s) of "now" are trusted even outside the cache/operator allowlist. Old host files (`/etc/passwd`, `~/.bashrc`, `~/.ssh/id_rsa`) have mtimes measured in days/months, well outside the window — prompt-injection paths pointing at pre-existing files are still rejected. 4. Hard denylist — `/etc`, `/proc`, `/sys`, `/dev`, `/root`, `/boot`, `/var/{log,lib,run}`, plus `$HOME/.{ssh,aws,gnupg,kube,docker,config, azure,gcloud}` and `Library/Keychains`. Denylist blocks delivery even when recency would trust the file, in case an attacker somehow refreshes a sensitive file's mtime. Operators who want strict-allowlist behavior set `gateway.trust_recent_files: false` and the system reverts to pre-existing behavior. Tests: 6 new cases in test_platform_base.py cover the recency window, disabled mode, system-path denylist, and the motivating PDF-in-project scenario. 3 existing tests (test_platform_base, test_tts_media_routing, test_send_message_tool) that exercised the strict-allowlist path are updated to disable recency trust explicitly. E2E validation: real `validate_media_delivery_path()` accepts fresh PDFs in /tmp and project dirs, rejects /etc/passwd, ~/.ssh/id_rsa, and files older than the window; config.yaml `gateway.*` keys bridge correctly to the env vars the validator reads.
gweeteve
pushed a commit
to gweeteve/hermes-agent
that referenced
this pull request
Jun 2, 2026
…ousResearch#32060) The gateway's media delivery allowlist required files live inside `~/.hermes/cache/{documents,images,...}`, which is the wrong shape for real agent usage. Agents naturally produce artifacts via terminal tools (`pandoc -o /tmp/report.pdf`, `matplotlib savefig`, etc.) or write_file into project directories — these never land under the cache. Result: users got a raw file path in chat instead of an attachment. This is doubly bad in deployment shapes where the cache directories aren't writable by the agent at all: Hermes running in Docker with a read-only mount, or with a Docker/Modal/SSH terminal backend whose filesystem isn't the gateway host's filesystem. Layered trust model: 1. Cache-dir allowlist (unchanged) — Hermes-managed roots always trusted. 2. Operator allowlist — `HERMES_MEDIA_ALLOW_DIRS` env var, now also surfaced as `gateway.media_delivery_allow_dirs` in config.yaml. 3. Recency-based trust (new, default on) — files whose mtime is within `gateway.trust_recent_files_seconds` (default 600s) of "now" are trusted even outside the cache/operator allowlist. Old host files (`/etc/passwd`, `~/.bashrc`, `~/.ssh/id_rsa`) have mtimes measured in days/months, well outside the window — prompt-injection paths pointing at pre-existing files are still rejected. 4. Hard denylist — `/etc`, `/proc`, `/sys`, `/dev`, `/root`, `/boot`, `/var/{log,lib,run}`, plus `$HOME/.{ssh,aws,gnupg,kube,docker,config, azure,gcloud}` and `Library/Keychains`. Denylist blocks delivery even when recency would trust the file, in case an attacker somehow refreshes a sensitive file's mtime. Operators who want strict-allowlist behavior set `gateway.trust_recent_files: false` and the system reverts to pre-existing behavior. Tests: 6 new cases in test_platform_base.py cover the recency window, disabled mode, system-path denylist, and the motivating PDF-in-project scenario. 3 existing tests (test_platform_base, test_tts_media_routing, test_send_message_tool) that exercised the strict-allowlist path are updated to disable recency trust explicitly. E2E validation: real `validate_media_delivery_path()` accepts fresh PDFs in /tmp and project dirs, rejects /etc/passwd, ~/.ssh/id_rsa, and files older than the window; config.yaml `gateway.*` keys bridge correctly to the env vars the validator reads.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Agent-produced files outside
~/.hermes/cache/*now upload as native gateway attachments instead of leaving a raw filepath in chat. Discord/Telegram/etc. PDF reports work again.Root cause: the cache-only allowlist mismatched real agent usage. Agents produce artifacts via
pandoc -o /tmp/report.pdf,matplotlib savefig,write_file('~/reports/foo.pdf', ...)— never into~/.hermes/cache/documents/. Worse, deployment shapes where the cache isn't writable (Hermes-in-Docker with read-only mount, Docker/Modal/SSH terminal backends) had no working path at all.Trust model (in order)
~/.hermes/cache/{documents,images,audio,video,screenshots}— always trusted (unchanged).gateway.media_delivery_allow_dirs(new config) +HERMES_MEDIA_ALLOW_DIRSenv — operator-trusted roots.gateway.trust_recent_files_seconds(default 600s) are accepted. Pre-existing host files (/etc/passwd,~/.bashrc,~/.ssh/id_rsa) have mtimes measured in days/months, well outside the window — prompt-injected paths pointing at old files are still rejected./etc /proc /sys /dev /root /boot /var/{log,lib,run},~/.{ssh,aws,gnupg,kube,docker,config,azure,gcloud},Library/Keychains, plus~/.hermes/{auth.json,.env,credentials}. Blocks delivery even when recency would accept (belt-and-braces against mtime-touch attacks).Strict-allowlist users set
gateway.trust_recent_files: falseand the system reverts to pre-existing behavior.Changes
gateway/platforms/base.py_media_delivery_recency_seconds(),_media_delivery_denied_paths(),_path_under_denied_prefix(),_file_is_recently_produced().validate_media_delivery_path()falls back to recency-trust when outside allowlists.gateway/run.pygateway.media_delivery_allow_dirs,gateway.trust_recent_files,gateway.trust_recent_files_secondsfromconfig.yamlto env vars.hermes_cli/config.pygateway:section inDEFAULT_CONFIGwith documented keys.tests/gateway/test_platform_base.pytests/gateway/test_tts_media_routing.py,tests/tools/test_send_message_tool.pyValidation
TestMediaDeliveryPathValidation(11 cases, 5 original + 6 new)tests/gateway/ tests/tools/(11,381)execute_codewith real imports/etc/passwd&~/.ssh/id_rsadenied, recency-off restores strict mode, operator allowlist worksgateway.*keys inconfig.yamlcorrectly bridge toHERMES_MEDIA_*env varsThreat model
The original allowlist defends against prompt-injection: untrusted text from a webpage/tool result contains
MEDIA:/etc/shadow, naive delivery uploads the host's secrets. Recency-based trust preserves that defense — injected paths name pre-existing files (mtimes in days/months) while agent-produced artifacts are seconds old. Denylist catches the corner case where someone manages to refresh a sensitive file's mtime.Infographic