From 7c0766e06ad87fee014499e42f28c9393e7665e4 Mon Sep 17 00:00:00 2001 From: ambition0802 <673088860@qq.com> Date: Sun, 3 May 2026 07:54:31 +0800 Subject: [PATCH] fix(gateway): translate inbound document host paths to container paths for Docker backend When terminal.backend is docker, inbound documents uploaded via messaging platforms (Telegram, Slack, Discord, Feishu, Email, etc.) are cached at a host path under ~/.hermes/cache/documents, but the container sandbox only sees them at the auto-mounted /root/.hermes/cache/documents path. This PR adds to_agent_visible_cache_path() in tools/credential_files.py (the natural sibling to get_cache_directory_mounts()) and calls it at the document-context-injection site in gateway/run.py so the agent always receives a path it can open directly, matching the mount layout already established by get_cache_directory_mounts() (#4846). Scope: only Docker backend for now; other backends use different mount semantics and are left unchanged until verified. Fixes #18787 --- gateway/run.py | 10 ++++++++-- tools/credential_files.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/gateway/run.py b/gateway/run.py index 15ce3ab08c..4f58aeee97 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -5757,6 +5757,7 @@ class GatewayRunner: if event.media_urls and event.message_type == MessageType.DOCUMENT: import mimetypes as _mimetypes + from tools.credential_files import to_agent_visible_cache_path _TEXT_EXTENSIONS = {".txt", ".md", ".csv", ".log", ".json", ".xml", ".yaml", ".yml", ".toml", ".ini", ".cfg"} for i, path in enumerate(event.media_urls): @@ -5777,16 +5778,21 @@ class GatewayRunner: display_name = parts[2] if len(parts) >= 3 else basename display_name = re.sub(r'[^\w.\- ]', '_', display_name) + # Translate host cache path to in-container path if running under Docker backend. + # This ensures the agent receives a path it can open inside its sandbox, as the + # cache directories are auto-mounted at /root/.hermes/cache/* by get_cache_directory_mounts(). + agent_path = to_agent_visible_cache_path(path) + if mtype.startswith("text/"): context_note = ( f"[The user sent a text document: '{display_name}'. " f"Its content has been included below. " - f"The file is also saved at: {path}]" + f"The file is also saved at: {agent_path}]" ) else: context_note = ( f"[The user sent a document: '{display_name}'. " - f"The file is saved at: {path}. " + f"The file is saved at: {agent_path}. " f"Ask the user what they'd like you to do with it.]" ) message_text = f"{context_note}\n\n{message_text}" diff --git a/tools/credential_files.py b/tools/credential_files.py index 2372950cfe..9026c67916 100644 --- a/tools/credential_files.py +++ b/tools/credential_files.py @@ -374,6 +374,34 @@ def get_cache_directory_mounts( return mounts +def to_agent_visible_cache_path( + host_path: str, + container_base: str = "/root/.hermes", +) -> str: + """Translate a host cache path to its mounted path inside the sandbox. + + Returns the input unchanged if it is not under any auto-mounted cache + directory, or if the active terminal backend does not require path + translation (only Docker for now). + """ + # Only Docker backend requires translation at this time. Other backends + # (Modal, Daytona, Vercel) use different mount semantics and will be + # addressed separately if needed. Backend is identified by TERMINAL_ENV + # (same env var tools/terminal_tool.py reads in _get_environment_config). + if os.environ.get("TERMINAL_ENV", "local") != "docker": + return host_path + + path = Path(host_path) + for mount in get_cache_directory_mounts(container_base=container_base): + host_dir = Path(mount["host_path"]) + try: + rel = path.relative_to(host_dir) + return str(Path(mount["container_path"]) / rel) + except ValueError: + continue + return host_path + + def iter_cache_files( container_base: str = "/root/.hermes", ) -> List[Dict[str, str]]: