Fix host CWD leaking into non-local terminal backends
When using Modal, Docker, SSH, or Singularity as the terminal backend from the CLI, the agent resolved cwd: "." to the host machine's local path (e.g. /Users/rewbs/code/hermes-agent) and passed it to the remote sandbox, where it doesn't exist. All commands failed with "No such file or directory". Root cause: cli.py unconditionally resolved "." to os.getcwd() and wrote it to TERMINAL_CWD regardless of backend type. Every tool then used that host-local path as the working directory inside the remote environment. Fixes: - cli.py: only resolve "." to os.getcwd() for the local backend. For all remote backends (ssh, docker, modal, singularity), leave TERMINAL_CWD unset so the tool layer uses per-backend defaults (/root, /, ~, etc.) - terminal_tool.py: added sanity check -- if TERMINAL_CWD contains a host-local prefix (/Users/, /home/, C:\) for a non-local backend, log a warning and fall back to the backend's default - terminal_tool.py: SSH default CWD is now ~ instead of os.getcwd() - file_operations.py: last-resort CWD fallback changed from os.getcwd() to "/" so host paths never leak into remote file operations
This commit is contained in:
15
cli.py
15
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",
|
||||
|
||||
@@ -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] = {}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user