diff --git a/gateway/run.py b/gateway/run.py index caecc8438a9..7736474e591 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -1481,13 +1481,11 @@ if _config_path.exists(): # never receives a literal "~/" which the kernel rejects. # SSH cwd is interpreted by the remote shell, so preserve # "~" / "~/..." for the SSH backend instead of expanding it - # to the Hermes host/container HOME (often /opt/data). + # to the Hermes host/container HOME (often /opt/data). Shared + # predicate with terminal_tool so the two sites can't drift. if _cfg_key == "cwd" and isinstance(_val, str): - _raw_cwd = _val.strip() - if not ( - _terminal_backend == "ssh" - and (_raw_cwd == "~" or _raw_cwd.startswith("~/")) - ): + from tools.terminal_tool import _is_ssh_remote_tilde_cwd + if not _is_ssh_remote_tilde_cwd(_terminal_backend, _val.strip()): _val = os.path.expanduser(_val) if isinstance(_val, (list, dict)): os.environ[_env_var] = json.dumps(_val) diff --git a/tests/gateway/test_config_cwd_bridge.py b/tests/gateway/test_config_cwd_bridge.py index f36b4a7ee7b..9d81da99d01 100644 --- a/tests/gateway/test_config_cwd_bridge.py +++ b/tests/gateway/test_config_cwd_bridge.py @@ -12,6 +12,8 @@ asserting the expected env var outcomes. import os import json +from tools.terminal_tool import _is_ssh_remote_tilde_cwd + def _simulate_config_bridge(cfg: dict, initial_env: dict | None = None): """Simulate the gateway config bridge logic from gateway/run.py. @@ -51,12 +53,9 @@ def _simulate_config_bridge(cfg: dict, initial_env: dict | None = None): # Expand shell tilde for local/container backends so subprocess.Popen # never receives a literal "~/" which the kernel rejects. SSH cwd is # interpreted by the remote shell, so preserve "~" / "~/..." there. + # Uses the same production predicate as gateway/run.py. if cfg_key == "cwd" and isinstance(val, str): - raw_cwd = val.strip() - if not ( - terminal_backend == "ssh" - and (raw_cwd == "~" or raw_cwd.startswith("~/")) - ): + if not _is_ssh_remote_tilde_cwd(terminal_backend, val.strip()): val = os.path.expanduser(val) if isinstance(val, list): env[env_var] = json.dumps(val) @@ -73,15 +72,7 @@ def _simulate_config_bridge(cfg: dict, initial_env: dict | None = None): alias_val = cfg.get(alias_key) if isinstance(alias_val, str) and alias_val.strip(): if alias_key == "cwd": - alias_backend = str( - cfg.get("backend") or env.get("TERMINAL_ENV") or "" - ).strip().lower() - raw_cwd = alias_val.strip() - if not ( - alias_backend == "ssh" - and (raw_cwd == "~" or raw_cwd.startswith("~/")) - ): - alias_val = os.path.expanduser(alias_val) + alias_val = os.path.expanduser(alias_val) env[alias_env] = alias_val.strip() # --- Replicate lines 144-147: MESSAGING_CWD fallback --- diff --git a/tools/terminal_tool.py b/tools/terminal_tool.py index 71bbc68658f..c5aec7e9523 100644 --- a/tools/terminal_tool.py +++ b/tools/terminal_tool.py @@ -1216,6 +1216,22 @@ _HOST_CWD_PREFIXES = ("/Users/", "/home/", "C:\\", "C:/") _CONTAINER_BACKENDS = frozenset({"docker", "singularity", "modal", "daytona"}) +def _is_ssh_remote_tilde_cwd(backend: str, cwd: str) -> bool: + """Return True when *cwd* is a tilde path that the remote SSH shell must + expand itself, so the Hermes host/container must NOT ``expanduser`` it. + + SSH ``cwd`` is interpreted by the *remote* shell (``cd ~`` / ``cd ~/x`` + over ``ssh ... bash -c``). Expanding ``~`` locally would rewrite it to the + Hermes host HOME (often ``/opt/data`` under Docker) and inject a + nonexistent path into the remote session. Only ``~`` / ``~/...`` on the + ``ssh`` backend qualify; absolute remote paths still pass through + unchanged, and every other backend keeps expanding locally. + """ + if (backend or "").strip().lower() != "ssh": + return False + return cwd == "~" or cwd.startswith("~/") + + def _is_unusable_container_cwd(cwd: str) -> bool: """Return True if *cwd* is a host/relative path that won't work as the working directory inside a container sandbox. @@ -1286,7 +1302,7 @@ def _get_env_config() -> Dict[str, Any]: # /workspace and track the original host path separately. Otherwise keep the # normal sandbox behavior and discard host paths. cwd = os.getenv("TERMINAL_CWD", default_cwd) - if cwd and not (env_type == "ssh" and (cwd == "~" or cwd.startswith("~/"))): + if cwd and not _is_ssh_remote_tilde_cwd(env_type, cwd): cwd = os.path.expanduser(cwd) host_cwd = None if env_type == "docker" and mount_docker_cwd: