from pathlib import Path from typing import Any from duck_core.tools.base import ToolResult from duck_core.tools.paths import WorkspacePathError, candidate_path, resolve_workspace_path class FileReadTool: name = "file_read" risk_level = "low" def __init__(self, workspace: str, max_bytes: int = 1_000_000): self.workspace = workspace self.max_bytes = max_bytes async def run(self, args: dict[str, Any]) -> ToolResult: raw_path = str(args.get("path", "")) approved = bool(args.get("_approved")) try: path = resolve_workspace_path(self.workspace, raw_path, allow_outside=approved) except WorkspacePathError as exc: path = candidate_path(self.workspace, raw_path) return self._approval_required(raw_path, path, str(exc)) if self._requires_approval(path) and not approved: return self._approval_required(raw_path, path, f"Reading {raw_path} requires explicit approval") if not path.is_file(): return ToolResult(ok=False, error=f"File not found: {raw_path}") if path.stat().st_size > self.max_bytes: return ToolResult(ok=False, error=f"File exceeds max size: {self.max_bytes}") return ToolResult( ok=True, output=path.read_text(errors="replace"), metadata={"path": str(path), "bytes_read": path.stat().st_size}, ) def _requires_approval(self, path: Path) -> bool: parts = set(path.parts) return path.name == ".env" or ".ssh" in parts or str(path) == "/etc/shadow" def _approval_required(self, raw_path: str, path: Path, reason: str) -> ToolResult: return ToolResult( ok=False, error=reason, metadata={ "path": str(path), "requires_approval": True, "risk_level": self.risk_level, "reason": reason, }, )