mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-23 10:42:00 +00:00
fix(file): anchor device symlink guard to task cwd
The read_file device guard now walks symlink hops before the file operation layer, but that hop walk still interpreted relative paths against the Python process cwd. In sessions where TERMINAL_CWD points at the task workspace, a relative workspace symlink to a blocked alias such as /dev/../dev/stdin could therefore miss the intermediate device target before later task-cwd resolution. Anchor relative device checks to the task base before symlink-hop inspection so the pre-I/O guard sees the same workspace path that read_file would otherwise read. Absolute device paths and the existing final realpath fallback remain unchanged. Refs #10141 Refs #29158
This commit is contained in:
parent
e267237671
commit
def3f6388f
2 changed files with 34 additions and 3 deletions
|
|
@ -170,6 +170,33 @@ class TestDevicePathBlocking(unittest.TestCase):
|
|||
self.assertIn("device file", result["error"])
|
||||
mock_ops.assert_not_called()
|
||||
|
||||
@patch("tools.file_tools._get_file_ops")
|
||||
def test_read_file_tool_rejects_task_cwd_relative_device_alias_symlink(self, mock_ops):
|
||||
if not os.path.exists("/dev/stdin"):
|
||||
self.skipTest("/dev/stdin is not available on this platform")
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
workspace = os.path.join(tmpdir, "workspace")
|
||||
process_cwd = os.path.join(tmpdir, "process")
|
||||
os.mkdir(workspace)
|
||||
os.mkdir(process_cwd)
|
||||
link_path = os.path.join(workspace, "stdin-link")
|
||||
try:
|
||||
os.symlink("/dev/../dev/stdin", link_path)
|
||||
except OSError as exc:
|
||||
self.skipTest(f"symlink unavailable: {exc}")
|
||||
|
||||
old_cwd = os.getcwd()
|
||||
try:
|
||||
os.chdir(process_cwd)
|
||||
with patch.dict(os.environ, {"TERMINAL_CWD": workspace}, clear=False):
|
||||
result = json.loads(read_file_tool("stdin-link", task_id="dev_rel_link_test"))
|
||||
finally:
|
||||
os.chdir(old_cwd)
|
||||
|
||||
self.assertIn("error", result)
|
||||
self.assertIn("device file", result["error"])
|
||||
mock_ops.assert_not_called()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Character-count limits
|
||||
|
|
|
|||
|
|
@ -302,14 +302,17 @@ def _is_blocked_device_path(path: str) -> bool:
|
|||
return False
|
||||
|
||||
|
||||
def _is_blocked_device(filepath: str) -> bool:
|
||||
def _is_blocked_device(filepath: str, base_dir: str | Path | None = None) -> bool:
|
||||
"""Return True if the path would hang the process (infinite output or blocking input).
|
||||
|
||||
Check the literal path first so aliases like /dev/stdin are caught before
|
||||
they resolve to terminal-specific paths. Then check each symlink hop before
|
||||
the final resolved path so aliases to devices cannot bypass the guard.
|
||||
"""
|
||||
normalized = os.path.normpath(os.path.expanduser(filepath))
|
||||
expanded = os.path.expanduser(filepath)
|
||||
if base_dir is not None and not os.path.isabs(expanded):
|
||||
expanded = os.path.join(os.fspath(base_dir), expanded)
|
||||
normalized = os.path.normpath(expanded)
|
||||
if _is_blocked_device_path(normalized):
|
||||
return True
|
||||
|
||||
|
|
@ -850,7 +853,8 @@ def read_file_tool(path: str, offset: int = 1, limit: int = 500, task_id: str =
|
|||
# ── Device path guard ─────────────────────────────────────────
|
||||
# Block paths that would hang the process (infinite output,
|
||||
# blocking on input). Pure path check — no I/O.
|
||||
if _is_blocked_device(path):
|
||||
device_base = None if Path(path).expanduser().is_absolute() else _resolve_base_dir(task_id)
|
||||
if _is_blocked_device(path, base_dir=device_base):
|
||||
return json.dumps({
|
||||
"error": (
|
||||
f"Cannot read '{path}': this is a device file that would "
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue