diff --git a/cli.py b/cli.py index 937a5332e..7d47f5073 100755 --- a/cli.py +++ b/cli.py @@ -173,10 +173,19 @@ def load_cli_config() -> Dict[str, Any]: if "backend" in terminal_config: terminal_config["env_type"] = terminal_config["backend"] - # Handle special cwd values: "." or "auto" means use current working directory + # Handle special cwd values: "." or "auto" means use current working directory. + # Only resolve to the host's CWD for the local backend where the host + # filesystem is directly accessible. For ALL remote/container backends + # (ssh, docker, modal, singularity), the host path doesn't exist on the + # target -- remove the key so terminal_tool.py uses its per-backend default. if terminal_config.get("cwd") in (".", "auto", "cwd"): - terminal_config["cwd"] = os.getcwd() - defaults["terminal"]["cwd"] = terminal_config["cwd"] + effective_backend = terminal_config.get("env_type", "local") + if effective_backend == "local": + terminal_config["cwd"] = os.getcwd() + defaults["terminal"]["cwd"] = terminal_config["cwd"] + else: + # Remove so TERMINAL_CWD stays unset → tool picks backend default + terminal_config.pop("cwd", None) env_mappings = { "env_type": "TERMINAL_ENV", diff --git a/tools/file_operations.py b/tools/file_operations.py index 7d399dc9b..4f7510622 100644 --- a/tools/file_operations.py +++ b/tools/file_operations.py @@ -257,9 +257,12 @@ class ShellFileOperations(FileOperations): cwd: Working directory (defaults to env's cwd or current directory) """ self.env = terminal_env - # Determine cwd from various possible sources + # Determine cwd from various possible sources. + # IMPORTANT: do NOT fall back to os.getcwd() -- that's the HOST's local + # path which doesn't exist inside container/cloud backends (modal, docker). + # If nothing provides a cwd, use "/" as a safe universal default. self.cwd = cwd or getattr(terminal_env, 'cwd', None) or \ - getattr(getattr(terminal_env, 'config', None), 'cwd', None) or os.getcwd() + getattr(getattr(terminal_env, 'config', None), 'cwd', None) or "/" # Cache for command availability checks self._command_cache: Dict[str, bool] = {} diff --git a/tools/terminal_tool.py b/tools/terminal_tool.py index c48508a8a..776a34a9c 100644 --- a/tools/terminal_tool.py +++ b/tools/terminal_tool.py @@ -1197,22 +1197,42 @@ def _get_env_config() -> Dict[str, Any]: env_type = os.getenv("TERMINAL_ENV", "local") # Default cwd depends on backend: - # - local/ssh: current working directory (CLI resolves "." before we get here) - # - docker/singularity: /tmp inside the container (singularity bind-mounts /scratch there) - # - modal: /root (ephemeral cloud container, full filesystem access) + # - local: host's current working directory + # - ssh: remote user's home (agent code is local, execution is remote) + # - docker: / inside the container + # - singularity/modal: /root (ephemeral cloud/container) if env_type in ("modal", "singularity"): default_cwd = "/root" elif env_type == "docker": default_cwd = "/" + elif env_type == "ssh": + default_cwd = "~" else: default_cwd = os.getcwd() + # Read TERMINAL_CWD but sanity-check it for non-local backends. + # If the CWD looks like a host-local path that can't exist inside a + # container/sandbox, fall back to the backend's own default. This + # catches the case where cli.py (or .env) leaked the host's CWD. + cwd = os.getenv("TERMINAL_CWD", default_cwd) + if env_type in ("modal", "docker", "singularity", "ssh") and cwd: + # Paths containing common host-only prefixes are clearly wrong + # inside a container. Also catch Windows-style paths (C:\...). + host_prefixes = ("/Users/", "/home/", "C:\\", "C:/") + if any(cwd.startswith(p) for p in host_prefixes) and cwd != default_cwd: + if not os.getenv("HERMES_QUIET"): + print( + f"[Terminal] Ignoring TERMINAL_CWD={cwd!r} for {env_type} backend " + f"(host path won't exist in sandbox). Using {default_cwd!r} instead." + ) + cwd = default_cwd + return { "env_type": env_type, "docker_image": os.getenv("TERMINAL_DOCKER_IMAGE", default_image), "singularity_image": os.getenv("TERMINAL_SINGULARITY_IMAGE", f"docker://{default_image}"), "modal_image": os.getenv("TERMINAL_MODAL_IMAGE", default_image), - "cwd": os.getenv("TERMINAL_CWD", default_cwd), + "cwd": cwd, "timeout": int(os.getenv("TERMINAL_TIMEOUT", "60")), "lifetime_seconds": int(os.getenv("TERMINAL_LIFETIME_SECONDS", "300")), # SSH-specific config