Files
hermes-agent/tests/gateway/test_remote_media_fetch.py
kshitijk4poor b499ab11fe fix(gateway): remote media fetch fails closed on unresolvable symlinks, skips strict mode, keys the sandbox by session id
readlink returning nothing no longer falls back to the unresolved name (a sandbox symlink to a
credential would otherwise be followed by [ -f ]). Strict delivery mode keeps its recency gate:
no fetch, since the copy lands in an allowlisted root. The active sandbox is looked up by the
HERMES_SESSION_ID the turn registered it under (per-session-isolated docker keys by task id, not
session key). MEDIA path normalisation (quotes/backticks/trailing punctuation) now matches the
host path; the denylist tables are built once at import.
2026-09-05 16:09:46 +05:30

98 lines
4.2 KiB
Python

"""MEDIA tags pointing into a remote terminal sandbox are fetched for delivery (#466).
The host cannot see files an agent writes inside an ssh/modal/daytona sandbox, so
``filter_media_delivery_paths`` used to drop them silently. With a remote backend active the path is
pulled through the environment's ``fetch_file`` into the document cache — after the SAME denylist
the host applies, so a sandbox path (or a symlink) at ``~/.ssh/...`` never crosses.
"""
from pathlib import Path
import pytest
import gateway.media_fetch as media_fetch
from gateway.platforms.base import BasePlatformAdapter
from tools.environments.base import BaseEnvironment, FileFetchError
class _FakeRemoteEnv:
"""Stands in for a live SSHEnvironment: a tiny remote filesystem + symlink table."""
_remote_home = "/home/agent"
def __init__(self):
self.files = {"/home/agent/out/report.txt": b"hello from sandbox",
"/home/agent/.ssh/id_rsa": b"SECRET"}
self.links = {"/home/agent/out/innocent.txt": "/home/agent/.ssh/id_rsa"}
self.fetched: list = []
def fetch_realpath(self, remote_path):
return self.links.get(remote_path, remote_path)
def fetch_file(self, remote_path, local_dest, *, max_bytes):
self.fetched.append(remote_path)
if remote_path not in self.files:
raise FileFetchError("missing")
Path(local_dest).write_bytes(self.files[remote_path])
@pytest.fixture
def remote_env(monkeypatch, tmp_path):
env = _FakeRemoteEnv()
monkeypatch.setattr(media_fetch, "_active_remote_env", lambda: env)
monkeypatch.setattr("gateway.platforms.base.DOCUMENT_CACHE_DIR", tmp_path / "cache" / "documents")
monkeypatch.setattr("gateway.platforms.base.MEDIA_DELIVERY_SAFE_ROOTS", (tmp_path / "cache" / "documents",))
return env
def test_sandbox_artifact_is_fetched_but_credentials_and_symlinks_to_them_are_not(remote_env):
media = [("/home/agent/out/report.txt", False), ("/home/agent/out/innocent.txt", False),
("/home/agent/.ssh/id_rsa", False), ("~/out/report.txt", False)]
delivered = BasePlatformAdapter.filter_media_delivery_paths(media)
assert [Path(p).read_bytes() for p, _ in delivered] == [b"hello from sandbox", b"hello from sandbox"]
assert all(Path(p).name.endswith("_report.txt") for p, _ in delivered)
# The credential file and the symlink that resolves to it were refused BEFORE any bytes moved.
assert remote_env.fetched == ["/home/agent/out/report.txt", "/home/agent/out/report.txt"]
def test_local_backend_and_strict_mode_do_not_fetch(monkeypatch, tmp_path, remote_env):
"""Strict mode keeps its recency gate: a fetched copy would land in an allowlisted root and skip it."""
monkeypatch.setenv("HERMES_MEDIA_DELIVERY_STRICT", "1")
assert BasePlatformAdapter.filter_media_delivery_paths([("/home/agent/out/report.txt", False)]) == []
monkeypatch.delenv("HERMES_MEDIA_DELIVERY_STRICT")
monkeypatch.setattr(media_fetch, "_active_remote_env", lambda: None)
assert BasePlatformAdapter.filter_media_delivery_paths([(str(tmp_path / "nope.txt"), False)]) == []
assert remote_env.fetched == []
class _ScriptedEnv(BaseEnvironment):
"""Runs the fetch command through a real shell so the transport (marker fencing, in-sandbox
size bound, base64 round-trip) is exercised end to end."""
def __init__(self):
pass
def cleanup(self):
pass
def execute(self, command, cwd="", **kwargs):
import subprocess
proc = subprocess.run(["bash", "-c", command], capture_output=True, text=True)
return {"output": "echo login-noise\n" + proc.stdout + proc.stderr, "returncode": proc.returncode}
def test_fetch_file_round_trips_bytes_and_enforces_the_in_sandbox_size_cap(tmp_path):
src = tmp_path / "artifact.bin"
src.write_bytes(bytes(range(256)) * 40)
dest = tmp_path / "copy.bin"
env = _ScriptedEnv()
env.fetch_file(str(src), dest, max_bytes=len(src.read_bytes()))
assert dest.read_bytes() == src.read_bytes()
with pytest.raises(FileFetchError, match="exceeds"):
env.fetch_file(str(src), dest, max_bytes=100)
with pytest.raises(FileFetchError, match="could not read"):
env.fetch_file(str(tmp_path), dest, max_bytes=100) # a directory is not a regular file