Source code for yoker.tools.read

"""Read tool implementation for Yoker.

Provides the ReadTool for reading file contents with guardrail validation,
path resolution, symlink rejection, and explicit encoding.
"""

import os
from pathlib import Path
from typing import TYPE_CHECKING, Any

from yoker.logging import get_logger
from yoker.tools.base import Tool, ToolResult

if TYPE_CHECKING:
  from yoker.tools.guardrails import Guardrail

log = get_logger(__name__)


[docs] class ReadTool(Tool): """Tool for reading file contents. Reads the entire contents of a file as text with defense-in-depth validation. When a guardrail is provided, validates parameters before reading. Resolves paths with realpath, rejects symlinks by default, and reads with explicit UTF-8 encoding. Error messages returned to the LLM are sanitized to avoid leaking filesystem structure. Full paths are logged internally for debugging. """ def __init__(self, guardrail: "Guardrail | None" = None) -> None: """Initialize ReadTool with optional guardrail. Args: guardrail: Optional guardrail for parameter validation. """ super().__init__(guardrail=guardrail) @property def name(self) -> str: return "read" @property def description(self) -> str: return "Read the contents of a file"
[docs] def get_schema(self) -> dict[str, Any]: """Return Ollama-compatible schema for the read tool. Returns: Dict with 'type': 'function' and function metadata. """ return { "type": "function", "function": { "name": self.name, "description": self.description, "parameters": { "type": "object", "properties": { "path": { "type": "string", "description": "Path to the file to read", } }, "required": ["path"], }, }, }
[docs] async def execute(self, **kwargs: Any) -> ToolResult: """Read a file and return its contents. Steps: 1. Validate parameters via guardrail if provided. 2. Resolve the path with os.path.realpath(). 3. Reject symlinks unless explicitly allowed. 4. Verify the file exists. 5. Read with UTF-8 encoding and replacement for invalid bytes. 6. Log access for audit trail. Args: **kwargs: Must contain 'path' key with file path. Returns: ToolResult with file content or sanitized error message. """ path_str = kwargs.get("path", "") # Defense-in-depth: validate via guardrail if provided if self._guardrail is not None: validation = self._guardrail.validate(self.name, kwargs) if not validation.valid: log.info( "read_guardrail_blocked", path=path_str, reason=validation.reason, ) return ToolResult( success=False, result="", error=validation.reason, ) # Ensure path is a string if not isinstance(path_str, str): log.warning("read_invalid_path_type", path_type=type(path_str).__name__) return ToolResult( success=False, result="", error="Invalid path parameter", ) # Reject symlinks before resolving to prevent traversal via symlinks original_path = Path(path_str) if original_path.is_symlink(): log.warning("read_symlink_rejected", path=path_str) return ToolResult( success=False, result="", error="Reading symlinks is not permitted", ) # Resolve the path to prevent traversal and normalize try: resolved = Path(os.path.realpath(path_str)) except (OSError, ValueError): log.warning("read_invalid_path", path=path_str) return ToolResult( success=False, result="", error="Invalid path", ) # Verify the resolved path exists and is a file if not resolved.exists(): log.info("read_file_not_found", path=str(resolved)) return ToolResult( success=False, result="", error="File not found", ) if not resolved.is_file(): log.info("read_not_a_file", path=str(resolved)) return ToolResult( success=False, result="", error="Path is not a file", ) # Read the file with explicit encoding try: content = resolved.read_text(encoding="utf-8", errors="replace") log.info( "read_success", path=str(resolved), bytes=len(content.encode("utf-8")), ) return ToolResult(success=True, result=content) except PermissionError: log.warning("read_permission_denied", path=str(resolved)) return ToolResult( success=False, result="", error="Permission denied", ) except OSError as e: log.error("read_os_error", path=str(resolved), error=str(e)) return ToolResult( success=False, result="", error="Error reading file", )