mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-28 06:51:16 +08:00
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:
|
if "backend" in terminal_config:
|
||||||
terminal_config["env_type"] = terminal_config["backend"]
|
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"):
|
if terminal_config.get("cwd") in (".", "auto", "cwd"):
|
||||||
terminal_config["cwd"] = os.getcwd()
|
effective_backend = terminal_config.get("env_type", "local")
|
||||||
defaults["terminal"]["cwd"] = terminal_config["cwd"]
|
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_mappings = {
|
||||||
"env_type": "TERMINAL_ENV",
|
"env_type": "TERMINAL_ENV",
|
||||||
|
|||||||
@@ -257,9 +257,12 @@ class ShellFileOperations(FileOperations):
|
|||||||
cwd: Working directory (defaults to env's cwd or current directory)
|
cwd: Working directory (defaults to env's cwd or current directory)
|
||||||
"""
|
"""
|
||||||
self.env = terminal_env
|
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 \
|
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
|
# Cache for command availability checks
|
||||||
self._command_cache: Dict[str, bool] = {}
|
self._command_cache: Dict[str, bool] = {}
|
||||||
|
|||||||
@@ -1197,22 +1197,42 @@ def _get_env_config() -> Dict[str, Any]:
|
|||||||
env_type = os.getenv("TERMINAL_ENV", "local")
|
env_type = os.getenv("TERMINAL_ENV", "local")
|
||||||
|
|
||||||
# Default cwd depends on backend:
|
# Default cwd depends on backend:
|
||||||
# - local/ssh: current working directory (CLI resolves "." before we get here)
|
# - local: host's current working directory
|
||||||
# - docker/singularity: /tmp inside the container (singularity bind-mounts /scratch there)
|
# - ssh: remote user's home (agent code is local, execution is remote)
|
||||||
# - modal: /root (ephemeral cloud container, full filesystem access)
|
# - docker: / inside the container
|
||||||
|
# - singularity/modal: /root (ephemeral cloud/container)
|
||||||
if env_type in ("modal", "singularity"):
|
if env_type in ("modal", "singularity"):
|
||||||
default_cwd = "/root"
|
default_cwd = "/root"
|
||||||
elif env_type == "docker":
|
elif env_type == "docker":
|
||||||
default_cwd = "/"
|
default_cwd = "/"
|
||||||
|
elif env_type == "ssh":
|
||||||
|
default_cwd = "~"
|
||||||
else:
|
else:
|
||||||
default_cwd = os.getcwd()
|
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 {
|
return {
|
||||||
"env_type": env_type,
|
"env_type": env_type,
|
||||||
"docker_image": os.getenv("TERMINAL_DOCKER_IMAGE", default_image),
|
"docker_image": os.getenv("TERMINAL_DOCKER_IMAGE", default_image),
|
||||||
"singularity_image": os.getenv("TERMINAL_SINGULARITY_IMAGE", f"docker://{default_image}"),
|
"singularity_image": os.getenv("TERMINAL_SINGULARITY_IMAGE", f"docker://{default_image}"),
|
||||||
"modal_image": os.getenv("TERMINAL_MODAL_IMAGE", 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")),
|
"timeout": int(os.getenv("TERMINAL_TIMEOUT", "60")),
|
||||||
"lifetime_seconds": int(os.getenv("TERMINAL_LIFETIME_SECONDS", "300")),
|
"lifetime_seconds": int(os.getenv("TERMINAL_LIFETIME_SECONDS", "300")),
|
||||||
# SSH-specific config
|
# SSH-specific config
|
||||||
|
|||||||
Reference in New Issue
Block a user