# NousResearch/hermes-agent · Gateway MEDIA delivery safe roots for cache paths

## Open now

- Nothing open on GitHub.

These issues and pull requests address a problem where cached media files (e.g., under cache/images or symlinked HERMES_HOME paths) are not recognized as safe roots for MEDIA delivery in the gateway. The work involves adding canonical cache directory paths to MEDIA_DELIVERY_SAFE_ROOTS and improving diagnostics for related rejections.

## TL;DR

- 10 member(s): 0 open issue(s), 2 closed issue(s), 0 open PR(s), 2 merged PR(s), 6 PR(s) closed without merging.
- A fix was merged to main in PR #34485. Verify it addresses your case.
- **Recommended:** read PR #34485 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `partially_resolved` · deciding PR(s): `pr-54060`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.23`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #31733: Generated images under cache/images may not deliver via Telegram MEDIA, while image_cache works
- #33549: Canonical media cache directories should be safe delivery roots

## Prior & competing PRs

- **#34485: fix(gateway): diagnosable MEDIA rejections + canonical cache roots + null-path guard**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `gateway/platforms/base.py`, `tests/gateway/test_platform_base.py`
- **#54060: fix(gateway): deliver profile-scoped cache media on symlinked HERMES_HOME**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `gateway/platforms/base.py`, `tests/gateway/test_platform_base.py`
  - fix hunks:
  - `gateway/platforms/base.py`

    ```diff
    @@ -1010,9 +1010,47 @@ def cache_video_from_bytes(data: bytes, ext: str = ".mp4") -> str:
     )
     
     
    +# Canonical cache subdirectories that hold deliverable artifacts. Used both
    +# for the top-level safe roots above and to enumerate per-profile cache roots
    +# at check time (see _media_delivery_allowed_roots).
    +_MEDIA_DELIVERY_CACHE_SUBDIRS = (
    +    "images",
    +    "audio",
    +    "videos",
    +    "documents",
    +    "screenshots",
    +)
    +
    +
    +def _profile_cache_roots() -> List[Path]:
    +    """Return per-profile canonical cache roots under the shared Hermes root.
    +
    +    Profile gateways write generated artifacts to
    +    ``<root>/profiles/<name>/cache/{images,audio,...}``. The static safe-roots
    +    list only covers the *active* HERMES_HOME's cache, so a gateway running at
    +    the root (e.g. ``HERMES_HOME=/opt/data``) while the model emits a
    +    profile-scoped path silently fails delivery. Enumerated dynamically at
    +    check time so profiles created after startup are covered, and so the
    +    resolved profile path is allowlisted *before* the ``/root`` system denylist
    +    is consulted (which otherwise wins when HERMES_HOME is symlinked under a
    +    denied prefix and $HOME is not that prefix). See issue #31733.
    +    """
    +    roots: List[Path] = []
    +    profiles_dir = _HERMES_ROOT / "profiles"
    +    try:
    +        profile_dirs = [p for p in profiles_dir.iterdir() if p.is_dir()]
    +    except OSError:
    +        return roots
    +    for profile_dir in profile_dirs:
    +        for subdir in _MEDIA_DELIVERY_CACHE_SUBDIRS:
    +            roots.append(profile_dir / "cache" / subdir)
    +    return roots
    +
    +
     def _media_delivery_allowed_roots() -> List[Path]:
         """Return roots from which model-emitted local media may be delivered."""
         roots = [Path(root) for root in MEDIA_DELIVERY_SAFE_ROOTS]
    +    roots.extend(_profile_cache_roots())
         extra_roots = os.environ.get(MEDIA_DELIVERY_ALLOW_DIRS_ENV, "")
         for chunk in extra_roots.split(os.pathsep):
             for raw_root in chunk.split(","):
    ```
  - `tests/gateway/test_platform_base.py`

    ```diff
    @@ -1174,6 +1174,69 @@ def test_root_home_hermes_env_still_blocked(self, tmp_path, monkeypatch):
     
             assert BasePlatformAdapter.validate_media_delivery_path(str(env_file)) is None
     
    +    def test_profile_scoped_cache_delivers_under_symlinked_root(self, tmp_path, monkeypatch):
    +        """Reopened #31733: a profile gateway whose HERMES_HOME is symlinked
    +        under a denied prefix (e.g. /opt/data -> /root/.hermes) emits
    +        profile-scoped paths (``<root>/profiles/<name>/cache/images/x.png``)
    +        that resolve under ``/root``. ``$HOME`` is NOT that prefix, so the
    +        root-home exception doesn't fire, and the top-level cache allowlist
    +        doesn't cover the profile subdir — the file was silently dropped.
    +        Per-profile cache roots must be allowlisted so it delivers.
    +        """
    +        self._patch_roots(monkeypatch)  # strict on, zero top-level cache roots
    +
    +        # Stand-in for the literal /root deny prefix in the deployment.
    +        denied_root = tmp_path / "root"
    +        hermes_root = denied_root / ".hermes"
    +        prof_cache = hermes_root / "profiles" / "myprof" / "cache" / "images"
    +        prof_cache.mkdir(parents=True)
    +        image = prof_cache / "gen.png"
    +        image.write_bytes(b"\x89PNG\r\n\x1a\n")
    +
    +        # $HOME is NOT the denied prefix (mirrors HOME=/opt/data/home).
    +        fake_home = tmp_path / "opt" / "data" / "home"
    +        fake_home.mkdir(parents=True)
    +        monkeypatch.setenv("HOME", str(fake_home))
    +        monkeypatch.setattr(
    +            "gateway.platforms.base._MEDIA_DELIVERY_DENIED_PREFIXES",
    +            (str(denied_root),),
    +        )
    +        monkeypatch.setattr(
    +            "gateway.platforms.base._HERMES_ROOT", hermes_root
    +        )
    +
    +        assert (
    +            BasePlatformAdapter.validate_media_delivery_path(str(image))
    +            == str(image.resolve())
    +        )
    +
    +    def test_profile_scoped_credential_still_blocked_under_root(self, tmp_path, monkeypatch):
    +        """The profile-cache allowlist must not un-block a credential sitting
    +        directly in the profile dir (``profiles/<name>/auth.json``): it's not
    +        under a cache subdir, so the credential denylist still rejects it.
    +        """
    +        self._patch_roots(monkeypatch)
    +
    +        denied_root = tmp_path / "root"
    +        hermes_root = denied_root / ".hermes"
    +        prof_dir = hermes_root / "profiles" / "myprof"
    +        prof_dir.mkdir(parents=True)
    +        cred = prof_dir / "auth.json"
    +        cred.write_text("{}")
    +
    +        fake_home = tmp_path / "opt" / "data" / "home"
    +        fake_home.mkdir(parents=True)
    +        monkeypatch.setenv("HOME", str(fake_home))
    +        monkeypatch.setattr(
    +            "gateway.platforms.base._MEDIA_DELIVERY_DENIED_PREFIXES",
    +            (str(denied_root),),
    +        )
    +        monkeypatch.setattr(
    +            "gateway.platforms.base._HERMES_ROOT", hermes_root
    +        )
    +
    +        assert BasePlatformAdapter.validate_media_delivery_path(str(cred)) is None
    +
         def test_other_users_home_still_blocked_for_nonroot(self, tmp_path, monkeypatch):
             """The exception only un-blocks the *running user's own* home. A
             non-root gateway ($HOME=/home/me) must not deliver another user's home
    ```
- **#31764: fix(gateway): add canonical cache paths to MEDIA_DELIVERY_SAFE_ROOTS**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#34485`, `#31733`, `#54060`
- **#33238: fix(gateway): add canonical cache paths to MEDIA_DELIVERY_SAFE_ROOTS**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#34485`, `#31733`, `#54060`
- **#33251: fix(gateway): diagnosable MEDIA delivery + canonical cache roots (#31733)**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `e28a668b40d3888fb69a624ef5fcb4dd59c9e5ff`, `#34485`, `#31733`
- **#33550: fix(gateway): allow canonical media cache delivery roots**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#34485`, `#31733`, `#54060`
- **#33586: fix(gateway): add canonical cache paths to MEDIA_DELIVERY_SAFE_ROOTS**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#34485`, `#31733`, `#54060`
- **#33901: fix: add cache subdirectories to MEDIA delivery safe roots**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#34485`, `#31733`, `#54060`

## Fix status

- Merged to main in PR #34485 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-06-28`.
- Subsystem: `gateway`
