""" tools/bound_filesystem_tool.py — Filesystem tool pre-scoped to a single directory. Used by email handling agents to read/write their memory and reasoning files. No filesystem_whitelist lookup — containment is enforced internally via realpath check. """ from __future__ import annotations import os from .base import BaseTool, ToolResult class BoundFilesystemTool(BaseTool): name = "filesystem" requires_confirmation = False allowed_in_scheduled_tasks = True def __init__(self, base_path: str) -> None: self._base = os.path.realpath(base_path) self.description = ( f"Read and write files inside your data folder ({self._base}). " "Operations: read_file, write_file, append_file, list_directory. " "All paths are relative to your data folder." ) self.input_schema = { "type": "object", "properties": { "operation": { "type": "string", "enum": ["read_file", "write_file", "append_file", "list_directory"], "description": "The operation to perform", }, "path": { "type": "string", "description": ( "File or directory path, relative to your data folder " f"(e.g. 'memory_work.md'). Absolute paths are also accepted " f"if they start with {self._base}." ), }, "content": { "type": "string", "description": "Content to write or append (for write_file / append_file)", }, }, "required": ["operation", "path"], } def _resolve(self, path: str) -> str | None: """Resolve path to absolute and verify it stays within base_path.""" if os.path.isabs(path): resolved = os.path.realpath(path) else: resolved = os.path.realpath(os.path.join(self._base, path)) if resolved == self._base or resolved.startswith(self._base + os.sep): return resolved return None # escape attempt async def execute(self, operation: str = "", path: str = "", content: str = "", **_) -> ToolResult: resolved = self._resolve(path) if resolved is None: return ToolResult(success=False, error=f"Path '{path}' is outside the allowed folder.") try: if operation == "read_file": if not os.path.isfile(resolved): return ToolResult(success=False, error=f"File not found: {path}") with open(resolved, encoding="utf-8") as f: text = f.read() return ToolResult(success=True, data={"path": path, "content": text, "size": len(text)}) elif operation == "write_file": os.makedirs(os.path.dirname(resolved), exist_ok=True) with open(resolved, "w", encoding="utf-8") as f: f.write(content) return ToolResult(success=True, data={"path": path, "bytes_written": len(content.encode())}) elif operation == "append_file": os.makedirs(os.path.dirname(resolved), exist_ok=True) with open(resolved, "a", encoding="utf-8") as f: f.write(content) return ToolResult(success=True, data={"path": path, "bytes_appended": len(content.encode())}) elif operation == "list_directory": target = resolved if os.path.isdir(resolved) else self._base entries = [] for name in sorted(os.listdir(target)): full = os.path.join(target, name) entries.append({"name": name, "type": "dir" if os.path.isdir(full) else "file", "size": os.path.getsize(full) if os.path.isfile(full) else None}) return ToolResult(success=True, data={"path": path, "entries": entries}) else: return ToolResult(success=False, error=f"Unknown operation: {operation!r}") except OSError as e: return ToolResult(success=False, error=f"Filesystem error: {e}")