fix(gateway,terminal): expand shell tilde in terminal.cwd before subprocess
Commit3c42064emade config.yaml the single source of truth for TERMINAL_CWD, but the config bridge passes cwd values verbatim to os.environ. When a user sets terminal.cwd: ~/ in config.yaml, the literal string '~/'' reaches subprocess.Popen, which the kernel rejects because it does not expand shell tilde syntax. This patch adds three defensive layers: 1. gateway/run.py — expanduser at config bridge time so TERMINAL_CWD is always an absolute path. 2. tools/terminal_tool.py — expanduser when reading TERMINAL_CWD in _get_env_config(), guarding against stale or manually-set env vars. 3. tools/environments/local.py — expanduser in LocalEnvironment before passing cwd to subprocess.Popen, the final safety net. Includes regression tests in test_config_cwd_bridge.py for nested terminal.cwd, top-level cwd alias, and precedence ordering. Refs:3c42064e
This commit is contained in:
parent
88e07c42b4
commit
80e474f11f
@ -286,6 +286,10 @@ 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.
|
||||
if _cfg_key == "cwd" and isinstance(_val, str):
|
||||
_val = os.path.expanduser(_val)
|
||||
if isinstance(_val, list):
|
||||
os.environ[_env_var] = json.dumps(_val)
|
||||
else:
|
||||
|
||||
@ -41,6 +41,10 @@ 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.
|
||||
if cfg_key == "cwd" and isinstance(val, str):
|
||||
val = os.path.expanduser(val)
|
||||
if isinstance(val, list):
|
||||
env[env_var] = json.dumps(val)
|
||||
else:
|
||||
@ -55,6 +59,8 @@ def _simulate_config_bridge(cfg: dict, initial_env: dict | None = None):
|
||||
if alias_env not in env:
|
||||
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)
|
||||
env[alias_env] = alias_val.strip()
|
||||
|
||||
# --- Replicate lines 144-147: MESSAGING_CWD fallback ---
|
||||
@ -205,3 +211,32 @@ class TestNestedTerminalCwdPlaceholderSkip:
|
||||
assert result["TERMINAL_ENV"] == "docker"
|
||||
assert result["TERMINAL_TIMEOUT"] == "300"
|
||||
assert result["TERMINAL_CWD"] == "/from/env"
|
||||
|
||||
|
||||
class TestTildeExpansion:
|
||||
"""terminal.cwd values containing shell tilde must be expanded.
|
||||
|
||||
subprocess.Popen does not expand shell syntax, so a literal "~/"
|
||||
causes FileNotFoundError. Regression test for commit 3c42064e.
|
||||
"""
|
||||
|
||||
def test_terminal_cwd_tilde_expanded(self):
|
||||
"""terminal.cwd: '~/projects' should expand to /home/<user>/projects."""
|
||||
cfg = {"terminal": {"cwd": "~/projects"}}
|
||||
result = _simulate_config_bridge(cfg)
|
||||
assert result["TERMINAL_CWD"] == os.path.expanduser("~/projects")
|
||||
|
||||
def test_top_level_cwd_tilde_expanded(self):
|
||||
"""top-level cwd: '~/' should expand to user's home directory."""
|
||||
cfg = {"cwd": "~/"}
|
||||
result = _simulate_config_bridge(cfg)
|
||||
assert result["TERMINAL_CWD"] == os.path.expanduser("~/")
|
||||
|
||||
def test_tilde_with_nested_precedence(self):
|
||||
"""Nested terminal.cwd should win over top-level, both expanded."""
|
||||
cfg = {
|
||||
"cwd": "~/top",
|
||||
"terminal": {"cwd": "~/nested"},
|
||||
}
|
||||
result = _simulate_config_bridge(cfg)
|
||||
assert result["TERMINAL_CWD"] == os.path.expanduser("~/nested")
|
||||
|
||||
@ -305,6 +305,8 @@ class LocalEnvironment(BaseEnvironment):
|
||||
"""
|
||||
|
||||
def __init__(self, cwd: str = "", timeout: int = 60, env: dict = None):
|
||||
if cwd:
|
||||
cwd = os.path.expanduser(cwd)
|
||||
super().__init__(cwd=cwd or os.getcwd(), timeout=timeout, env=env)
|
||||
self.init_session()
|
||||
|
||||
|
||||
@ -925,6 +925,8 @@ 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:
|
||||
cwd = os.path.expanduser(cwd)
|
||||
host_cwd = None
|
||||
host_prefixes = ("/Users/", "/home/", "C:\\", "C:/")
|
||||
if env_type == "docker" and mount_docker_cwd:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user