diff --git a/backend/packages/harness/deerflow/community/boxlite/box.py b/backend/packages/harness/deerflow/community/boxlite/box.py index 1850c8aa72..240e37fb5f 100644 --- a/backend/packages/harness/deerflow/community/boxlite/box.py +++ b/backend/packages/harness/deerflow/community/boxlite/box.py @@ -231,8 +231,10 @@ class BoxliteBox(Sandbox): if start_line is None and end_line is None: return content lines = content.splitlines() - start = start_line or 1 - end = end_line if end_line is not None else len(lines) + # Clamp like LocalSandbox.read_file: a negative start would otherwise + # wrap around through Python's negative-index slicing. + start = max(start_line or 1, 1) + end = max(end_line, 0) if end_line is not None else len(lines) return "\n".join(lines[start - 1 : end]) def write_file(self, path: str, content: str, append: bool = False) -> None: diff --git a/backend/packages/harness/deerflow/community/e2b_sandbox/e2b_sandbox.py b/backend/packages/harness/deerflow/community/e2b_sandbox/e2b_sandbox.py index 429d641666..980fb4738c 100644 --- a/backend/packages/harness/deerflow/community/e2b_sandbox/e2b_sandbox.py +++ b/backend/packages/harness/deerflow/community/e2b_sandbox/e2b_sandbox.py @@ -234,8 +234,10 @@ class E2BSandbox(Sandbox): return content.decode("utf-8", errors="replace") if isinstance(content, bytes) else content or "" text = content.decode("utf-8", errors="replace") if isinstance(content, bytes) else content or "" lines = text.splitlines() - start = start_line or 1 - end = end_line if end_line is not None else len(lines) + # Clamp like LocalSandbox.read_file: a negative start would otherwise + # wrap around through Python's negative-index slicing. + start = max(start_line or 1, 1) + end = max(end_line, 0) if end_line is not None else len(lines) content = "\n".join(lines[start - 1 : end]) return content except Exception as e: diff --git a/backend/packages/harness/deerflow/community/opensandbox/sandbox.py b/backend/packages/harness/deerflow/community/opensandbox/sandbox.py index 643a0722c6..64377f6c48 100644 --- a/backend/packages/harness/deerflow/community/opensandbox/sandbox.py +++ b/backend/packages/harness/deerflow/community/opensandbox/sandbox.py @@ -272,8 +272,10 @@ class OpenSandboxSandbox(Sandbox): if start_line is None and end_line is None: return content or "" lines = (content or "").splitlines() - start = start_line or 1 - end = end_line if end_line is not None else len(lines) + # Clamp like LocalSandbox.read_file: a negative start would otherwise + # wrap around through Python's negative-index slicing. + start = max(start_line or 1, 1) + end = max(end_line, 0) if end_line is not None else len(lines) return "\n".join(lines[start - 1 : end]) def write_file(self, path: str, content: str, append: bool = False) -> None: diff --git a/backend/tests/test_boxlite_provider.py b/backend/tests/test_boxlite_provider.py index 02803dd281..736d6d45e6 100644 --- a/backend/tests/test_boxlite_provider.py +++ b/backend/tests/test_boxlite_provider.py @@ -204,6 +204,11 @@ def test_read_file_supports_optional_line_ranges(monkeypatch: pytest.MonkeyPatch assert box.read_file("/mnt/user-data/workspace/range.txt", start_line=2, end_line=4) == "line 2\nline 3\nline 4" assert box.read_file("/mnt/user-data/workspace/range.txt", start_line=4) == "line 4\nline 5" assert box.read_file("/mnt/user-data/workspace/range.txt", end_line=2) == "line 1\nline 2" + # A start past EOF comes back empty rather than raising, and a negative + # start reads from the first line instead of wrapping around. + assert box.read_file("/mnt/user-data/workspace/range.txt", start_line=99) == "" + assert box.read_file("/mnt/user-data/workspace/range.txt", start_line=-1) == "line 1\nline 2\nline 3\nline 4\nline 5" + assert box.read_file("/mnt/user-data/workspace/range.txt", end_line=-1) == "" assert all(call == ("cat", "--", "/mnt/user-data/workspace/range.txt") for call in calls) diff --git a/backend/tests/test_e2b_sandbox_provider.py b/backend/tests/test_e2b_sandbox_provider.py index f60d9e073d..829d32d911 100644 --- a/backend/tests/test_e2b_sandbox_provider.py +++ b/backend/tests/test_e2b_sandbox_provider.py @@ -3550,6 +3550,11 @@ def test_read_file_supports_bounded_ranges(): assert sb.read_file("/mnt/user-data/workspace/range.txt", start_line=2, end_line=4) == "line 2\nline 3\nline 4" assert sb.read_file("/mnt/user-data/workspace/range.txt", start_line=4) == "line 4\nline 5" assert sb.read_file("/mnt/user-data/workspace/range.txt", end_line=2) == "line 1\nline 2" + # A start past EOF comes back empty rather than raising, and a negative + # start reads from the first line instead of wrapping around. + assert sb.read_file("/mnt/user-data/workspace/range.txt", start_line=99) == "" + assert sb.read_file("/mnt/user-data/workspace/range.txt", start_line=-1) == "line 1\nline 2\nline 3\nline 4\nline 5" + assert sb.read_file("/mnt/user-data/workspace/range.txt", end_line=-1) == "" resolved_path = "/home/user/workspace/range.txt" assert all(path == resolved_path for path, _fmt in files.read_calls), files.read_calls diff --git a/backend/tests/test_opensandbox_provider.py b/backend/tests/test_opensandbox_provider.py index a1e32c54d9..793004ec01 100644 --- a/backend/tests/test_opensandbox_provider.py +++ b/backend/tests/test_opensandbox_provider.py @@ -599,6 +599,11 @@ def test_text_binary_append_and_line_ranges() -> None: assert box.read_file(path, 2, 3) == "two\nthree" box.write_file(path, "\nfour", append=True) assert box.read_file(path) == "one\ntwo\nthree\nfour" + # A start past EOF comes back empty rather than raising, and a negative + # start reads from the first line instead of wrapping around. + assert box.read_file(path, 99) == "" + assert box.read_file(path, -1) == "one\ntwo\nthree\nfour" + assert box.read_file(path, end_line=-1) == "" binary_path = "/mnt/user-data/outputs/blob.bin" box.update_file(binary_path, b"\x00\xffpayload") assert box.download_file(binary_path) == b"\x00\xffpayload"