fix(agent): admit and sync gateway-staged attachments on remote execution backends (#110174)
Desktop paste/file attachments land in Hermes-managed staging dirs on the GATEWAY (composer-pastes/ for large text pastes, attachments/ for dropped files), but on the Remote SSH topology the workspace root (TERMINAL_CWD) is a path on the SSH HOST - the two filesystems are fully disjoint, as the issue thread confirms. Two gaps combined to reject every staged attachment with "path is outside the allowed workspace": - _resolve_path admitted only allowed_root + composer-paste roots, so a gateway-staged attachments/ path was refused outright. Admit the _CACHE_DIRS staging roots (attachments/, images/, cache/*, composer-pastes/) via a helper that asks get_cache_directory_mounts - the gateway's OWN payload is never a workspace escape, and the path-traversal and credential-deny guards in _ensure_reference_path_allowed still run after. Anything else outside the workspace stays blocked. - composer-pastes/ was missing from _CACHE_DIRS, so its bytes never reached the remote: ssh/daytona/vercel_sandbox sync via iter_sync_files -> iter_cache_files, and to_agent_visible_cache_path only translates mounted dirs - a paste attached on a fresh session dangled on the remote host. Tests cover the disjoint-filesystem SSH topology end-to-end (text inlines, binary renders the synced ~/.hermes path), the still-refused stranger path, local-backend unchanged, and the composer-pastes mount+sync enumeration. Consolidates PR #110387 by Finn763 (the _agent_staged_path guard widening and the SSH-topology tests, adapted to the current _ensure_reference_path_allowed ordering) with PR #103412 by ericmaddox (whose mapping insight is subsumed by the _CACHE_DIRS entry, which fixes both the sync and the translation). Co-authored-by: ericmaddox <ericmaddox@users.noreply.github.com>
This commit is contained in:
@@ -459,6 +459,27 @@ def _composer_paste_roots() -> list[Path]:
|
||||
return [hermes_dir / COMPOSER_PASTES_DIRNAME for hermes_dir in _hermes_dirs()]
|
||||
|
||||
|
||||
def _agent_staged_path(path: Path) -> bool:
|
||||
"""True when *path* sits in a Hermes dir the gateway stages for the agent.
|
||||
|
||||
Those are the ``_CACHE_DIRS`` roots — ``attachments/`` (file drops),
|
||||
``images/`` (image uploads), ``cache/*`` (platform downloads) and now
|
||||
``composer-pastes/`` (large text pastes) — the gateway's OWN payload, never
|
||||
a workspace escape, and they live outside the workspace by construction: on
|
||||
a remote execution backend (ssh and friends) the workspace root is a path
|
||||
on THAT host (#110174), so the workspace check below rejected every staged
|
||||
attachment there. The bytes are staged to (or already live on) the gateway
|
||||
either way, so the ref still expands — text inlines; binaries point at the
|
||||
backend-visible path via ``to_agent_visible_cache_path``.
|
||||
"""
|
||||
try:
|
||||
from tools.credential_files import get_cache_directory_mounts
|
||||
return any(_is_under(path, Path(entry["host_path"]).expanduser().resolve())
|
||||
for entry in get_cache_directory_mounts())
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _resolve_path(cwd: Path, target: str, *, allowed_root: Path | None = None) -> Path:
|
||||
from agent.file_safety import is_nt_namespace_path
|
||||
if is_nt_namespace_path(target): # raw-string check: resolving such a path is the NTLM-leak trigger
|
||||
@@ -468,6 +489,7 @@ def _resolve_path(cwd: Path, target: str, *, allowed_root: Path | None = None) -
|
||||
allowed_root is not None
|
||||
and not _is_under(resolved, allowed_root)
|
||||
and not any(_is_under(resolved, root) for root in _composer_paste_roots())
|
||||
and not _agent_staged_path(resolved)
|
||||
):
|
||||
raise ValueError("path is outside the allowed workspace")
|
||||
return resolved
|
||||
|
||||
Reference in New Issue
Block a user