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
35 changes: 25 additions & 10 deletions libs/deepagents/backends/filesystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ def ls_info(self, path: str) -> list[FileInfo]:

results: list[FileInfo] = []

# Convert cwd to string for comparison
cwd_str = str(self.cwd)
# Convert cwd to string for comparison (normalize to forward slashes)
cwd_str = str(self.cwd).replace("\\", "/")
if not cwd_str.endswith("/"):
cwd_str += "/"

Expand All @@ -110,6 +110,8 @@ def ls_info(self, path: str) -> list[FileInfo]:
continue

abs_path = str(child_path)
# Normalize to POSIX style (forward slashes only) for consistency
abs_path = abs_path.replace("\\", "/")

if not self.virtual_mode:
# Non-virtual mode: use absolute paths
Expand Down Expand Up @@ -140,16 +142,18 @@ def ls_info(self, path: str) -> list[FileInfo]:
except OSError:
results.append({"path": abs_path + "/", "is_dir": True})
else:
# Virtual mode: strip cwd prefix
# Virtual mode: strip cwd prefix and normalize to POSIX
if abs_path.startswith(cwd_str):
relative_path = abs_path[len(cwd_str) :]
elif abs_path.startswith(str(self.cwd)):
elif abs_path.startswith(str(self.cwd).replace("\\", "/")):
# Handle case where cwd doesn't end with /
relative_path = abs_path[len(str(self.cwd)) :].lstrip("/")
relative_path = abs_path[len(str(self.cwd).replace("\\", "/")) :].lstrip("/").lstrip("\\")
else:
# Path is outside cwd, return as-is or skip
relative_path = abs_path

# Normalize to POSIX style (forward slashes only)
relative_path = relative_path.replace("\\", "/")
virt_path = "/" + relative_path

if is_file:
Expand Down Expand Up @@ -372,7 +376,10 @@ def _ripgrep_search(self, pattern: str, base_full: Path, include_glob: str | Non
p = Path(ftext)
if self.virtual_mode:
try:
virt = "/" + str(p.resolve().relative_to(self.cwd))
rel_path = str(p.resolve().relative_to(self.cwd))
# Normalize to POSIX style (forward slashes only)
rel_path = rel_path.replace("\\", "/")
virt = "/" + rel_path
except Exception:
continue
else:
Expand Down Expand Up @@ -412,7 +419,10 @@ def _python_search(self, pattern: str, base_full: Path, include_glob: str | None
if regex.search(line):
if self.virtual_mode:
try:
virt_path = "/" + str(fp.resolve().relative_to(self.cwd))
rel_path = str(fp.resolve().relative_to(self.cwd))
# Normalize to POSIX style (forward slashes only)
rel_path = rel_path.replace("\\", "/")
virt_path = "/" + rel_path
except Exception:
continue
else:
Expand Down Expand Up @@ -440,6 +450,9 @@ def glob_info(self, pattern: str, path: str = "/") -> list[FileInfo]:
if not is_file:
continue
abs_path = str(matched_path)
# Normalize to POSIX style (forward slashes only)
abs_path = abs_path.replace("\\", "/")

if not self.virtual_mode:
try:
st = matched_path.stat()
Expand All @@ -454,15 +467,17 @@ def glob_info(self, pattern: str, path: str = "/") -> list[FileInfo]:
except OSError:
results.append({"path": abs_path, "is_dir": False})
else:
cwd_str = str(self.cwd)
cwd_str = str(self.cwd).replace("\\", "/")
if not cwd_str.endswith("/"):
cwd_str += "/"
if abs_path.startswith(cwd_str):
relative_path = abs_path[len(cwd_str) :]
elif abs_path.startswith(str(self.cwd)):
relative_path = abs_path[len(str(self.cwd)) :].lstrip("/")
elif abs_path.startswith(str(self.cwd).replace("\\", "/")):
relative_path = abs_path[len(str(self.cwd).replace("\\", "/")) :].lstrip("/").lstrip("\\")
else:
relative_path = abs_path
# Normalize to POSIX style (forward slashes only)
relative_path = relative_path.replace("\\", "/")
virt = "/" + relative_path
try:
st = matched_path.stat()
Expand Down
5 changes: 4 additions & 1 deletion libs/deepagents/middleware/filesystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,10 @@ def _validate_path(path: str, *, allowed_prefixes: Sequence[str] | None = None)
normalized = os.path.normpath(path)
normalized = normalized.replace("\\", "/")

if not normalized.startswith("/"):
# Only add leading slash if it's not a Windows absolute path (with drive letter or UNC prefix)
# Windows absolute paths like "C:/Users/..." will be kept as-is
drive, _ = os.path.splitdrive(normalized)
if not normalized.startswith("/") and not drive:
normalized = f"/{normalized}"

if allowed_prefixes is not None and not any(normalized.startswith(prefix) for prefix in allowed_prefixes):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ def test_filesystem_backend_normal_mode(tmp_path: Path):
# ls_info absolute path - should only list files in root, not subdirectories
infos = be.ls_info(str(root))
paths = {i["path"] for i in infos}
assert str(f1) in paths # File in root should be listed
assert str(f2) not in paths # File in subdirectory should NOT be listed
assert (str(root) + "/dir/") in paths # Directory should be listed
# Normalize paths for comparison (backend returns POSIX-style paths)
assert str(f1).replace("\\", "/") in paths # File in root should be listed
assert str(f2).replace("\\", "/") not in paths # File in subdirectory should NOT be listed
assert (str(root).replace("\\", "/") + "/dir/") in paths # Directory should be listed

# read, edit, write
txt = be.read(str(f1))
Expand All @@ -39,7 +40,7 @@ def test_filesystem_backend_normal_mode(tmp_path: Path):

# glob_info
g = be.glob_info("*.py", path=str(root))
assert any(i["path"] == str(f2) for i in g)
assert any(i["path"] == str(f2).replace("\\", "/") for i in g)


def test_filesystem_backend_virtual_mode(tmp_path: Path):
Expand Down Expand Up @@ -148,15 +149,16 @@ def test_filesystem_backend_ls_normal_mode_nested(tmp_path: Path):
root_listing = be.ls_info(str(root))
root_paths = [fi["path"] for fi in root_listing]

assert str(root / "file1.txt") in root_paths
assert str(root / "subdir") + "/" in root_paths
assert str(root / "subdir" / "file2.txt") not in root_paths
# Normalize paths for comparison (backend returns POSIX-style paths)
assert str(root / "file1.txt").replace("\\", "/") in root_paths
assert str(root / "subdir").replace("\\", "/") + "/" in root_paths
assert str(root / "subdir" / "file2.txt").replace("\\", "/") not in root_paths

subdir_listing = be.ls_info(str(root / "subdir"))
subdir_paths = [fi["path"] for fi in subdir_listing]
assert str(root / "subdir" / "file2.txt") in subdir_paths
assert str(root / "subdir" / "nested") + "/" in subdir_paths
assert str(root / "subdir" / "nested" / "file3.txt") not in subdir_paths
assert str(root / "subdir" / "file2.txt").replace("\\", "/") in subdir_paths
assert str(root / "subdir" / "nested").replace("\\", "/") + "/" in subdir_paths
assert str(root / "subdir" / "nested" / "file3.txt").replace("\\", "/") not in subdir_paths


def test_filesystem_backend_ls_trailing_slash(tmp_path: Path):
Expand Down