Skip to content

fix(file-tools): make the device/proc read-guard fire on Windows - #69403

Open
Sora-bluesky wants to merge 1 commit into
NousResearch:mainfrom
Sora-bluesky:fix/issue-69373
Open

Sora-bluesky wants to merge 1 commit into
NousResearch:mainfrom
Sora-bluesky:fix/issue-69373

Conversation

@Sora-bluesky

Copy link
Copy Markdown
Contributor

What

_is_blocked_device_path() runs os.path.normpath on the candidate before comparing it against the POSIX strings in _BLOCKED_DEVICE_PATHS. On Windows normpath("/dev/zero") returns \dev\zero, which never matches /dev/zero, so the whole device/fd/proc read-guard is a silent no-op there, including the /proc/*/environ secret-leak checks added for #4427.

It bites on Windows specifically because read I/O shells through Git Bash, whose MSYS layer emulates /dev/* and /proc/* as real (often blocking) streams, so read_file("/dev/zero") genuinely hangs the agent.

Fixes #69373.

Fix

One guard at the top of _is_blocked_device_path: when os.sep == "\\", fold the post-normpath separators back to / so the existing comparisons fire. Every caller routes through this function (literal path, each symlink hop, and the realpath re-check in _is_blocked_device), so the single point covers all of them, #10141's realpath guard included.

Drive-qualified native paths keep their prefix (C:\dev\zero becomes C:/dev/zero) and still fall through unblocked. The one conservative over-match is a bare root-relative native path like \dev\zero, which collapses to /dev/zero and is treated as blocked, an acceptable call for such an exotic input.

Test

test_windows_normpath_device_paths_are_blocked drives the function's normpath through ntpath with os.sep forced to \\, so it reproduces the reported Windows flow on Linux CI without needing a real Windows box. It asserts forward-slash, mixed, redundant, and trailing-separator /dev and /proc inputs are blocked, and that C:/dev/zero, C:\Users\me\notes.txt, and //server/share/dev/zero stay unblocked. Verified failing without the fix and passing with it (48 passed, 2 skipped in the guard suite).

@alt-glitch alt-glitch added type/security Security vulnerability or hardening P2 Medium — degraded but workaround exists tool/file File tools (read, write, patch, search) platform/windows Native Windows-specific behavior or breakage sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows duplicate This issue or pull request already exists labels Jul 22, 2026
@alt-glitch

Copy link
Copy Markdown
Contributor

This was generated by AI during triage.

Duplicate of #69401: both repair the Windows normpath separator mismatch in the same device/proc blocklist path for #69373. This PR has stronger Windows simulation coverage, but no distinct runtime scope.

@Sora-bluesky

Copy link
Copy Markdown
Contributor Author

The duplicate note above no longer applies: #69401 was closed by its author on 2026-07-24 as stale, so there is nothing left to choose between and this is the only open fix for #69373.

Re-checked on Windows against main 199f558. _is_blocked_device_path still blocks none of /dev/zero, /dev/random, /proc/self/environ or /proc/1/maps. normpath rewrites each of them to backslashes before the POSIX comparison, so the whole guard is off, including the /proc/*/environ secret-leak family from #4427. This branch blocks all four. tests/tools/test_file_read_guards.py is 48 passed, 2 skipped, and the branch still merges clean.

@monerostar monerostar left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

monerostar native Windows live-verify

Environment

  • Windows 11 native (Build 26200) · Git Bash · Python 3.11.15 (hermes venv)
  • Control: install / origin/main tools/file_tools.py (no separator fold)
  • PR HEAD: f85d26979 — worktree %LOCALAPPDATA%\hermes\pr-worktrees\pr-69403
  • CI: Python test slices + ruff pass on this PR

Root cause confirmed on this host

os.sep == '\\'
os.path.normpath('/dev/zero') == '\\dev\\zero'   # never equals '/dev/zero'

So the POSIX string comparisons in _is_blocked_device_path are a silent no-op on Windows without a fold.

Live _is_blocked_device_path matrix

Path main (control) this PR
/dev/zero False True
/dev/tty False True
/proc/self/environ False True
/proc/1/maps False True
\\dev\\zero False True
/dev/../dev/zero False True
C:/dev/zero False False (drive-qualified kept)
C:\Users\...\notes.txt False False
//server/share/dev/zero False False

Exactly the intended shape: MSYS-style device/proc paths block; native drive/UNC stay free.

Unit tests

PYTHONPATH=<pr> pytest tests/tools/test_file_read_guards.py -q -o addopts=
→ 45 passed, 5 skipped in ~2.3s

Includes new test_windows_normpath_device_paths_are_blocked (ntpath + os.sep='\\' simulation).

Note: Running the main tree's full test_file_read_guards.py on this box hung past 60s mid-run (failures already showing). That is consistent with unguarded device/fd reads in the suite or tool path — another reason this guard should land. I did not leave a hung reader running.

Native open('/dev/zero') aside

Plain Win32 CPython here: open('/dev/zero')FileNotFoundError (no MSYS). The issue's hang class is specifically agent read I/O via Git Bash/MSYS, which emulates /dev/* and /proc/*. The fold still has to fire on the path string before any backend open — which this PR does at the single choke point used by symlink/realpath callers too.

Assessment

  • Correct, minimal (+ fold + tests), no competing open fix PR for #69373.
  • Conservative over-match of bare \\dev\\zero is called out and acceptable.
  • Prefer this over a Windows-only parallel blocklist.

Formal Approve: blocked for external collaborator — comment review only.

Related: #69373, #4427, #10141

@Sora-bluesky

Copy link
Copy Markdown
Contributor Author

Thanks for running this on real hardware. The control-vs-PR matrix is exactly the shape I hoped for, and it makes the fold's effect easy to check at a glance, including the drive-qualified and UNC non-matches staying free.

The hang you hit mid-run on main's test_file_read_guards.py fits the failure class from #69373: an unguarded read reaching an MSYS-emulated device. That's useful extra evidence for why the guard has to fire on the path string before any backend open happens.

@teknium1

Copy link
Copy Markdown
Collaborator

Thanks for the focused Windows regression fix. Current main still normalizes in tools/file_tools.py:444 and then compares the result with POSIX-form /dev and /proc patterns at tools/file_tools.py:445-474, so the reported Windows mismatch remains present.

The proposed fold is at the shared helper used by the literal, symlink-hop, and realpath guard paths (tools/file_tools.py:489, 502, 513). The added ntpath simulation exercises that branch without requiring a Windows CI runner and covers both protected paths and drive/UNC non-matches. Current main changed only the later error-envelope code after the PR base (1a7f73b8), leaving this target hunk intact, so salvage should be mechanical.

Automated hermes-sweeper review.

@teknium1 teknium1 added sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit labels Jul 30, 2026
@alt-glitch alt-glitch removed the duplicate This issue or pull request already exists label Jul 30, 2026
@egilewski

Copy link
Copy Markdown
Contributor

suggesting changes

The Windows normalization and NT-namespace checks repair the reported local-backend bypass, but the new search guard is explicitly disabled for every container-path backend. A caller can therefore pass a literal blocked path such as /dev/zero or /proc/self/environ to search_tool under Docker, Singularity, Modal, or Daytona and reach the backend search implementation. That preserves both the blocking-device and proc-data exposure class on those supported trust boundaries. The guard should always apply its pure lexical checks and restrict only host symlink/realpath dereferencing to local backends.

  • [P2] Container-backed searches bypass the new device and proc path guard (tools/file_tools.py:1933)
    The new validation is wholly nested under if not _uses_container_paths(task_id). For a container task, even an exact literal /dev/zero, /dev/stdin, or /proc/self/environ skips _is_blocked_device and is passed to file_ops.search. The shell backend can fall back to grep, so device inputs can block, while proc pseudo-files are within the same secret-bearing family this helper is intended to reject. Avoiding host-side symlink dereferencing for container paths is correct, but it does not justify skipping the pure lexical _is_blocked_device_path(path) check.
    Remediation: Apply _is_blocked_device_path(path) to every backend before dispatch. Keep host readlink/realpath checks local-only, and add backend-aware validation if container symlink aliases must also be covered. Add a regression test proving a container task rejects literal /dev/zero and /proc/self/environ without calling _get_file_ops.

Security evidence:

  • trust boundary: Untrusted model/tool arguments supply path to read_file_tool and search_tool. These functions are the validator boundary before ShellFileOperations executes shell utilities in a local or configured remote/container environment. Device and proc pseudo-files are privileged sinks because they can block indefinitely, emit infinite data, or expose process secrets and memory-layout data.
  • source/sink/invariant: Every user-controlled path that names a blocked device, standard-input alias, or sensitive /proc/<pid> pseudo-file must be rejected before _get_file_ops or file_ops.search/read_file executes. Ordinary files and Windows drive-qualified long paths must continue to dispatch. The PR enforces this for local reads/searches and NT namespace spellings, but violates it when _uses_container_paths(task_id) is true.
  • current-main reproduction: At current main 36e41c0, _is_blocked_device_path applies os.path.normpath and compares against POSIX strings without folding Windows separators. A focused ntpath simulation produced \\dev\\zero False and \\proc\\self\\fd\\0 False. Current main also has no pre-dispatch device validation in search_tool, confirming the reported Windows premise and the broader search exposure.
  • PR-head or patch-replay validation: The leased checkout is exactly PR head 06e7e47169e70c0784d04b8ca0edb86c1fb50455. A focused PR-head probe with Windows ntpath semantics returned true for /dev/zero, /proc/self/fd/0, \\\\.\\PhysicalDrive0, and \\\\?\\GLOBALROOT\\Device\\Harddisk0. A mocked local search rejected /dev/zero before backend dispatch. Repeating the same search with _uses_container_paths=True returned the mocked backend result and recorded one ops.search call, reproducing the residual bypass.
  • positive/negative cases: Positive cases validated on PR head: Windows-spelled POSIX device/proc paths and Win32 device namespaces are classified as blocked; local search of /dev/zero returns a device-file error. Negative case validated: local search of ordinary tools dispatches once and returns its normal result. Residual negative-security case: container search of literal /dev/zero dispatches instead of rejecting. The repository's 50 unittest-style file-read guard tests ran successfully with PYTHONPATH=..
  • residual bypass search: Reviewed the changed lexical normalization, symlink-hop and realpath flow, search pre-dispatch validation, container-path exception, and ShellFileOperations search sinks. Checked mixed Windows separators, drive-qualified paths, UNC/Win32 namespaces, ordinary safe paths, local versus container dispatch, and direct DOS reserved names. The actionable bypass is the unconditional container exception; raw DOS names such as CON remain an uncertainty because this Linux lease cannot establish the exact Git Bash device-opening behavior on Windows.
  • reviewer validation: Independent source tracing confirmed that search_tool reaches ShellFileOperations.search, which invokes rg or falls back to grep with the supplied path. The focused container mock demonstrates validator-to-sink reachability without relying on the PR's assertions. python -B -m compileall tools/file_tools.py succeeded. Pytest was unavailable in the leased environment (pytest absent and /usr/bin/python has no pytest), so the focused unittest module and direct deterministic probes were used instead. Network and external-review tools were not used because the work order prohibits them.

Uncertainty: No Windows runner was available, so actual Git Bash handling of raw DOS reserved names such as CON and NUL was not established.; Pytest was not installed in the leased checkout environment, preventing execution of the three focused pytest files through the requested runner grammar.; No live Docker, Singularity, Modal, or Daytona backend was invoked; the container bypass was validated at the dispatch boundary and against the backend source.; A full current-main patch replay was not required by the bound decision and was not performed; the reviewed head was coherent and the changed functions were compared directly with current main.

Signed: GPT-5.6-sol-xhigh in Codex

@alt-glitch alt-glitch removed the sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data label Jul 30, 2026
@Sora-bluesky

Copy link
Copy Markdown
Contributor Author

You were right, and the container exception was worse than one skipped check. Fixed in 234c8605d.

I reproduced it first: with a container task, search_tool("/dev/zero") and search_tool("/proc/self/environ") both reached file_ops.search with the guard never firing.

The pure path check now runs for every backend; only the symlink and realpath hops stay local, since those dereference on the host. Container tasks classify with POSIX semantics on the resolved path, plus the raw input when it is absolute, because _resolve_path_for_task collapses .. before the check would see it. The walk also folds leading slashes and unwraps /proc/<pid|self|thread-self>/root and its task/<tid>/root form, which alias the container's own root.

Measured through search_tool with no resolver mock. Refused before _get_file_ops: /dev/zero, /proc/self/environ, //dev/zero, ///dev/zero, /proc/self/root/../dev/zero, /proc/1/task/1/root/dev/zero, /proc/1/root/proc/self/root/dev/zero, /proc/thread-self/root/dev/zero. Still dispatched: //./etc/hosts, //./workspace/tools, /etc/hosts, tools, workspace/src, bare /proc/self/root, and /workspace/proc/self/root/dev/zero.

On your point about NT namespace rules: //./etc/hosts is an ordinary Linux path, so applying the Windows rules to container paths would have blocked it. My first attempt did exactly that. The local branch is now byte-identical in behaviour to what it was before this fix, and the NT rules only apply there.

Two cases are still open, and I would rather name them than imply they are covered. A workspace symlink inside the container pointing at a device is not reachable from a host-side path check. Neither is ~user: ShellFileOperations.search calls _expand_path after dispatch, so ~root/../dev/zero becomes /dev/zero inside the container with the guard already behind it. Both belong at the expansion point rather than here, which is a different change from this one.

@alt-glitch

Copy link
Copy Markdown
Contributor

This was generated by AI during triage.

Re-triage correction: #69401 is closed, while this current head remains the open focused Windows fix for #69373. Clearing the stale duplicate relationship; its Windows/NT device-path coverage is independently reviewable.

@egilewski

Copy link
Copy Markdown
Contributor

suggesting changes

The PR fixes the reported Windows normalization failure and adds a pre-dispatch device guard to search_tool, including a container-specific lexical normalizer. The Windows fix behaved correctly in direct positive and negative probes, and the current-main failure was reproduced from the exact bound main source. One in-scope container bypass remains: the new normalizer models /proc//root symlinks but not /proc//cwd. A search path routed through that symlink can resolve to /dev/zero inside the backend while the guard classifies the lexical spelling as safe and invokes the search backend. This preserves the hang/timeout class the new search guard is intended to eliminate.

  • [P2] Container search guard can be bypassed through /proc//cwd (tools/file_tools.py:519)
    The container predicate only gives special semantics to /proc//root. It treats cwd as an ordinary segment, so /proc/self/cwd/../dev/zero normalizes lexically to /proc/self/dev/zero and returns false. Under Linux name resolution, /proc/self/cwd is a symlink to the searching process's working directory; with a normal one-level container cwd such as /workspace, the same spelling resolves to /dev/zero. A production-path probe with the backend mocked showed search_tool dispatching this path to file_ops.search, and an OS probe from /home confirmed the corresponding /proc/self/cwd/../dev/zero spelling resolves to /dev/zero. An agent can also change the backend cwd to /dev and use /proc/self/cwd/zero. Searching an infinite device therefore still consumes the backend search timeout and can repeatedly stall the agent.
    Remediation: Handle /proc/<pid|self|thread-self>/cwd and task//cwd aliases before dispatch, using backend path semantics rather than host resolution. A conservative option is to reject search roots traversing these proc cwd aliases; otherwise resolve them against the backend's authoritative cwd and then re-run the device/proc policy. Add production-path tests for /proc/self/cwd/../dev/zero and for a backend cwd of /dev with /proc/self/cwd/zero, plus safe cwd-alias cases.

Security evidence:

  • trust boundary: The source is the model/user-controlled path argument to read_file or search_files/search_tool. The sink is ShellFileOperations.search, which runs rg, grep, or find in a local or container-backed shell. Device and sensitive proc paths cross from untrusted path text into potentially blocking streams or process metadata. get_read_block_error and the device predicates are the validators that must reject unsafe targets before _get_file_ops and backend command execution.
  • source/sink/invariant: Claimed invariant: every literal, normalized, aliased, or container-resolved search root that denotes a blocked device or sensitive proc pseudo-file is rejected before backend creation or I/O, while ordinary files, drive-qualified Windows files, UNC files, and ordinary container paths continue to dispatch. The PR enforces this for direct /dev and /proc spellings, Windows separator folding, NT device namespaces, and selected /proc root aliases, but not for /proc cwd symlink aliases.
  • current-main reproduction: The exact bound main source at 07447bd dispatches search_tool directly from get_read_block_error to _get_file_ops without a device predicate. Executing its exact _is_blocked_device_path AST with ntpath semantics returned false for both /dev/zero and /proc/self/environ, reproducing the reported Windows failure. Main lines 1882-1894 also confirm that device search roots reach file_ops.search.
  • PR-head or patch-replay validation: HEAD matched the bound 234c8605d3ebfdafbb3368f37520b2a9fca37066, and the three-commit diff against bound current main applied coherently in the leased checkout. Under simulated Windows ntpath/os.sep semantics, PR head returned true for /dev/zero and /proc/self/environ and false for C:/dev/zero and //server/share/dev/zero. Container probes returned true for direct /dev/zero, /proc/self/environ, and /proc/1/task/1/root/dev/zero. However, /proc/self/cwd/../dev/zero returned false, and a mocked production search_tool call dispatched it once to file_ops.search.
  • positive/negative cases: Positive blocking cases exercised: Windows /dev/zero and /proc/self/environ; container /dev/zero, /proc/self/environ, and /proc/1/task/1/root/dev/zero. Negative compatibility cases exercised: C:/dev/zero, //server/share/dev/zero, /etc/hosts, and /workspace/proc/self/root/dev/zero remained unblocked. Adversarial negative case: /proc/self/cwd/../dev/zero was incorrectly unblocked. A safe OS-only resolution probe from cwd /home resolved that alias spelling to /dev/zero without reading the device.
  • residual bypass search: Reviewed literal and mixed Windows separators, NT \.\ and \?\ namespaces, drive and UNC exclusions, relative resolution, container dot segments, /proc root and task-root aliases, backend selection, and the rg/grep/find sink. The residual bypass is the unmodeled proc cwd symlink family. Higher-numbered /proc fd aliases remain outside the existing stated policy and were not treated as a new finding.
  • reviewer validation: Independent source review and focused Python probes validated the Windows correction and independently reproduced the container cwd bypass. git diff --check completed cleanly, and python -B -m compileall tools/file_tools.py succeeded. The focused pytest invocation could not run because the leased checkout has no test virtualenv and /usr/bin/python has no pytest module; no external AI review was used as authority.

Uncertainty: The changed pytest files could not be executed because no pytest-capable Python environment is present in the leased checkout.; No live Docker backend was created for the probe; backend dispatch and Linux symlink resolution were validated separately with deterministic local probes.; The exact timeout behavior of each supported remote/container backend was not exercised, though all affected search implementations pass the path to rg, grep, or find after this guard.

Signed: GPT-5.6-sol-xhigh in Codex

@alt-glitch

Copy link
Copy Markdown
Contributor

This was generated by AI during triage.

Correction: #69401 is closed, so it is not a duplicate anchor. #69403 remains the focused open repair for the Windows separator mismatch in the device/proc read guard.

@alt-glitch alt-glitch added type/bug Something isn't working needs-decision Awaiting maintainer decision before any implementation and removed type/security Security vulnerability or hardening needs-decision Awaiting maintainer decision before any implementation labels Jul 30, 2026
@alt-glitch

Copy link
Copy Markdown
Contributor

This was generated by AI during triage.

The earlier duplicate rationale is stale: #69401 is closed. The current head is the remaining open implementation for #69373 and adds resolution-path coverage, so it is kept independently reviewable. Related: #69401, #69373.

@GottZ

GottZ commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

This was generated by AI during triage.

Summary

Two PRs address #69373's Windows separator mismatch: both restore POSIX-form comparisons after normpath, while #69403 additionally covers NT namespaces, search dispatch, and container-path handling. #69401 is the narrower closed implementation; #69403 is the remaining open implementation with runner-independent ntpath coverage and native Windows evidence.

Related pull requests

Duplicates

#69401 and #69403 duplicate the core Windows separator-normalization repair for #69373; #69403 is not a full duplicate because it adds NT-namespace, search-dispatch, container-path, and stronger runner-independent regression coverage.

Suggested consolidation

Keep #69403 open with a salvage path: preserve its verified Windows separator fold and ntpath/native-Windows coverage, while addressing the blocking /proc/<pid>/cwd review or splitting the broader search/container changes from the focused Windows repair. Keep #69401 closed as the narrower overlapping reference rather than reopening it; its core change is duplicated by #69403, but it does not carry #69403's additional scope.

Complex graph

flowchart LR
    classDef open fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
    classDef merged fill:#dcfce7,stroke:#15803d,color:#14532d
    classDef closed fill:#e5e7eb,stroke:#6b7280,color:#1f2937
    classDef unverified fill:#f3f4f6,stroke:#9ca3af,color:#374151
    classDef best stroke-width:3px,stroke:#b45309
    classDef target stroke-width:3px,stroke:#4338ca
    I69373(["issue #69373 (open)"])
    subgraph Dup69401 ["PRs duplicating each other"]
        P69401["PR #69401 (closed)"]
        P69403["PR #69403 (open)"]
    end
    P69403 -->|best fix| I69373
    class I69373 open
    class P69401 closed
    class P69403 open
    class P69401 best
    class P69403 best
    class P69403 target
    click I69373 "https://github.com/NousResearch/hermes-agent/issues/69373"
    click P69401 "https://github.com/NousResearch/hermes-agent/pull/69401"
    click P69403 "https://github.com/NousResearch/hermes-agent/pull/69403"
Loading

Graph: solid arrow = fixes / best fix, dashed arrow = partial or unverified (see edge label); boxed group = PRs duplicating each other; amber border = best fix; indigo border = target; gray node = closed (state tag in the node label).

Cross-PR triage: Reviewed 2 pull requests and 1 issue in this complex. Each diff was read against this issue; Assessment working set: 26 kB of PR diffs, 7 kB of issue/PR text, 21 kB of discussion (14 comments), 4 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch.

@egilewski

Copy link
Copy Markdown
Contributor

suggesting changes

The new guard still lets a container search reach a blocked device through a /proc/.../cwd traversal alias. With cwd /workspace, /proc/self/cwd/../dev/zero resolves to /dev/zero, but _is_blocked_container_device_path models /proc/<pid|self|thread-self>/root and task-root forms without handling cwd; search_tool therefore dispatches the path to file_ops.search. Please reject or safely canonicalize these cwd traversal aliases before backend dispatch and add refusal/no-dispatch coverage for this reproducer.

Security evidence:

  • trust boundary: Search paths supplied to search_tool cross the container path guard into the backend shell-search sink.
  • source/sink/invariant: _is_blocked_container_device_path must reject aliases of blocked device and proc pseudo-files before _get_file_ops(...).search; the confirmed cwd traversal alias remains unmodeled.
  • current-main reproduction: Current main at a991dfc25daf68994c21d6adcdfbafb1b3dc23cf dispatched /proc/self/cwd/../dev/zero, and its Windows guard matrix returned false for direct device, proc, and NT-device paths.
  • PR-head or patch-replay validation: PR head 80797ade47726060e2c6cf382533ea94178d6a1a passed the focused suite while the traversal reproducer still dispatched, and a replay onto current main reproduced the same bypass.
  • positive/negative cases: Direct device, proc, and NT-namespace paths were blocked on the PR head and replay while safe drive, UNC, and ordinary paths remained unblocked, but the cwd traversal was the negative failure.
  • residual bypass search: The residual search covered direct proc-root forms, workspace-prefixed proc paths, the cwd traversal reproducer, and safe paths; /proc/self/cwd/../dev/zero was the remaining confirmed device alias reaching the backend.
  • reviewer validation: Independent source tracing through the container guard, search_tool dispatch, and ShellFileOperations execution agrees with the focused traversal probe and realpath result.

Not checked:

  • CodeRabbit review

Signed: GPT-5.6-sol-xhigh in Codex

@alt-glitch alt-glitch added the needs-decision Awaiting maintainer decision before any implementation label Aug 3, 2026
@alt-glitch alt-glitch removed the needs-decision Awaiting maintainer decision before any implementation label Aug 3, 2026
@Sora-bluesky

Copy link
Copy Markdown
Contributor Author

Checked this against 80797ade4. The gap is real: _is_blocked_proc_path (tools/file_tools.py:534-538) gates on _BLOCKED_PROC_PATH_SUFFIXES (line 154-167), which has no /cwd entry, and _is_blocked_container_device_path (580-613) -- the string-only walker container backends use -- only collapses /proc/<pid|self|thread-self>/root (and task/<tid>/root) to /. cwd and exe aren't handled, so a /proc/<pid>/cwd alias passes that walker untouched.

This is a known residual class in this PR's guard -- the walker fold for cwd/exe, refusal tests included, is already spec'd as the planned next change on this branch.

@Sora-bluesky

Copy link
Copy Markdown
Contributor Author

@egilewski Rebased onto current main and pushed round 2 (change commit 9ef41a2b69). What changed since your last look:

  • Container-backed searches now resolve /proc/self/cwd, /proc/thread-self/cwd, and their /task/<tid>/cwd forms against the task's actual cwd before the device guard runs. Numeric-pid spellings fail closed instead of guessing.
  • /exe is now in the blocked proc suffix set, which both the container walker and the host-side device guard consult, so /proc/<pid>/exe and the task-scoped form are refused on reads and searches alike.
  • The not-found search cache moved behind that enforcement: a warm cache entry can no longer answer for a path the guard would refuse. The regression tests seed the cache first and check that the refusal still wins.

Coverage: 7 new test functions plus extended parametrized tables (43 newly collected cases) across the alias forms, dot/slash bypass spellings, and the cache ordering. CI is green on the current head; the one red slice on the first run was a Telegram polling-timing test this PR doesn't touch, and it cleared on the retrigger.

@Sora-bluesky

Copy link
Copy Markdown
Contributor Author

#101714 (48a00349) salvaged #97770 into tools/file_tools.py (search_tool order= and the search_files schema enum). merge-tree of this head 69b0328d0c onto 593aa74 marks tools/file_tools.py, tests/tools/test_file_tools.py, and tests/tools/test_file_read_guards.py changed-in-both with no conflict markers. The Windows device-guard and container cwd-alias work stays.

_is_blocked_device_path() normalizes with os.path.normpath and compares
against POSIX strings ("/dev/zero", "/proc/*/environ", ...). On Windows
normpath turns "/dev/zero" into "\dev\zero", which never matches, so the
entire device/fd/proc read-guard -- including the /proc secret-leak family
from NousResearch#4427 and the realpath re-check from NousResearch#10141 -- was a silent no-op there.
Windows read I/O shells through Git Bash, whose MSYS layer emulates /dev/*
and /proc/* as real streams, so read_file("/dev/zero") genuinely hangs the
agent (the repo's own device-rejection test wedges past 120s on Windows).

Fold separators back to "/" after normpath when os.sep is "\\", so the POSIX
comparisons fire. Every caller (literal path, each symlink hop, and the
realpath re-check) routes through _is_blocked_device_path, so this single
point fixes them all. Drive-qualified and UNC paths keep their prefix
("C:\dev\zero" -> "C:/dev/zero", "\\server\share\dev\zero" ->
"//server/share/dev/zero") and still fall through unblocked. The one
deliberate over-match is a bare root-relative native path such as
"\dev\zero" on the current drive, which collapses to "/dev/zero" and is now
blocked; an exotic spelling, and blocking it is the conservative side.

Adds a regression test that drives the function's normpath through
ntpath.normpath with os.sep forced to "\\", so the Windows branch runs on
Linux CI too, not only on a real Windows box.

Fixes NousResearch#69373.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P2 Medium — degraded but workaround exists platform/windows Native Windows-specific behavior or breakage sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows tool/file File tools (read, write, patch, search) type/bug Something isn't working

Projects

None yet

6 participants