Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,10 @@ def list_dir(self, path: str, max_depth: int = 2) -> list[str]:
result = self._client.shell.exec_command(command=f"find {shlex.quote(path)} -maxdepth {max_depth} -type f -o -type d 2>/dev/null | head -500", no_change_timeout=self._DEFAULT_NO_CHANGE_TIMEOUT)
output = result.data.output if result.data else ""
if output:
return [line.strip() for line in output.strip().split("\n") if line.strip()]
# splitlines() already removed the terminators; do NOT strip
# entries — a filename that legitimately ends in whitespace
# would be corrupted and never resolve again.
return [line for line in output.splitlines() if line]
return []
except Exception as e:
logger.error(f"Failed to list directory in sandbox: {e}")
Expand Down
6 changes: 4 additions & 2 deletions backend/packages/harness/deerflow/community/boxlite/box.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,9 @@ def download_file(self, path: str) -> bytes:
def list_dir(self, path: str, max_depth: int = 2) -> list[str]:
resolved = self._resolve_path(path)
r = self._sh(f"find {shlex.quote(resolved)} -maxdepth {int(max_depth)} \\( -type f -o -type d \\) 2>/dev/null | head -500")
return [line.strip() for line in (r.stdout or "").splitlines() if line.strip()]
# splitlines() already removed the terminators; do NOT strip entries —
# a filename that legitimately ends in whitespace would be corrupted.
return [line for line in (r.stdout or "").splitlines() if line]

def glob(
self,
Expand All @@ -305,7 +307,7 @@ def glob(
root = resolved.rstrip("/") or "/"
root_prefix = root if root == "/" else f"{root}/"
for entry in (r.stdout or "").splitlines():
entry = entry.strip()
# Do NOT strip: trailing whitespace can be part of the filename.
if not entry or (entry != root and not entry.startswith(root_prefix)):
continue
if should_ignore_path(entry):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,10 @@ def list_dir(self, path: str, max_depth: int = 2) -> list[str]:
try:
result = client.commands.run(f"find {shlex.quote(resolved)} -maxdepth {int(max_depth)} \\( -type f -o -type d \\) 2>/dev/null | head -500")
output = getattr(result, "stdout", "") or ""
return [line.strip() for line in output.splitlines() if line.strip()]
# splitlines() already removed the terminators; do NOT strip
# entries — a filename that legitimately ends in whitespace
# would be corrupted and never resolve again.
return [line for line in output.splitlines() if line]
except Exception as e:
logger.error("Failed to list_dir %s in e2b sandbox: %s", resolved, e)
return []
Expand Down Expand Up @@ -397,7 +400,7 @@ def glob(
root = resolved.rstrip("/") or "/"
root_prefix = root if root == "/" else f"{root}/"
for entry in output.splitlines():
entry = entry.strip()
# Do NOT strip: trailing whitespace can be part of the filename.
if not entry:
continue
if entry != root and not entry.startswith(root_prefix):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,9 @@ def list_dir(self, path: str, max_depth: int = 2) -> list[str]:
raise ValueError("max_depth must be non-negative")
resolved = self._resolve_path(path)
execution = self._run(f"find {shlex.quote(resolved)} -maxdepth {depth} \\( -type f -o -type d \\) 2>/dev/null | head -500")
return [line.strip() for line in execution_stdout(execution).splitlines() if line.strip()]
# splitlines() already removed the terminators; do NOT strip entries —
# a filename that legitimately ends in whitespace would be corrupted.
return [line for line in execution_stdout(execution).splitlines() if line]

def glob(self, path: str, pattern: str, *, include_dirs: bool = False, max_results: int = 200) -> tuple[list[str], bool]:
if max_results <= 0:
Expand All @@ -334,7 +336,7 @@ def glob(self, path: str, pattern: str, *, include_dirs: bool = False, max_resul
root = resolved.rstrip("/") or "/"
root_prefix = root if root == "/" else f"{root}/"
for entry in execution_stdout(execution).splitlines():
entry = entry.strip()
# Do NOT strip: trailing whitespace can be part of the filename.
if not entry or (entry != root and not entry.startswith(root_prefix)) or should_ignore_path(entry):
continue
relative = entry[len(root) :].lstrip("/")
Expand Down
6 changes: 4 additions & 2 deletions backend/packages/harness/deerflow/community/tenki/sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,9 @@ def download_file(self, path: str) -> bytes:
def list_dir(self, path: str, max_depth: int = 2) -> list[str]:
resolved = self._resolve_path(path)
r = self._sh(f"find {shlex.quote(resolved)} -maxdepth {int(max_depth)} \\( -type f -o -type d \\) 2>/dev/null | head -500")
return [self._virtual_path(line.strip()) for line in (r.stdout_text or "").splitlines() if line.strip()]
# splitlines() already removed the terminators; do NOT strip entries —
# a filename that legitimately ends in whitespace would be corrupted.
return [self._virtual_path(line) for line in (r.stdout_text or "").splitlines() if line]

def glob(
self,
Expand All @@ -382,7 +384,7 @@ def glob(
root = resolved.rstrip("/") or "/"
root_prefix = root if root == "/" else f"{root}/"
for entry in (r.stdout_text or "").splitlines():
entry = entry.strip()
# Do NOT strip: trailing whitespace can be part of the filename.
if not entry or (entry != root and not entry.startswith(root_prefix)):
continue
if should_ignore_path(entry):
Expand Down
8 changes: 8 additions & 0 deletions backend/tests/test_aio_sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -578,3 +578,11 @@ def test_close_when_no_close_attr_does_not_raise(self, sandbox):
sandbox._client = SimpleNamespace() # no close, no _client_wrapper
sandbox.close() # must not raise
assert sandbox._client is None


def test_list_dir_preserves_trailing_space_in_filename(sandbox):
""" "notes.txt " (trailing space) is a legal Linux filename; find prints it
verbatim, one entry per line, so a per-line strip() corrupts the name."""
sandbox._client.shell.exec_command = MagicMock(return_value=SimpleNamespace(data=SimpleNamespace(output="/test/notes.txt \n/test/sub\n")))

assert sandbox.list_dir("/test") == ["/test/notes.txt ", "/test/sub"]
16 changes: 16 additions & 0 deletions backend/tests/test_boxlite_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -1226,3 +1226,19 @@ def _swap_during_health_check(*args, **kwargs):
)
assert not replacement.is_closed
provider.shutdown()


def test_list_dir_and_glob_preserve_trailing_space_in_filename() -> None:
# "notes.txt " (trailing space) is a legal Linux filename; find prints it
# verbatim, one entry per line, so a per-line strip() corrupts the name.
class _FindBox:
async def exec(self, *argv, env=None, timeout=None):
return types.SimpleNamespace(stdout="/mnt/user-data/workspace/notes.txt \n", stderr="", exit_code=0)

box = BoxliteBox("box-id", box=_FindBox(), run=_fake_run)

assert box.list_dir("/mnt/user-data/workspace") == ["/mnt/user-data/workspace/notes.txt "]

found, truncated = box.glob("/mnt/user-data/workspace", "notes*")
assert found == ["/mnt/user-data/workspace/notes.txt "]
assert truncated is False
22 changes: 22 additions & 0 deletions backend/tests/test_e2b_sandbox_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -4112,3 +4112,25 @@ def pause_after_reserve(
assert client.closed
assert p._sandboxes == {}
assert p._reserved_slots == 0


def test_list_dir_preserves_trailing_space_in_filename():
# "notes.txt " (trailing space) is a legal Linux filename; find prints it
# verbatim, one entry per line, so a per-line strip() corrupts the name and
# every follow-up file API call on the listed path misses the real file.
listing = SimpleNamespace(stdout="/home/user/notes.txt \n/home/user/sub\n", stderr="", exit_code=0)
client = FakeClient(commands=FakeCommandsAPI([listing]))
sb = _make_sandbox(client)

assert sb.list_dir("/home/user") == ["/home/user/notes.txt ", "/home/user/sub"]


def test_glob_preserves_trailing_space_in_filename():
listing = SimpleNamespace(stdout="/home/user/notes.txt \n", stderr="", exit_code=0)
client = FakeClient(commands=FakeCommandsAPI([listing]))
sb = _make_sandbox(client)

matches, truncated = sb.glob("/home/user", "notes*")

assert matches == ["/home/user/notes.txt "]
assert truncated is False
14 changes: 14 additions & 0 deletions backend/tests/test_opensandbox_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -711,3 +711,17 @@ def slow_create(image: str, **kwargs: Any) -> _FakeRemote:
assert len(results) == 2 and results[0] == results[1]
assert len(sdk.create_calls) == 1
provider.shutdown()


def test_list_dir_and_glob_preserve_trailing_space_in_filename() -> None:
# "notes.txt " (trailing space) is a legal Linux filename; find prints it
# verbatim, one entry per line, so a per-line strip() corrupts the name.
remote = _FakeRemote("remote")
box = _box(remote)
box.write_file("/mnt/user-data/workspace/notes.txt ", "payload")

assert "/mnt/user-data/workspace/notes.txt " in box.list_dir("/mnt/user-data/workspace")

found, truncated = box.glob("/mnt/user-data/workspace", "notes*")
assert found == ["/mnt/user-data/workspace/notes.txt "]
assert truncated is False
13 changes: 13 additions & 0 deletions backend/tests/test_tenki_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -941,3 +941,16 @@ def test_integration_real_sandbox(monkeypatch):
assert box.read_file("/mnt/user-data/workspace/it.txt") == "tenki-e2e"
finally:
provider.shutdown()


def test_search_preserves_trailing_space_in_filename() -> None:
# "notes.txt " (trailing space) is a legal Linux filename; find prints it
# verbatim, one entry per line, so a per-line strip() corrupts the name.
box = TenkiSandbox("sb", _FakeSandbox())
box.write_file("/mnt/user-data/workspace/notes.txt ", "payload\n")

assert box.list_dir("/mnt/user-data/workspace") == ["/mnt/user-data/workspace/notes.txt "]

found, truncated = box.glob("/mnt/user-data/workspace", "notes*")
assert found == ["/mnt/user-data/workspace/notes.txt "]
assert truncated is False