{"repo":"NousResearch/hermes-agent","public_id":"38106","display_label":"Gateway staging fix for local media files","summary":"These items address a bug where the gateway silently drops safe local media files from certain working directories (e.g. /root) instead of staging them, along with corresponding fixes to ensure proper staging and delivery.","subsystem":null,"counts":{"members":3,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":39063,"merged_at":"2026-06-04T14:50:23+00:00"},"resolution":{"state":"resolved","by":"pr-39063"},"freshness":{"state":"verified","last_verified_at":"2026-06-04T14:50:23+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":38106,"pr_number":39063,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-06-04T14:50:23+00:00"}],"members":[{"kind":"issue","number":38106,"title":"[Bug]: Gateway silently drops safe local MEDIA files from working dirs (e.g. /root) instead of staging them","url":null,"state":"closed"},{"kind":"pr","number":38108,"title":"fix(gateway): stage safe local MEDIA files before delivery","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#39063","number":39063}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":39063,"title":"fix(gateway): deliver $HOME deliverables on root-run gateways","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["gateway/platforms/base.py","tests/gateway/test_platform_base.py"],"fix_hunks":[{"file":"gateway/platforms/base.py","hunks":["@@ -967,14 +967,35 @@ def _media_delivery_denied_paths() -> List[Path]:\n \n \n def _path_under_denied_prefix(resolved: Path) -> bool:\n-    \"\"\"Return True if ``resolved`` lives under a deny-listed system path.\"\"\"\n+    \"\"\"Return True if ``resolved`` lives under a deny-listed system path.\n+\n+    One narrow exception: when a denied prefix IS the running user's own home,\n+    the home itself is not treated as denied. ``/root`` is on the system-path\n+    denylist so that a non-root gateway can't deliver another user's home, but\n+    on a root-run gateway ``$HOME=/root`` and the operator's own deliverables\n+    (``/root/work/proposal.docx``) live directly under it. The credential\n+    sub-directories inside home (``~/.ssh``, ``~/.aws``, ...) and Hermes\n+    secrets (``~/.hermes/.env``, ``auth.json``) are *separate, more-specific*\n+    denied paths, so they stay blocked regardless of this exception — it can\n+    only un-block a plain file sitting in the running user's home tree, never a\n+    credential location or another user's home.\n+    \"\"\"\n+    try:\n+        home = Path(os.path.expanduser(\"~\")).resolve(strict=False)\n+    except (OSError, RuntimeError, ValueError):\n+        home = None\n     for denied in _media_delivery_denied_paths():\n         try:\n             resolved_denied = denied.expanduser().resolve(strict=False)\n         except (OSError, RuntimeError, ValueError):\n             continue\n-        if _path_is_within(resolved, resolved_denied) or resolved == resolved_denied:\n-            return True\n+        if not (_path_is_within(resolved, resolved_denied) or resolved == resolved_denied):\n+            continue\n+        # Allow the running user's own home tree; its credential sub-dirs are\n+        # caught by their own (more-specific) denylist entries above.\n+        if home is not None and resolved_denied == home:\n+            continue\n+        return True\n     return False\n \n "]},{"file":"tests/gateway/test_platform_base.py","hunks":["@@ -954,6 +954,120 @@ def test_filter_passes_default_files_through(self, tmp_path, monkeypatch):\n         out = BasePlatformAdapter.filter_local_delivery_paths([str(notes)])\n         assert out == [str(notes.resolve())]\n \n+    def test_root_home_deliverable_is_accepted(self, tmp_path, monkeypatch):\n+        \"\"\"The motivating bug (#38106): a root-run gateway has ``$HOME=/root``,\n+        which is on the system-prefix denylist. A plain deliverable the agent\n+        produced in its working dir (``/root/work/proposal.docx``) must still\n+        deliver — the home itself is not a credential location.\n+        \"\"\"\n+        self._patch_roots(monkeypatch)\n+\n+        fake_home = tmp_path / \"root\"\n+        workdir = fake_home / \"work\"\n+        workdir.mkdir(parents=True)\n+        doc = workdir / \"proposal.docx\"\n+        doc.write_bytes(b\"PK\\x03\\x04\")\n+        monkeypatch.setenv(\"HOME\", str(fake_home))\n+        # $HOME is itself on the denied-prefix list, mirroring /root.\n+        monkeypatch.setattr(\n+            \"gateway.platforms.base._MEDIA_DELIVERY_DENIED_PREFIXES\",\n+            (str(fake_home),),\n+        )\n+\n+        assert (\n+            BasePlatformAdapter.validate_media_delivery_path(str(doc))\n+            == str(doc.resolve())\n+        )\n+\n+    def test_root_home_credential_subdir_still_blocked(self, tmp_path, monkeypatch):\n+        \"\"\"The $HOME exception must NOT un-block credential sub-dirs inside\n+        home. ``/root/.ssh/id_rsa`` stays denied because ``~/.ssh`` is a\n+        separate, more-specific denylist entry — even when $HOME is itself a\n+        denied prefix.\n+        \"\"\"\n+        self._patch_roots(monkeypatch)\n+\n+        fake_home = tmp_path / \"root\"\n+        ssh_dir = fake_home / \".ssh\"\n+        ssh_dir.mkdir(parents=True)\n+        key = ssh_dir / \"id_rsa\"\n+        key.write_bytes(b\"[redacted:private-key]"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}