diff --git a/.env.example b/.env.example index 3e76aef..aac9765 100644 --- a/.env.example +++ b/.env.example @@ -5,7 +5,7 @@ ANTHROPIC_API_KEY= # Filesystem tool: comma-separated allowed root paths -FS_ALLOWED_PATHS=/tmp,/home,/etc,/var,/opt +FS_ALLOWED_PATHS=* # Terminal tool: "*" = allow all commands (default, suitable for local use) # Restrict with a comma-separated list, e.g.: ls,cat,git,systemctl diff --git a/navi/config.py b/navi/config.py index fedd6cc..d2ba20f 100644 --- a/navi/config.py +++ b/navi/config.py @@ -11,7 +11,7 @@ anthropic_api_key: str = "" # Filesystem tool: comma-separated allowed root paths - fs_allowed_paths: str = "/tmp,/home,/etc,/var,/opt" + fs_allowed_paths: str = "*" # Terminal tool: "*" = allow all commands (recommended for local use) # or comma-separated list of allowed executables, e.g. "ls,cat,git" diff --git a/navi/tools/filesystem.py b/navi/tools/filesystem.py index 83afdc6..c06ec0a 100644 --- a/navi/tools/filesystem.py +++ b/navi/tools/filesystem.py @@ -1,24 +1,36 @@ -"""Filesystem tool — read/write/list files within allowed paths.""" +"""Filesystem tool — read/write/list/delete files. -import os +If FS_ALLOWED_PATHS=* (default), any path is accessible. +Otherwise set a comma-separated list of allowed root paths, e.g.: + FS_ALLOWED_PATHS=/home/user,/var/www +""" + +import shutil from pathlib import Path from navi.config import settings from .base import Tool, ToolResult -_ALLOWED = [Path(p).resolve() for p in settings.fs_allowed_paths_list] - def _check_path(path_str: str) -> Path | None: - """Return resolved Path if it's within an allowed root, else None.""" + """Return resolved Path if access is allowed, else None. + + Called per-request so config changes take effect on restart without + needing module-level state. + """ try: - p = Path(path_str).resolve() + p = Path(path_str).expanduser().resolve() except Exception: return None - for allowed in _ALLOWED: + + if settings.fs_allowed_paths.strip() == "*": + return p + + allowed = [Path(r).expanduser().resolve() for r in settings.fs_allowed_paths_list] + for root in allowed: try: - p.relative_to(allowed) + p.relative_to(root) return p except ValueError: continue @@ -28,8 +40,7 @@ class FilesystemTool(Tool): name = "filesystem" description = ( - "Read, write, list, or delete files and directories. " - "Only paths within configured allowed roots are accessible." + "Read, write, list, or delete files and directories on the local filesystem." ) parameters = { "type": "object", @@ -39,7 +50,10 @@ "enum": ["read", "write", "list", "delete", "exists"], "description": "Operation to perform", }, - "path": {"type": "string", "description": "Absolute or relative file/directory path"}, + "path": { + "type": "string", + "description": "Absolute or relative file/directory path (~ is expanded)", + }, "content": { "type": "string", "description": "Content to write (required for 'write' action)", @@ -54,9 +68,13 @@ path = _check_path(raw_path) if path is None: + allowed = settings.fs_allowed_paths return ToolResult( success=False, - output=f"Access denied: '{raw_path}' is outside allowed paths.", + output=( + f"Access denied: '{raw_path}' is outside allowed paths ({allowed}). " + f"Set FS_ALLOWED_PATHS=* in .env to allow all paths." + ), error="access_denied", ) @@ -80,16 +98,13 @@ if path.is_file(): return ToolResult(success=True, output=str(path)) entries = sorted(path.iterdir(), key=lambda e: (e.is_file(), e.name)) - lines = [ - f"{' ' if e.is_file() else 'd '}{e.name}" for e in entries - ] + lines = [f"{' ' if e.is_file() else 'd '}{e.name}" for e in entries] return ToolResult(success=True, output="\n".join(lines) or "(empty directory)") case "delete": if not path.exists(): return ToolResult(success=False, output=f"Not found: {path}", error="not_found") if path.is_dir(): - import shutil shutil.rmtree(path) else: path.unlink()