Merge pull request #81414 from helix4u/fix/ssh-remote-tilde-cwd

fix(terminal): preserve SSH remote home cwd
This commit is contained in:
brooklyn! 2026-08-08 00:32:45 -05:00 committed by GitHub
commit b9aa9289a8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 44 additions and 24 deletions

View File

@ -63,7 +63,7 @@ from agent.interrupt_compat import request_hard_interrupt
from agent.turn_context import (
compression_made_progress,
)
from hermes_cli.config import cfg_get
from hermes_cli.config import _is_ssh_remote_tilde_cwd, cfg_get
from hermes_cli.fallback_config import get_fallback_chain
# --- Agent cache tuning ---------------------------------------------------
@ -2129,7 +2129,6 @@ if _config_path.exists():
# 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):
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)):

View File

@ -3229,6 +3229,18 @@ def terminal_config_env_var_for_key(key: str) -> Optional[str]:
return TERMINAL_CONFIG_ENV_MAP.get(key[len(prefix):])
def _is_ssh_remote_tilde_cwd(backend: str, cwd: str) -> bool:
"""Return whether the remote SSH shell must expand *cwd* itself.
Expanding ``~`` on the Hermes host rewrites it to the host or container
home before SSH sees it. Preserve ``~`` and ``~/...`` so they follow the
user selected by the SSH connection.
"""
if (backend or "").strip().lower() != "ssh":
return False
return cwd == "~" or cwd.startswith("~/")
def apply_terminal_config_to_env(
*,
env: Optional[Dict[str, str]] = None,
@ -3265,6 +3277,15 @@ def apply_terminal_config_to_env(
# override existing env values; keys inherited from DEFAULT_CONFIG are
# backfill-only.
explicit_keys = terminal_cfg.keys() if config is not None else raw_terminal_cfg.keys()
backend_is_explicit = config is not None or "backend" in raw_terminal_cfg
if backend_is_explicit:
terminal_backend = str(
terminal_cfg.get("backend") or target.get("TERMINAL_ENV") or ""
)
else:
terminal_backend = str(
target.get("TERMINAL_ENV") or terminal_cfg.get("backend") or ""
)
for cfg_key, env_var in TERMINAL_CONFIG_ENV_MAP.items():
if cfg_key not in terminal_cfg:
@ -3274,7 +3295,9 @@ def apply_terminal_config_to_env(
raw_cwd = str(value or "").strip()
if raw_cwd in {".", "auto", "cwd"}:
continue
if isinstance(value, str):
if isinstance(value, str) and not _is_ssh_remote_tilde_cwd(
terminal_backend, raw_cwd
):
value = os.path.expanduser(value)
if (should_override and cfg_key in explicit_keys) or env_var not in target:
target[env_var] = _terminal_env_value(value)

View File

@ -13,7 +13,7 @@ import os
import json
from gateway.cwd_placeholder import CWD_PLACEHOLDERS, resolve_placeholder_terminal_cwd
from tools.terminal_tool import _is_ssh_remote_tilde_cwd
from hermes_cli.config import _is_ssh_remote_tilde_cwd
def _simulate_config_bridge(cfg: dict, initial_env: dict | None = None):

View File

@ -864,14 +864,13 @@ def test_terminal_task_cwd_ssh_falls_back_to_config(monkeypatch):
assert server._terminal_task_cwd({"cwd": "/some/host/dir"}) == remote
def test_terminal_task_cwd_ssh_sentinel_cwd_falls_back_to_session(monkeypatch):
"""Sentinel/auto cwd values are not real remote paths, so the SSH branch
must defer to the session cwd rather than registering a meaningless dir."""
def test_terminal_task_cwd_ssh_sentinel_cwd_uses_remote_home(monkeypatch):
"""An SSH placeholder must not register the TUI host's session cwd."""
monkeypatch.setenv("TERMINAL_ENV", "ssh")
monkeypatch.setenv("TERMINAL_CWD", "auto")
monkeypatch.setattr(server, "_load_cfg", lambda: {"terminal": {"cwd": "."}})
assert server._terminal_task_cwd({"cwd": "/host/session/dir"}) == "/host/session/dir"
assert server._terminal_task_cwd({"cwd": "/host/session/dir"}) == "~"
class _ChunkyStdout:

View File

@ -85,6 +85,18 @@ def test_explicit_config_key_overrides_matching_env_value(monkeypatch):
assert config["docker_image"] == "config/image:1"
def test_ssh_config_preserves_remote_tilde_cwd(monkeypatch):
"""SSH ``~`` belongs to the remote user, not the Hermes host/container."""
_write_config("terminal:\n backend: ssh\n cwd: '~'\n")
monkeypatch.setenv("HOME", "/opt/data/home")
monkeypatch.setenv("USERPROFILE", r"C:\opt\data\home")
config = terminal_tool._get_env_config()
assert os.environ["TERMINAL_CWD"] == "~"
assert config["cwd"] == "~"
def test_env_is_preserved_when_config_has_no_terminal_section(monkeypatch):
_write_config("agent:\n max_turns: 100\n")
monkeypatch.setenv("TERMINAL_ENV", "ssh")

View File

@ -1368,22 +1368,6 @@ _HOST_CWD_PREFIXES = ("/Users/", "/home/", "C:\\", "C:/")
_CONTAINER_BACKENDS = frozenset({"docker", "singularity", "modal", "daytona", "vercel_sandbox"})
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.
@ -1513,6 +1497,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)
from hermes_cli.config import _is_ssh_remote_tilde_cwd
if cwd and not _is_ssh_remote_tilde_cwd(env_type, cwd):
cwd = os.path.expanduser(cwd)
host_cwd = None

View File

@ -2439,6 +2439,8 @@ def _terminal_task_cwd(session: dict | None) -> str:
raw = ""
if raw and raw not in {".", "auto", "cwd"}:
return raw
if backend == "ssh":
return "~"
return _session_cwd(session)