Merge pull request #81414 from helix4u/fix/ssh-remote-tilde-cwd
fix(terminal): preserve SSH remote home cwd
This commit is contained in:
commit
b9aa9289a8
|
|
@ -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)):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue