fix(sdk): align glob path default with grep (#3666)

> [!IMPORTANT]
> **TL;DR:** Omitting `path` in `glob` now searches each backend's own
default root (e.g. `FilesystemBackend`'s `cwd`) instead of forcing a
literal `"/"`, matching how `grep` already behaves. Explicit `path="/"`
is unchanged.

> [!WARNING]
> **Custom backends:** `BackendProtocol.glob`/`aglob` now declare `path:
str | None = None`, and the tool layer passes `None` (not `"/"`) when
the caller omits `path`. Built-in backends are updated to treat `None`
as their default root. A custom `BackendProtocol` subclass that assumed
`path` is always a string must add a guard such as `path = path or "/"`
at the top of its `glob`/`aglob` to avoid an `AttributeError` on
unscoped calls.

---

`glob` and `grep` are paired filesystem search tools. Previously their
schemas disagreed on the default search root: `glob.path` defaulted to
`"/"` while `grep.path` defaulted to `null`. This makes omitted
`glob.path` flow through as `None`, so each backend resolves its own
default root instead of being handed a literal `"/"`.

**Behavior with an omitted `path`:**

- **`FilesystemBackend`** searches its configured `root_dir` / `cwd` —
the agent's working directory, *not* the real OS root. Both an omitted
`path` and an explicit `path="/"` resolve to `cwd`.
- **`StateBackend`**, **`StoreBackend`**, **sandbox**, **context hub**,
**Harbor**, and **composite** backends search their respective default
root.
- Error messages for an omitted `path` now display `<default>` rather
than a misleading `'/'`.

**Behavior with an explicit `path`:**

- `path="/"` remains fully supported and behaves exactly as before, so
existing callers are unaffected.
- Any other explicit `path` is resolved relative to the backend as it
always has been.

**Schema change:**

- Generated tool schemas now expose `glob.path` as nullable with default
`null`, matching `grep.path`. Unscoped `glob()` calls keep the same
runtime behavior.

This is source-compatible for normal callers. The one compatibility risk
is code that introspects the Python signature or generated tool schema
and expects `glob.path.default == "/"` or a non-nullable `path` — those
will now see `None` / a nullable field.
This commit is contained in:
Mason Daugherty
2026-06-02 17:13:52 -04:00
committed by GitHub
parent fd5c154372
commit ece8e75205
20 changed files with 158 additions and 81 deletions
@@ -402,21 +402,22 @@ class CompositeBackend(BackendProtocol):
# Path specified but doesn't match a route - search only default
return self._coerce_grep_result(await self.default.agrep(pattern, path, glob))
def glob(self, pattern: str, path: str = "/") -> GlobResult:
def glob(self, pattern: str, path: str | None = None) -> GlobResult:
"""Find files matching a glob pattern, routing by path prefix."""
results: list[FileInfo] = []
backend, backend_path, route_prefix = _route_for_path(
default=self.default,
sorted_routes=self.sorted_routes,
path=path,
)
if route_prefix is not None:
glob_result = backend.glob(pattern, backend_path)
matches = glob_result.matches if isinstance(glob_result, GlobResult) else glob_result
if isinstance(glob_result, GlobResult) and glob_result.error:
return glob_result
return GlobResult(matches=[_remap_file_info_path(fi, route_prefix) for fi in (matches or [])])
if path is not None:
backend, backend_path, route_prefix = _route_for_path(
default=self.default,
sorted_routes=self.sorted_routes,
path=path,
)
if route_prefix is not None:
glob_result = backend.glob(pattern, backend_path)
matches = glob_result.matches if isinstance(glob_result, GlobResult) else glob_result
if isinstance(glob_result, GlobResult) and glob_result.error:
return glob_result
return GlobResult(matches=[_remap_file_info_path(fi, route_prefix) for fi in (matches or [])])
# Path doesn't match any specific route - search default backend AND all routed backends
default_result = self.default.glob(pattern, path)
@@ -433,21 +434,22 @@ class CompositeBackend(BackendProtocol):
results.sort(key=lambda x: x.get("path", ""))
return GlobResult(matches=results)
async def aglob(self, pattern: str, path: str = "/") -> GlobResult:
async def aglob(self, pattern: str, path: str | None = None) -> GlobResult:
"""Async version of glob."""
results: list[FileInfo] = []
backend, backend_path, route_prefix = _route_for_path(
default=self.default,
sorted_routes=self.sorted_routes,
path=path,
)
if route_prefix is not None:
glob_result = await backend.aglob(pattern, backend_path)
matches = glob_result.matches if isinstance(glob_result, GlobResult) else glob_result
if isinstance(glob_result, GlobResult) and glob_result.error:
return glob_result
return GlobResult(matches=[_remap_file_info_path(fi, route_prefix) for fi in (matches or [])])
if path is not None:
backend, backend_path, route_prefix = _route_for_path(
default=self.default,
sorted_routes=self.sorted_routes,
path=path,
)
if route_prefix is not None:
glob_result = await backend.aglob(pattern, backend_path)
matches = glob_result.matches if isinstance(glob_result, GlobResult) else glob_result
if isinstance(glob_result, GlobResult) and glob_result.error:
return glob_result
return GlobResult(matches=[_remap_file_info_path(fi, route_prefix) for fi in (matches or [])])
# Path doesn't match any specific route - search default backend AND all routed backends
default_result = await self.default.aglob(pattern, path)
@@ -264,7 +264,7 @@ class ContextHubBackend(BackendProtocol):
return GrepResult(matches=matches)
def glob(self, pattern: str, path: str = "/") -> GlobResult: # noqa: ARG002
def glob(self, pattern: str, path: str | None = None) -> GlobResult: # noqa: ARG002
"""Return files matching `pattern` (`path` unused — flat namespace)."""
try:
cache = self._ensure_cache()
@@ -773,12 +773,12 @@ class FilesystemBackend(BackendProtocol):
return results, None
def glob(self, pattern: str, path: str = "/") -> GlobResult: # noqa: C901, PLR0912 # Complex virtual_mode logic
def glob(self, pattern: str, path: str | None = None) -> GlobResult: # noqa: C901, PLR0912, PLR0915 # Complex virtual_mode logic
"""Find files matching a glob pattern.
Args:
pattern: Glob pattern to match files against (e.g., `'*.py'`, `'**/*.txt'`).
path: Base directory to search from. Defaults to root (`/`).
path: Base directory to search from. Defaults to `root_dir` / `cwd`.
Returns:
GlobResult with matching files or error.
@@ -791,11 +791,12 @@ class FilesystemBackend(BackendProtocol):
raise ValueError(msg)
try:
search_path = self.cwd if path == "/" else self._resolve_path(path)
search_path = self.cwd if path is None or path == "/" else self._resolve_path(path)
if not search_path.exists() or not search_path.is_dir():
return GlobResult(matches=[])
except (OSError, RuntimeError) as e:
return GlobResult(error=f"Error globbing path '{path}': {e}", matches=[])
display_path = path if path is not None else "<default>"
return GlobResult(error=f"Error globbing path '{display_path}': {e}", matches=[])
results: list[FileInfo] = []
try:
@@ -851,7 +852,8 @@ class FilesystemBackend(BackendProtocol):
except (OSError, RuntimeError, ValueError) as e:
# rglob() raised mid-iteration. Return whatever was accumulated
# but flag the partial result so callers don't trust it as complete.
msg = f"Glob of '{path}' aborted partway: {e}"
display_path = path if path is not None else "<default>"
msg = f"Glob of '{display_path}' aborted partway: {e}"
logger.warning("%s", msg, exc_info=True)
results.sort(key=lambda x: x.get("path", ""))
return GlobResult(error=msg, matches=results)
@@ -465,7 +465,7 @@ class BackendProtocol(abc.ABC): # noqa: B024
"""Async version of `grep`."""
return await asyncio.to_thread(self.grep, pattern, path, glob)
def glob(self, pattern: str, path: str = "/") -> "GlobResult":
def glob(self, pattern: str, path: str | None = None) -> "GlobResult":
"""Find files matching a glob pattern.
Args:
@@ -478,9 +478,9 @@ class BackendProtocol(abc.ABC): # noqa: B024
- `?` matches a single character
- `[abc]` matches one character from set
path: Base directory to search from.
path: Optional base directory to search from.
Default: `'/'` (root).
If omitted, the backend chooses its default search root.
The pattern is applied relative to this path.
@@ -498,11 +498,11 @@ class BackendProtocol(abc.ABC): # noqa: B024
message=("`glob_info` is deprecated and will be removed in deepagents==0.7.0; rename to `glob` instead."),
package="deepagents",
)
return GlobResult(matches=self.glob_info(pattern, path))
return GlobResult(matches=self.glob_info(pattern, path or "/"))
raise NotImplementedError
async def aglob(self, pattern: str, path: str = "/") -> "GlobResult":
async def aglob(self, pattern: str, path: str | None = None) -> "GlobResult":
"""Async version of `glob`."""
return await asyncio.to_thread(self.glob, pattern, path)
@@ -829,11 +829,12 @@ except PermissionError:
return GrepResult(matches=matches)
def glob(self, pattern: str, path: str = "/") -> GlobResult:
def glob(self, pattern: str, path: str | None = None) -> GlobResult:
"""Structured glob matching returning `GlobResult`."""
search_path = path or "/"
# Encode pattern and path as base64 to avoid escaping issues
pattern_b64 = base64.b64encode(pattern.encode("utf-8")).decode("ascii")
path_b64 = base64.b64encode(path.encode("utf-8")).decode("ascii")
path_b64 = base64.b64encode(search_path.encode("utf-8")).decode("ascii")
cmd = _GLOB_COMMAND_TEMPLATE.format(path_b64=path_b64, pattern_b64=pattern_b64)
result = self.execute(cmd)
@@ -864,7 +865,7 @@ except PermissionError:
)
if error is not None:
return GlobResult(matches=None, error=f"Path '{path}': {error}")
return GlobResult(matches=None, error=f"Path '{search_path}': {error}")
return GlobResult(matches=file_infos)
@property
+1 -1
View File
@@ -300,7 +300,7 @@ class StateBackend(BackendProtocol):
files = self._read_files()
return grep_matches_from_files(files, pattern, path if path is not None else "/", glob)
def glob(self, pattern: str, path: str = "/") -> GlobResult:
def glob(self, pattern: str, path: str | None = None) -> GlobResult:
"""Get FileInfo for files matching glob pattern."""
files = self._read_files()
result = _glob_search_files(files, pattern, path)
+1 -1
View File
@@ -701,7 +701,7 @@ class StoreBackend(BackendProtocol):
continue
return grep_matches_from_files(files, pattern, path, glob)
def glob(self, pattern: str, path: str = "/") -> GlobResult:
def glob(self, pattern: str, path: str | None = None) -> GlobResult:
"""Find files matching a glob pattern in the store."""
store = self._get_store()
namespace = self._get_namespace()
+2 -2
View File
@@ -546,14 +546,14 @@ def _filter_files_by_path(files: dict[str, Any], normalized_path: str) -> dict[s
def _glob_search_files(
files: dict[str, Any],
pattern: str,
path: str = "/",
path: str | None = None,
) -> str:
r"""Search files dict for paths matching glob pattern.
Args:
files: Dictionary of file paths to FileData.
pattern: Glob pattern (e.g., "*.py", "**/*.ts").
path: Base path to search from.
path: Base path to search from. `None` defaults to root.
Returns:
Newline-separated file paths, sorted by modification time (most recent first).
@@ -308,7 +308,7 @@ class GlobSchema(BaseModel):
"""Input schema for the `glob` tool."""
pattern: str = Field(description="Glob pattern to match files (e.g., '**/*.py', '*.txt', '/subdir/**/*.md').")
path: str = Field(default="/", description="Base directory to search from. Defaults to root '/'.")
path: str | None = Field(default=None, description="Base directory to search from. Defaults to the backend's default root.")
class GrepSchema(BaseModel):
@@ -385,7 +385,7 @@ Returns a list of absolute file paths that match the pattern.
Examples:
- `**/*.py` - Find all Python files
- `*.txt` - Find all text files in root
- `*.txt` - Find all text files in the backend's default root
- `/subdir/**/*.md` - Find all markdown files under /subdir"""
GREP_TOOL_DESCRIPTION = """Search for a text pattern across files.
@@ -1197,12 +1197,12 @@ class FilesystemMiddleware(AgentMiddleware[FilesystemState, ContextT, ResponseT]
def sync_glob(
pattern: Annotated[str, "Glob pattern to match files (e.g., '**/*.py', '*.txt', '/subdir/**/*.md')."],
runtime: ToolRuntime[None, FilesystemState],
path: Annotated[str, "Base directory to search from. Defaults to root '/'."] = "/",
path: Annotated[str | None, "Base directory to search from. Defaults to the backend's default root."] = None,
) -> ToolMessage:
"""Synchronous wrapper for glob tool."""
resolved_backend = self._get_backend(runtime)
try:
validated_path = validate_path(path)
permission_path = validate_path(path if path is not None else "/")
except ValueError as e:
return ToolMessage(
content=f"Error: {e}",
@@ -1210,16 +1210,17 @@ class FilesystemMiddleware(AgentMiddleware[FilesystemState, ContextT, ResponseT]
tool_call_id=runtime.tool_call_id,
status="error",
)
if _check_fs_permission(self._permissions, "read", validated_path) == "deny":
if _check_fs_permission(self._permissions, "read", permission_path) == "deny":
return ToolMessage(
content=f"Error: permission denied for read on {validated_path}",
content=f"Error: permission denied for read on {permission_path}",
name="glob",
tool_call_id=runtime.tool_call_id,
status="error",
)
backend_path = permission_path if path is not None else None
ctx = contextvars.copy_context()
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
future = executor.submit(lambda: ctx.run(resolved_backend.glob, pattern, path=validated_path))
future = executor.submit(lambda: ctx.run(resolved_backend.glob, pattern, path=backend_path))
try:
glob_result = future.result(timeout=GLOB_TIMEOUT)
except concurrent.futures.TimeoutError:
@@ -1248,12 +1249,12 @@ class FilesystemMiddleware(AgentMiddleware[FilesystemState, ContextT, ResponseT]
async def async_glob(
pattern: Annotated[str, "Glob pattern to match files (e.g., '**/*.py', '*.txt', '/subdir/**/*.md')."],
runtime: ToolRuntime[None, FilesystemState],
path: Annotated[str, "Base directory to search from. Defaults to root '/'."] = "/",
path: Annotated[str | None, "Base directory to search from. Defaults to the backend's default root."] = None,
) -> ToolMessage:
"""Asynchronous wrapper for glob tool."""
resolved_backend = self._get_backend(runtime)
try:
validated_path = validate_path(path)
permission_path = validate_path(path if path is not None else "/")
except ValueError as e:
return ToolMessage(
content=f"Error: {e}",
@@ -1261,16 +1262,17 @@ class FilesystemMiddleware(AgentMiddleware[FilesystemState, ContextT, ResponseT]
tool_call_id=runtime.tool_call_id,
status="error",
)
if _check_fs_permission(self._permissions, "read", validated_path) == "deny":
if _check_fs_permission(self._permissions, "read", permission_path) == "deny":
return ToolMessage(
content=f"Error: permission denied for read on {validated_path}",
content=f"Error: permission denied for read on {permission_path}",
name="glob",
tool_call_id=runtime.tool_call_id,
status="error",
)
backend_path = permission_path if path is not None else None
try:
glob_result = await asyncio.wait_for(
resolved_backend.aglob(pattern, path=validated_path),
resolved_backend.aglob(pattern, path=backend_path),
timeout=GLOB_TIMEOUT,
)
except TimeoutError:
@@ -109,6 +109,8 @@ def test_composite_backend_filesystem_plus_store(tmp_path: Path):
# glob
gl = comp.glob("*.md", path="/").matches
assert any(i["path"] == "/memories/notes.md" for i in gl)
gl_default = comp.glob("*.md").matches
assert gl_default == gl
def test_composite_backend_store_to_store():
@@ -63,6 +63,25 @@ def test_filesystem_backend_normal_mode(tmp_path: Path):
assert any(i["path"] == str(f2) for i in g)
def test_filesystem_backend_glob_default_matches_backend_root(tmp_path: Path) -> None:
root = tmp_path / "root"
in_root = root / "dir" / "inside.py"
outside_root = tmp_path / "outside.py"
write_file(in_root, "print('inside')")
write_file(outside_root, "print('outside')")
be = FilesystemBackend(root_dir=str(root), virtual_mode=False)
omitted = be.glob("**/*.py").matches or []
explicit_root = be.glob("**/*.py", path="/").matches or []
omitted_paths = {info["path"] for info in omitted}
explicit_root_paths = {info["path"] for info in explicit_root}
assert omitted_paths == explicit_root_paths
assert str(in_root) in omitted_paths
assert str(outside_root) not in omitted_paths
def test_filesystem_backend_virtual_mode(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
root = tmp_path
f1 = root / "a.txt"
@@ -145,7 +145,7 @@ class TestDeprecatedMethodsRouteToNewNames:
def test_glob_info_delegates_to_glob(self) -> None:
class MyBackend(BackendProtocol):
def glob(self, pattern: str, path: str = "/") -> GlobResult:
def glob(self, pattern: str, path: str | None = None) -> GlobResult:
return GlobResult(matches=[{"path": f"{path}/{pattern}"}])
with warnings.catch_warnings(record=True) as w:
@@ -82,3 +82,15 @@ class TestFilesystemToolSchemas:
f"Sync schema: {sync_schema}\n"
f"Async schema: {async_schema}"
)
def test_glob_path_defaults_to_nullable_backend_root(self) -> None:
"""Verify `glob.path` matches `grep.path` optional default semantics."""
middleware = FilesystemMiddleware(backend=StateBackend())
glob_tool = next(tool for tool in middleware.tools if tool.name == "glob")
schema = glob_tool.tool_call_schema.model_json_schema()
path_schema = schema["properties"]["path"]
assert path_schema["default"] is None
assert {"type": "null"} in path_schema["anyOf"]
assert "backend's default root" in path_schema["description"]
@@ -146,14 +146,21 @@
},
{
"function": {
"description": "Find files matching a glob pattern.\n\nSupports standard glob patterns: `*` (any characters), `**` (any directories), `?` (single character).\nReturns a list of absolute file paths that match the pattern.\n\nExamples:\n- `**/*.py` - Find all Python files\n- `*.txt` - Find all text files in root\n- `/subdir/**/*.md` - Find all markdown files under /subdir",
"description": "Find files matching a glob pattern.\n\nSupports standard glob patterns: `*` (any characters), `**` (any directories), `?` (single character).\nReturns a list of absolute file paths that match the pattern.\n\nExamples:\n- `**/*.py` - Find all Python files\n- `*.txt` - Find all text files in the backend's default root\n- `/subdir/**/*.md` - Find all markdown files under /subdir",
"name": "glob",
"parameters": {
"properties": {
"path": {
"default": "/",
"description": "Base directory to search from. Defaults to root '/'.",
"type": "string"
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Base directory to search from. Defaults to the backend's default root."
},
"pattern": {
"description": "Glob pattern to match files (e.g., '**/*.py', '*.txt', '/subdir/**/*.md').",
@@ -146,14 +146,21 @@
},
{
"function": {
"description": "Find files matching a glob pattern.\n\nSupports standard glob patterns: `*` (any characters), `**` (any directories), `?` (single character).\nReturns a list of absolute file paths that match the pattern.\n\nExamples:\n- `**/*.py` - Find all Python files\n- `*.txt` - Find all text files in root\n- `/subdir/**/*.md` - Find all markdown files under /subdir",
"description": "Find files matching a glob pattern.\n\nSupports standard glob patterns: `*` (any characters), `**` (any directories), `?` (single character).\nReturns a list of absolute file paths that match the pattern.\n\nExamples:\n- `**/*.py` - Find all Python files\n- `*.txt` - Find all text files in the backend's default root\n- `/subdir/**/*.md` - Find all markdown files under /subdir",
"name": "glob",
"parameters": {
"properties": {
"path": {
"default": "/",
"description": "Base directory to search from. Defaults to root '/'.",
"type": "string"
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Base directory to search from. Defaults to the backend's default root."
},
"pattern": {
"description": "Glob pattern to match files (e.g., '**/*.py', '*.txt', '/subdir/**/*.md').",
@@ -146,14 +146,21 @@
},
{
"function": {
"description": "Find files matching a glob pattern.\n\nSupports standard glob patterns: `*` (any characters), `**` (any directories), `?` (single character).\nReturns a list of absolute file paths that match the pattern.\n\nExamples:\n- `**/*.py` - Find all Python files\n- `*.txt` - Find all text files in root\n- `/subdir/**/*.md` - Find all markdown files under /subdir",
"description": "Find files matching a glob pattern.\n\nSupports standard glob patterns: `*` (any characters), `**` (any directories), `?` (single character).\nReturns a list of absolute file paths that match the pattern.\n\nExamples:\n- `**/*.py` - Find all Python files\n- `*.txt` - Find all text files in the backend's default root\n- `/subdir/**/*.md` - Find all markdown files under /subdir",
"name": "glob",
"parameters": {
"properties": {
"path": {
"default": "/",
"description": "Base directory to search from. Defaults to root '/'.",
"type": "string"
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Base directory to search from. Defaults to the backend's default root."
},
"pattern": {
"description": "Glob pattern to match files (e.g., '**/*.py', '*.txt', '/subdir/**/*.md').",
@@ -146,14 +146,21 @@
},
{
"function": {
"description": "Find files matching a glob pattern.\n\nSupports standard glob patterns: `*` (any characters), `**` (any directories), `?` (single character).\nReturns a list of absolute file paths that match the pattern.\n\nExamples:\n- `**/*.py` - Find all Python files\n- `*.txt` - Find all text files in root\n- `/subdir/**/*.md` - Find all markdown files under /subdir",
"description": "Find files matching a glob pattern.\n\nSupports standard glob patterns: `*` (any characters), `**` (any directories), `?` (single character).\nReturns a list of absolute file paths that match the pattern.\n\nExamples:\n- `**/*.py` - Find all Python files\n- `*.txt` - Find all text files in the backend's default root\n- `/subdir/**/*.md` - Find all markdown files under /subdir",
"name": "glob",
"parameters": {
"properties": {
"path": {
"default": "/",
"description": "Base directory to search from. Defaults to root '/'.",
"type": "string"
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Base directory to search from. Defaults to the backend's default root."
},
"pattern": {
"description": "Glob pattern to match files (e.g., '**/*.py', '*.txt', '/subdir/**/*.md').",
@@ -146,14 +146,21 @@
},
{
"function": {
"description": "Find files matching a glob pattern.\n\nSupports standard glob patterns: `*` (any characters), `**` (any directories), `?` (single character).\nReturns a list of absolute file paths that match the pattern.\n\nExamples:\n- `**/*.py` - Find all Python files\n- `*.txt` - Find all text files in root\n- `/subdir/**/*.md` - Find all markdown files under /subdir",
"description": "Find files matching a glob pattern.\n\nSupports standard glob patterns: `*` (any characters), `**` (any directories), `?` (single character).\nReturns a list of absolute file paths that match the pattern.\n\nExamples:\n- `**/*.py` - Find all Python files\n- `*.txt` - Find all text files in the backend's default root\n- `/subdir/**/*.md` - Find all markdown files under /subdir",
"name": "glob",
"parameters": {
"properties": {
"path": {
"default": "/",
"description": "Base directory to search from. Defaults to root '/'.",
"type": "string"
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Base directory to search from. Defaults to the backend's default root."
},
"pattern": {
"description": "Glob pattern to match files (e.g., '**/*.py', '*.txt', '/subdir/**/*.md').",
@@ -194,9 +194,10 @@ class LocalSubprocessSandbox(BaseSandbox):
match["path"] = self._to_virtual_path(match["path"])
return result
def glob(self, pattern: str, path: str = "/") -> GlobResult:
def glob(self, pattern: str, path: str | None = None) -> GlobResult:
"""Run glob against mapped real paths."""
result = super().glob(pattern, path=self._to_real_path(path))
mapped_path = self._to_real_path(path) if path is not None else None
result = super().glob(pattern, path=mapped_path)
if result.error is not None:
result.error = self._to_virtual_path(result.error)
if result.matches is not None:
+5 -4
View File
@@ -435,13 +435,14 @@ done
"""
raise NotImplementedError(_SYNC_NOT_SUPPORTED)
async def aglob(self, pattern: str, path: str = "/") -> GlobResult:
async def aglob(self, pattern: str, path: str | None = None) -> GlobResult:
"""Find files matching glob pattern using shell commands.
Please note that this implementation does not currently support all glob
patterns.
"""
safe_path = shlex.quote(path)
search_path = path or "/"
safe_path = shlex.quote(search_path)
safe_pattern = shlex.quote(pattern)
cmd = f"""
@@ -462,7 +463,7 @@ done
if result.exit_code != 0:
detail = result.output.strip() if result.output else ""
return GlobResult(
error=f"Path not found or not accessible: {path}"
error=f"Path not found or not accessible: {search_path}"
+ (f" ({detail})" if detail else "")
)
@@ -488,7 +489,7 @@ done
return GlobResult(matches=file_infos)
def glob(self, pattern: str, path: str = "/") -> GlobResult:
def glob(self, pattern: str, path: str | None = None) -> GlobResult:
"""Find files matching glob pattern using shell commands."""
raise NotImplementedError(_SYNC_NOT_SUPPORTED)