mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-08-05 20:38:41 +00:00
fix(sandbox): scope e2b grep() glob filter to its directory prefix (#4168)
E2BSandbox.grep()'s glob handling reduced a directory-scoped pattern like "src/*.js" down to just "*.js" before passing it to `grep --include=`, dropping the directory-scoping prefix entirely. GNU grep's `--include` matches by basename only, at any depth, so the search silently broadened to every matching-extension file in the sandbox tree instead of just the directory the caller asked for. Keep the basename portion as a coarse `--include=` pre-filter (a superset of the true match set) and post-filter grep's raw hits through path_matches(), the same helper glob() already uses to enforce directory scoping correctly, so grep and glob agree on what a directory-scoped pattern means.
This commit is contained in:
parent
bb0088127b
commit
495e90832c
@ -428,6 +428,13 @@ class E2BSandbox(Sandbox):
|
|||||||
else:
|
else:
|
||||||
flags.append("-E")
|
flags.append("-E")
|
||||||
if glob is not None:
|
if glob is not None:
|
||||||
|
# ``grep --include`` only matches by basename, at any depth -- it
|
||||||
|
# cannot express a directory-scoping prefix like ``src/`` in
|
||||||
|
# ``src/*.js``. Pass just the basename portion as a coarse
|
||||||
|
# pre-filter (a superset of the true match set: every file
|
||||||
|
# ``path_matches`` can accept also satisfies this basename
|
||||||
|
# pattern) and enforce the real directory scope below via
|
||||||
|
# ``path_matches``, the same helper ``glob()`` uses.
|
||||||
include_pattern = glob.split("/")[-1] or glob
|
include_pattern = glob.split("/")[-1] or glob
|
||||||
flags.append(f"--include={include_pattern}")
|
flags.append(f"--include={include_pattern}")
|
||||||
|
|
||||||
@ -448,6 +455,9 @@ class E2BSandbox(Sandbox):
|
|||||||
logger.error("Failed to grep in e2b sandbox: %s", e)
|
logger.error("Failed to grep in e2b sandbox: %s", e)
|
||||||
return [], False
|
return [], False
|
||||||
|
|
||||||
|
root = resolved.rstrip("/") or "/"
|
||||||
|
root_prefix = root if root == "/" else f"{root}/"
|
||||||
|
|
||||||
matches: list[GrepMatch] = []
|
matches: list[GrepMatch] = []
|
||||||
truncated = False
|
truncated = False
|
||||||
for raw in output.splitlines():
|
for raw in output.splitlines():
|
||||||
@ -461,6 +471,14 @@ class E2BSandbox(Sandbox):
|
|||||||
continue
|
continue
|
||||||
if should_ignore_path(file_path):
|
if should_ignore_path(file_path):
|
||||||
continue
|
continue
|
||||||
|
if glob is not None:
|
||||||
|
# Restrict to the caller's real directory scope -- the
|
||||||
|
# ``--include`` flag above only pre-filtered by basename.
|
||||||
|
if file_path != root and not file_path.startswith(root_prefix):
|
||||||
|
continue
|
||||||
|
rel_path = file_path[len(root) :].lstrip("/")
|
||||||
|
if not rel_path or not path_matches(glob, rel_path):
|
||||||
|
continue
|
||||||
matches.append(
|
matches.append(
|
||||||
GrepMatch(
|
GrepMatch(
|
||||||
path=file_path,
|
path=file_path,
|
||||||
|
|||||||
@ -1230,3 +1230,78 @@ def test_sync_outputs_to_host_skips_oversize_files(monkeypatch, tmp_path):
|
|||||||
assert files.read_calls == [], "oversize files must be skipped without invoking download_file"
|
assert files.read_calls == [], "oversize files must be skipped without invoking download_file"
|
||||||
host_target = Paths(base_dir=tmp_path).thread_dir("t1", user_id="u1") / "user-data" / "outputs" / "huge.bin"
|
host_target = Paths(base_dir=tmp_path).thread_dir("t1", user_id="u1") / "user-data" / "outputs" / "huge.bin"
|
||||||
assert not host_target.exists(), "no oversize artefact must be written to host"
|
assert not host_target.exists(), "no oversize artefact must be written to host"
|
||||||
|
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
# grep() directory-scoped glob filtering
|
||||||
|
#
|
||||||
|
# Real GNU grep's ``--include=PATTERN`` matches by basename only, at any
|
||||||
|
# depth -- it cannot express the directory-scoping portion of a pattern like
|
||||||
|
# ``src/*.js``. These tests can't invoke a real grep binary (the command
|
||||||
|
# runs remotely inside the e2b VM via the mocked ``client.commands.run``), so
|
||||||
|
# they instead supply the raw stdout a real broadened ``--include=*.js``
|
||||||
|
# grep would actually return (matches from every directory, not just the
|
||||||
|
# intended one) and assert ``E2BSandbox.grep`` narrows it down to the
|
||||||
|
# caller's real directory scope via ``path_matches`` -- the same helper
|
||||||
|
# ``glob()`` already uses -- exactly as verified empirically against a real
|
||||||
|
# GNU grep binary during development of this fix.
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
def test_grep_scoped_glob_excludes_unrelated_directory_matches():
|
||||||
|
"""Regression: grep(glob="src/*.js") must not leak matches from sibling
|
||||||
|
directories that merely share the file extension."""
|
||||||
|
raw_stdout = "/home/user/workspace/other_dir/unrelated.js:1:console.log('needle in other_dir');\n/home/user/workspace/src/app.js:1:console.log('needle in src');\n"
|
||||||
|
client = FakeClient(commands=FakeCommandsAPI([SimpleNamespace(stdout=raw_stdout, stderr="", exit_code=0)]))
|
||||||
|
sb = _make_sandbox(client)
|
||||||
|
|
||||||
|
matches, truncated = sb.grep("/mnt/user-data/workspace", "needle", glob="src/*.js")
|
||||||
|
|
||||||
|
paths = [m.path for m in matches]
|
||||||
|
assert paths == ["/home/user/workspace/src/app.js"]
|
||||||
|
assert "/home/user/workspace/other_dir/unrelated.js" not in paths
|
||||||
|
assert truncated is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_grep_plain_glob_matches_files_in_any_directory():
|
||||||
|
"""No regression: a plain non-scoped glob (no ``/`` in the pattern) must
|
||||||
|
keep matching files at any depth, same as before the directory-scoping
|
||||||
|
fix."""
|
||||||
|
raw_stdout = "/home/user/workspace/other_dir/deep/mod.py:1:needle in a deeply nested file\n/home/user/workspace/src/app.py:1:needle in a python file too\n"
|
||||||
|
client = FakeClient(commands=FakeCommandsAPI([SimpleNamespace(stdout=raw_stdout, stderr="", exit_code=0)]))
|
||||||
|
sb = _make_sandbox(client)
|
||||||
|
|
||||||
|
matches, truncated = sb.grep("/mnt/user-data/workspace", "needle", glob="*.py")
|
||||||
|
|
||||||
|
paths = {m.path for m in matches}
|
||||||
|
assert paths == {
|
||||||
|
"/home/user/workspace/other_dir/deep/mod.py",
|
||||||
|
"/home/user/workspace/src/app.py",
|
||||||
|
}
|
||||||
|
assert truncated is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_grep_scoped_glob_still_passes_coarse_include_flag():
|
||||||
|
"""The coarse ``--include=<basename>`` pre-filter is kept as a perf
|
||||||
|
optimization (it narrows what grep has to search) even though it can't
|
||||||
|
express directory scoping by itself -- the real scoping enforcement
|
||||||
|
happens in the post-filter, not by dropping ``--include``."""
|
||||||
|
client = FakeClient(commands=FakeCommandsAPI([SimpleNamespace(stdout="", stderr="", exit_code=0)]))
|
||||||
|
sb = _make_sandbox(client)
|
||||||
|
|
||||||
|
sb.grep("/mnt/user-data/workspace", "needle", glob="src/*.js")
|
||||||
|
|
||||||
|
assert any("--include=*.js" in cmd for cmd in client.commands.calls)
|
||||||
|
|
||||||
|
|
||||||
|
def test_grep_without_glob_is_unaffected():
|
||||||
|
"""No regression: omitting ``glob`` entirely must return every match
|
||||||
|
with no path-based post-filtering."""
|
||||||
|
raw_stdout = "/home/user/workspace/anywhere/file.txt:3:needle here\n"
|
||||||
|
client = FakeClient(commands=FakeCommandsAPI([SimpleNamespace(stdout=raw_stdout, stderr="", exit_code=0)]))
|
||||||
|
sb = _make_sandbox(client)
|
||||||
|
|
||||||
|
matches, truncated = sb.grep("/mnt/user-data/workspace", "needle")
|
||||||
|
|
||||||
|
assert [m.path for m in matches] == ["/home/user/workspace/anywhere/file.txt"]
|
||||||
|
assert truncated is False
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user