diff --git a/tests/tools/test_terminal_task_cwd.py b/tests/tools/test_terminal_task_cwd.py index 1947836fb..b49e8e1e6 100644 --- a/tests/tools/test_terminal_task_cwd.py +++ b/tests/tools/test_terminal_task_cwd.py @@ -218,3 +218,27 @@ def test_registering_non_cwd_override_leaves_live_env_cwd_untouched(monkeypatch) terminal_tool.register_task_env_overrides(task_id, {"modal_image": "custom:latest"}) assert fake_env.cwd == "/workspace/keep" + + +def test_safe_getcwd_returns_real_cwd(monkeypatch): + monkeypatch.setattr(terminal_tool.os, "getcwd", lambda: "/home/user/project") + assert terminal_tool._safe_getcwd() == "/home/user/project" + + +def test_safe_getcwd_falls_back_to_terminal_cwd_when_cwd_deleted(monkeypatch): + def _boom(): + raise FileNotFoundError("[Errno 2] No such file or directory") + + monkeypatch.setattr(terminal_tool.os, "getcwd", _boom) + monkeypatch.setenv("TERMINAL_CWD", "/srv/work") + assert terminal_tool._safe_getcwd() == "/srv/work" + + +def test_safe_getcwd_falls_back_to_home_when_no_terminal_cwd(monkeypatch): + def _boom(): + raise FileNotFoundError() + + monkeypatch.setattr(terminal_tool.os, "getcwd", _boom) + monkeypatch.delenv("TERMINAL_CWD", raising=False) + monkeypatch.setattr(terminal_tool.os.path, "expanduser", lambda p: "/home/me") + assert terminal_tool._safe_getcwd() == "/home/me" diff --git a/tools/terminal_tool.py b/tools/terminal_tool.py index 14577b9bd..3e81eff9f 100644 --- a/tools/terminal_tool.py +++ b/tools/terminal_tool.py @@ -1030,6 +1030,20 @@ def _parse_env_var(name: str, default: str, converter=int, type_label: str = "in ) +def _safe_getcwd() -> str: + """Return the current working directory, tolerating a deleted CWD. + + ``os.getcwd()`` raises FileNotFoundError when the process's working + directory has been removed out from under it (e.g. a scratch workspace + that was cleaned up mid-session). Fall back to TERMINAL_CWD, then the + user's home directory, so terminal setup never crashes on a stale CWD. + """ + try: + return os.getcwd() + except FileNotFoundError: + return os.getenv("TERMINAL_CWD") or os.path.expanduser("~") + + def _get_env_config() -> Dict[str, Any]: """Get terminal environment configuration from environment variables.""" # Default image with Python and Node.js for maximum compatibility @@ -1042,7 +1056,7 @@ def _get_env_config() -> Dict[str, Any]: # remote home, and everything else starts in the backend's default # root-like cwd. if env_type == "local": - default_cwd = os.getcwd() + default_cwd = _safe_getcwd() elif env_type == "ssh": default_cwd = "~" else: @@ -1058,7 +1072,7 @@ def _get_env_config() -> Dict[str, Any]: host_cwd = None host_prefixes = ("/Users/", "/home/", "C:\\", "C:/") if env_type == "docker" and mount_docker_cwd: - docker_cwd_source = os.getenv("TERMINAL_CWD") or os.getcwd() + docker_cwd_source = os.getenv("TERMINAL_CWD") or _safe_getcwd() candidate = os.path.abspath(os.path.expanduser(docker_cwd_source)) if ( any(candidate.startswith(p) for p in host_prefixes) @@ -2516,7 +2530,7 @@ if __name__ == "__main__": print(f" TERMINAL_SINGULARITY_IMAGE: {os.getenv('TERMINAL_SINGULARITY_IMAGE', f'docker://{default_img}')}") print(f" TERMINAL_MODAL_IMAGE: {os.getenv('TERMINAL_MODAL_IMAGE', default_img)}") print(f" TERMINAL_DAYTONA_IMAGE: {os.getenv('TERMINAL_DAYTONA_IMAGE', default_img)}") - print(f" TERMINAL_CWD: {os.getenv('TERMINAL_CWD', os.getcwd())}") + print(f" TERMINAL_CWD: {os.getenv('TERMINAL_CWD', _safe_getcwd())}") from hermes_constants import display_hermes_home as _dhh print(f" TERMINAL_SANDBOX_DIR: {os.getenv('TERMINAL_SANDBOX_DIR', f'{_dhh()}/sandboxes')}") print(f" TERMINAL_TIMEOUT: {os.getenv('TERMINAL_TIMEOUT', '60')}")