"File Tools: ReadFileTool, WriteFileTool, ListFilesTool — Path Validation Deep Dive"

The first set of agent tools — how ReadFileTool validates paths with realpath() + str_starts_with() against allowed/blocked lists, why WriteFileTool uses atomic temp+rename writes, ListFilesTool's glob patterns and depth limits, the new BatchReadTool that reads up to 8 files in one call (saving 3-5 iterations), and the ToolInterface contract. Why tools live in baseKRIZAN, not LIMAgents.

Every agent that interacts with the codebase starts with file tools. ReadFileTool reads files. WriteFileTool creates or overwrites them. ListFilesTool shows what's there. Simple operations, but the security model around them is what makes agents safe.

All tools live in app/baseKRIZAN/Agents/Tools/ — framework code, not addon code. Any agent in any module uses the same tools with the same security guarantees.


ToolInterface: The Contract

interface ToolInterface
{
    public function getName(): string;
    public function getDescription(): string;
    public function getInputSchema(): array;
    public function execute(array $params): array;
}

Four methods. getName() returns the tool identifier the AI uses in tool_use blocks. getDescription() gives the AI context about when to use the tool. getInputSchema() returns JSON Schema for parameters. execute() does the work and returns ['success' => bool, 'content' => string].


ReadFileTool

class ReadFileTool implements ToolInterface
{
    public function __construct(
        private string $basePath,
        private array $allowedPaths = [],
        private array $blockedPaths = [],
        private int $maxFileSize = 102400  // 100KB
    ) {}
}

Path Validation

Every file read goes through two checks:

1. realpath() resolution — resolves symlinks and ../ traversal:

$realPath = realpath($fullPath);
if ($realPath === false) {
    return ['success' => false, 'error' => 'File not found'];
}

If an agent requests ../../etc/passwd, realpath() resolves it to /etc/passwd — which won't pass the next check.

2. str_starts_with() against base path:

if (!str_starts_with($realPath, $this->basePath)) {
    return ['success' => false, 'error' => 'Path outside allowed directory'];
}

The file must be within the project root. No escaping to system files.

3. Blocked paths — additional restrictions per agent:

foreach ($this->blockedPaths as $blocked) {
    if (str_starts_with($realPath, $blocked)) {
        return ['success' => false, 'error' => 'Path is blocked'];
    }
}

VajbCoder has app/baseKRIZAN/ in its blocked list — it can't read framework internals. TestAgent has no blocked paths — it needs to read everything to write effective tests.

Line Range Support

'input_schema' => [
    'properties' => [
        'file' => ['type' => 'string'],
        'start_line' => ['type' => 'integer'],
        'end_line' => ['type' => 'integer'],
    ],
    'required' => ['file'],
]

Agents can request specific line ranges — useful when they already know which part of a file they need. Reduces context window usage.


WriteFileTool

Same constructor pattern (basePath, allowedPaths, blockedPaths, maxFileSize). Additional security: allowed paths are checked positively — the file must be in at least one allowed path.

Atomic Writes

$tempFile = $fullPath . '.tmp.' . bin2hex(random_bytes(4));
file_put_contents($tempFile, $content);
rename($tempFile, $fullPath);

Write to temp file, then rename(). This is atomic on most filesystems — the file either has the old content or the new content, never a partially-written state. If the write fails (disk full, permissions), the original file is untouched.

Agent-Specific Boundaries

// VajbCoder can write to:
$allowedPaths = ['moduli/', 'app/Controllers/', 'app/Models/', 'app/Jobs/'];

// TestAgent can only write to:
$allowedPaths = ['tests/'];

// ReviewAgent has NO WriteFileTool (read-only agent)

The boundaries are set in agent constructors and enforced at the tool level. Even if an AI generates a write_file call for app/baseKRIZAN/Bootstrap.php, the tool rejects it before touching the filesystem.


ListFilesTool

public function execute(array $params): array
{
    $path = $params['path'] ?? '.';
    $pattern = $params['pattern'] ?? '*';
    $maxDepth = min($params['depth'] ?? 3, 5);  // Capped at 5
    // ...
}

Glob-based directory listing. Depth is capped at 5 levels to prevent agents from requesting the entire filesystem tree. The tool returns file names, sizes, and types — enough for an agent to decide what to read next.


Why baseKRIZAN, Not LIMAgents

Tools used to live in app/addons/LIMAgents/Agents/Tools/. They were moved to app/baseKRIZAN/Agents/Tools/ during the refactoring. The reason: module-specific agents (PMAgent in the PM module, FiskAgent in fiskalizacija) also need file tools. If tools live in LIMAgents, every module depends on LIMAgents. If tools live in baseKRIZAN, they're framework utilities — available to any agent regardless of which addon it belongs to.

DelegateAgentTool, ListAgentsTool, and other orchestration tools also moved to baseKRIZAN for the same reason.


Up Next

Next up: GrepCodebaseTool and DatabaseSchemaTool: How Agents Understand the Codebase — regex search with file type filters, result truncation at 6K chars, and database introspection that gives agents the schema context they need to write correct SQL.

Comments (0)

No comments yet. Be the first to share your thoughts!

Leave a Comment

Recent Posts