mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-09 13:21:42 +00:00
fix(ssh): preserve remote tilde cwd
This commit is contained in:
parent
af0ce1cf8e
commit
83fb8ec277
4 changed files with 93 additions and 12 deletions
|
|
@ -1436,6 +1436,9 @@ if _config_path.exists():
|
|||
# config.yaml overrides .env for these since it's the documented config path.
|
||||
_terminal_cfg = _cfg.get("terminal", {})
|
||||
if _terminal_cfg and isinstance(_terminal_cfg, dict):
|
||||
_terminal_backend = str(
|
||||
_terminal_cfg.get("backend") or os.environ.get("TERMINAL_ENV") or ""
|
||||
).strip().lower()
|
||||
_terminal_env_map = {
|
||||
"backend": "TERMINAL_ENV",
|
||||
"cwd": "TERMINAL_CWD",
|
||||
|
|
@ -1474,10 +1477,18 @@ if _config_path.exists():
|
|||
# Only bridge explicit absolute paths from config.yaml.
|
||||
if _cfg_key == "cwd" and str(_val) in {".", "auto", "cwd"}:
|
||||
continue
|
||||
# Expand shell tilde in cwd so subprocess.Popen never
|
||||
# receives a literal "~/" which the kernel rejects.
|
||||
# Expand shell tilde in local/container cwd so subprocess.Popen
|
||||
# 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).
|
||||
if _cfg_key == "cwd" and isinstance(_val, str):
|
||||
_val = os.path.expanduser(_val)
|
||||
_raw_cwd = _val.strip()
|
||||
if not (
|
||||
_terminal_backend == "ssh"
|
||||
and (_raw_cwd == "~" or _raw_cwd.startswith("~/"))
|
||||
):
|
||||
_val = os.path.expanduser(_val)
|
||||
if isinstance(_val, (list, dict)):
|
||||
os.environ[_env_var] = json.dumps(_val)
|
||||
else:
|
||||
|
|
@ -1656,8 +1667,11 @@ os.environ["HERMES_EXEC_ASK"] = "1"
|
|||
# fallback (deprecated — the warning above tells users to migrate).
|
||||
_configured_cwd = os.environ.get("TERMINAL_CWD", "")
|
||||
if not _configured_cwd or _configured_cwd in {".", "auto", "cwd"}:
|
||||
_fallback = os.getenv("MESSAGING_CWD") or str(Path.home())
|
||||
os.environ["TERMINAL_CWD"] = _fallback
|
||||
if os.environ.get("TERMINAL_ENV", "").strip().lower() == "ssh":
|
||||
os.environ.pop("TERMINAL_CWD", None)
|
||||
else:
|
||||
_fallback = os.getenv("MESSAGING_CWD") or str(Path.home())
|
||||
os.environ["TERMINAL_CWD"] = _fallback
|
||||
|
||||
from gateway.config import (
|
||||
ChannelOverride,
|
||||
|
|
|
|||
|
|
@ -28,6 +28,9 @@ def _simulate_config_bridge(cfg: dict, initial_env: dict | None = None):
|
|||
# --- Replicate lines 59-87: terminal config bridge ---
|
||||
terminal_cfg = cfg.get("terminal", {})
|
||||
if terminal_cfg and isinstance(terminal_cfg, dict):
|
||||
terminal_backend = str(
|
||||
terminal_cfg.get("backend") or env.get("TERMINAL_ENV") or ""
|
||||
).strip().lower()
|
||||
terminal_env_map = {
|
||||
"backend": "TERMINAL_ENV",
|
||||
"cwd": "TERMINAL_CWD",
|
||||
|
|
@ -45,10 +48,16 @@ def _simulate_config_bridge(cfg: dict, initial_env: dict | None = None):
|
|||
# TERMINAL_CWD. Mirrors the fix in gateway/run.py.
|
||||
if cfg_key == "cwd" and str(val) in {".", "auto", "cwd"}:
|
||||
continue
|
||||
# Expand shell tilde so subprocess.Popen never receives a literal
|
||||
# "~/" which the kernel rejects.
|
||||
# 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.
|
||||
if cfg_key == "cwd" and isinstance(val, str):
|
||||
val = os.path.expanduser(val)
|
||||
raw_cwd = val.strip()
|
||||
if not (
|
||||
terminal_backend == "ssh"
|
||||
and (raw_cwd == "~" or raw_cwd.startswith("~/"))
|
||||
):
|
||||
val = os.path.expanduser(val)
|
||||
if isinstance(val, list):
|
||||
env[env_var] = json.dumps(val)
|
||||
else:
|
||||
|
|
@ -64,14 +73,25 @@ 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_val = os.path.expanduser(alias_val)
|
||||
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)
|
||||
env[alias_env] = alias_val.strip()
|
||||
|
||||
# --- Replicate lines 144-147: MESSAGING_CWD fallback ---
|
||||
configured_cwd = env.get("TERMINAL_CWD", "")
|
||||
if not configured_cwd or configured_cwd in {".", "auto", "cwd"}:
|
||||
messaging_cwd = env.get("MESSAGING_CWD") or "/root" # Path.home() for root
|
||||
env["TERMINAL_CWD"] = messaging_cwd
|
||||
if env.get("TERMINAL_ENV", "").strip().lower() == "ssh":
|
||||
env.pop("TERMINAL_CWD", None)
|
||||
else:
|
||||
messaging_cwd = env.get("MESSAGING_CWD") or "/root" # Path.home() for root
|
||||
env["TERMINAL_CWD"] = messaging_cwd
|
||||
|
||||
return env
|
||||
|
||||
|
|
@ -249,3 +269,26 @@ class TestTildeExpansion:
|
|||
}
|
||||
result = _simulate_config_bridge(cfg)
|
||||
assert result["TERMINAL_CWD"] == os.path.expanduser("~/nested")
|
||||
|
||||
def test_ssh_terminal_cwd_tilde_preserved_for_remote_shell(self, monkeypatch):
|
||||
"""SSH cwd '~' must mean the remote user's home, not the gateway host HOME."""
|
||||
monkeypatch.setenv("HOME", "/opt/data")
|
||||
cfg = {"terminal": {"backend": "ssh", "cwd": "~"}}
|
||||
result = _simulate_config_bridge(cfg)
|
||||
assert result["TERMINAL_ENV"] == "ssh"
|
||||
assert result["TERMINAL_CWD"] == "~"
|
||||
|
||||
def test_ssh_terminal_cwd_tilde_child_preserved_for_remote_shell(self, monkeypatch):
|
||||
"""SSH cwd '~/x' must survive until the SSH shell expands remote HOME."""
|
||||
monkeypatch.setenv("HOME", "/opt/data")
|
||||
cfg = {"terminal": {"backend": "ssh", "cwd": "~/work"}}
|
||||
result = _simulate_config_bridge(cfg)
|
||||
assert result["TERMINAL_CWD"] == "~/work"
|
||||
|
||||
def test_ssh_terminal_placeholder_cwd_does_not_fallback_to_host_home(self, monkeypatch):
|
||||
"""SSH placeholder cwd should let terminal_tool use its remote-home default."""
|
||||
monkeypatch.setenv("HOME", "/opt/data")
|
||||
cfg = {"terminal": {"backend": "ssh", "cwd": "auto"}}
|
||||
result = _simulate_config_bridge(cfg, {"MESSAGING_CWD": "/host/project"})
|
||||
assert result["TERMINAL_ENV"] == "ssh"
|
||||
assert "TERMINAL_CWD" not in result
|
||||
|
|
|
|||
|
|
@ -232,6 +232,30 @@ def test_get_env_config_ignores_bad_docker_json_for_ssh_backend(monkeypatch):
|
|||
assert config["docker_env"] == {}
|
||||
|
||||
|
||||
def test_get_env_config_preserves_ssh_tilde_cwd(monkeypatch):
|
||||
"""SSH cwd '~' is expanded by the remote shell, not the Hermes host."""
|
||||
monkeypatch.setenv("TERMINAL_ENV", "ssh")
|
||||
monkeypatch.setenv("TERMINAL_CWD", "~")
|
||||
monkeypatch.setenv("HOME", "/opt/data")
|
||||
|
||||
config = terminal_tool._get_env_config()
|
||||
|
||||
assert config["env_type"] == "ssh"
|
||||
assert config["cwd"] == "~"
|
||||
|
||||
|
||||
def test_get_env_config_preserves_ssh_tilde_child_cwd(monkeypatch):
|
||||
"""SSH cwd '~/x' must not become the local/container HOME path."""
|
||||
monkeypatch.setenv("TERMINAL_ENV", "ssh")
|
||||
monkeypatch.setenv("TERMINAL_CWD", "~/project")
|
||||
monkeypatch.setenv("HOME", "/opt/data")
|
||||
|
||||
config = terminal_tool._get_env_config()
|
||||
|
||||
assert config["env_type"] == "ssh"
|
||||
assert config["cwd"] == "~/project"
|
||||
|
||||
|
||||
def test_get_env_config_still_rejects_bad_docker_json_for_docker_backend(monkeypatch):
|
||||
"""Selecting Docker should keep the existing actionable config error."""
|
||||
monkeypatch.setenv("TERMINAL_ENV", "docker")
|
||||
|
|
|
|||
|
|
@ -1286,7 +1286,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:
|
||||
if cwd and not (env_type == "ssh" and (cwd == "~" or cwd.startswith("~/"))):
|
||||
cwd = os.path.expanduser(cwd)
|
||||
host_cwd = None
|
||||
if env_type == "docker" and mount_docker_cwd:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue