mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-30 11:52:04 +00:00
fix(file-tools): sanitize host/relative cwd override before it reaches container sandbox (#54447) (#54616)
(cherry picked from commit 82132f7911)
Co-authored-by: Tranquil-Flow <66773372+Tranquil-Flow@users.noreply.github.com>
This commit is contained in:
parent
e1f4098b9f
commit
8fe800ee1a
2 changed files with 133 additions and 0 deletions
|
|
@ -144,3 +144,114 @@ class TestOverrideCwdSanitizedAtCallSite:
|
|||
# RL/benchmark envs set an in-container path; it must pass through.
|
||||
cwd = self._run_and_capture_cwd(monkeypatch, "/workspace/task42")
|
||||
assert cwd == "/workspace/task42"
|
||||
|
||||
|
||||
class TestFileOpsCwdSanitizedAtCallSite:
|
||||
"""E2E pin: file tools (_get_file_ops) must sanitize a host/relative cwd
|
||||
override before it reaches _create_environment on a container backend —
|
||||
the same guard the terminal tool got in #50636. Without it, a Desktop/TUI
|
||||
host cwd (e.g. ``/Users/me/workspace``) leaks straight into
|
||||
``docker run -w`` and ``search_files`` returns an empty workspace (#54447).
|
||||
"""
|
||||
|
||||
def _run_and_capture_cwd(self, monkeypatch, override_cwd, env_type="docker",
|
||||
config_cwd="/workspace"):
|
||||
"""Drive ``_get_file_ops()`` on a container backend with a host-path cwd
|
||||
override registered, and return the cwd that reached
|
||||
``_create_environment`` (i.e. the cwd passed to ``docker run -w``).
|
||||
"""
|
||||
import tools.terminal_tool as tt
|
||||
import tools.file_tools as ft
|
||||
|
||||
captured = {}
|
||||
|
||||
config = {
|
||||
"env_type": env_type,
|
||||
"docker_image": "pytorch/pytorch:latest",
|
||||
"singularity_image": "docker://pytorch/pytorch:latest",
|
||||
"modal_image": "pytorch/pytorch:latest",
|
||||
"daytona_image": "pytorch/pytorch:latest",
|
||||
"cwd": config_cwd,
|
||||
"host_cwd": None,
|
||||
"timeout": 180,
|
||||
"lifetime_seconds": 300,
|
||||
"container_cpu": 1,
|
||||
"container_memory": 5120,
|
||||
"container_disk": 51200,
|
||||
"container_persistent": True,
|
||||
"docker_volumes": [],
|
||||
"docker_env": {},
|
||||
"docker_extra_args": [],
|
||||
"docker_mount_cwd_to_workspace": False,
|
||||
"docker_run_as_host_user": False,
|
||||
"docker_forward_env": [],
|
||||
"modal_mode": "auto",
|
||||
"ssh_host": "",
|
||||
"ssh_user": "",
|
||||
"ssh_port": 22,
|
||||
"ssh_key": "",
|
||||
"ssh_persistent": False,
|
||||
"local_persistent": False,
|
||||
}
|
||||
|
||||
class _DummyEnv:
|
||||
cwd = config_cwd
|
||||
|
||||
def execute(self, *a, **k):
|
||||
return {"output": "", "exit_code": 0}
|
||||
|
||||
def fake_create_environment(env_type, image, cwd, timeout, **kwargs):
|
||||
captured["cwd"] = cwd
|
||||
return _DummyEnv()
|
||||
|
||||
monkeypatch.setattr(tt, "_get_env_config", lambda: config)
|
||||
monkeypatch.setattr(tt, "_start_cleanup_thread", lambda: None)
|
||||
monkeypatch.setattr(tt, "_create_environment", fake_create_environment)
|
||||
# Force a fresh environment build.
|
||||
monkeypatch.setattr(tt, "_active_environments", {})
|
||||
monkeypatch.setattr(tt, "_last_activity", {})
|
||||
monkeypatch.setattr(ft, "_file_ops_cache", {})
|
||||
monkeypatch.setattr(ft, "_last_known_cwd", {})
|
||||
|
||||
task_id = "sess-fileops-host-cwd"
|
||||
tt.register_task_env_overrides(task_id, {"cwd": override_cwd})
|
||||
try:
|
||||
ft._get_file_ops(task_id)
|
||||
finally:
|
||||
tt.clear_task_env_overrides(task_id)
|
||||
return captured.get("cwd")
|
||||
|
||||
def test_macos_host_override_does_not_reach_container(self, monkeypatch):
|
||||
# Desktop/TUI registers /Users/<me>/workspace as the session cwd.
|
||||
cwd = self._run_and_capture_cwd(monkeypatch, "/Users/me/workspace")
|
||||
assert cwd == "/workspace", (
|
||||
f"Host-path cwd override leaked to the container builder: {cwd!r}. "
|
||||
"It must be sanitized back to config['cwd']."
|
||||
)
|
||||
|
||||
def test_posix_home_host_override_does_not_reach_container(self, monkeypatch):
|
||||
cwd = self._run_and_capture_cwd(monkeypatch, "/home/someuser/project")
|
||||
assert cwd == "/workspace"
|
||||
|
||||
def test_windows_host_override_does_not_reach_container(self, monkeypatch):
|
||||
cwd = self._run_and_capture_cwd(monkeypatch, r"C:\Users\someuser")
|
||||
assert cwd == "/workspace"
|
||||
|
||||
def test_relative_cwd_override_does_not_reach_container(self, monkeypatch):
|
||||
cwd = self._run_and_capture_cwd(monkeypatch, "src/app")
|
||||
assert cwd == "/workspace"
|
||||
|
||||
def test_valid_container_override_is_preserved(self, monkeypatch):
|
||||
# RL/benchmark envs set an in-container path; it must pass through.
|
||||
cwd = self._run_and_capture_cwd(monkeypatch, "/workspace/task42")
|
||||
assert cwd == "/workspace/task42"
|
||||
|
||||
def test_host_override_sanitized_on_singularity(self, monkeypatch):
|
||||
cwd = self._run_and_capture_cwd(
|
||||
monkeypatch, "/Users/me/workspace", env_type="singularity")
|
||||
assert cwd == "/workspace"
|
||||
|
||||
def test_host_override_sanitized_on_modal(self, monkeypatch):
|
||||
cwd = self._run_and_capture_cwd(
|
||||
monkeypatch, "/Users/me/workspace", env_type="modal")
|
||||
assert cwd == "/workspace"
|
||||
|
|
|
|||
|
|
@ -832,6 +832,8 @@ def _get_file_ops(task_id: str = "default") -> ShellFileOperations:
|
|||
_creation_locks,
|
||||
_creation_locks_lock,
|
||||
_resolve_container_task_id,
|
||||
_is_unusable_container_cwd,
|
||||
_CONTAINER_BACKENDS,
|
||||
)
|
||||
import time
|
||||
|
||||
|
|
@ -893,6 +895,26 @@ def _get_file_ops(task_id: str = "default") -> ShellFileOperations:
|
|||
image = ""
|
||||
|
||||
cwd = overrides.get("cwd") or _last_known_cwd.get(task_id) or config["cwd"]
|
||||
# Re-apply the container cwd guard that _get_env_config() already
|
||||
# ran on config["cwd"] (see #50636). A per-task cwd override
|
||||
# registered by the gateway/TUI/ACP for workspace tracking is a
|
||||
# raw host path (e.g. a Desktop session's /Users/<me>/workspace or
|
||||
# C:\\Users\\<me>). On a container backend that reaches
|
||||
# ``docker run -w <host-path>`` and the container starts in a
|
||||
# directory that doesn't exist inside the sandbox, so search_files
|
||||
# and friends silently return empty results (#54447). Sanitize it
|
||||
# back to the already-validated config["cwd"] so the override can't
|
||||
# bypass the guard. Valid in-container override paths (RL/benchmark
|
||||
# sandboxes that set cwd to /workspace, /root, etc.) are absolute
|
||||
# non-host paths and pass through untouched.
|
||||
if env_type in _CONTAINER_BACKENDS and _is_unusable_container_cwd(cwd):
|
||||
if cwd != config["cwd"]:
|
||||
logger.info(
|
||||
"Ignoring host/relative cwd override %r for %s backend "
|
||||
"(won't exist in sandbox). Using %r instead.",
|
||||
cwd, env_type, config["cwd"],
|
||||
)
|
||||
cwd = config["cwd"]
|
||||
logger.info("Creating new %s environment for task %s...", env_type, task_id[:8])
|
||||
|
||||
container_config = None
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue