Files
chat-gpz/lib/tools/file-operations.ts
T
Zacharias-Brohn bcebaed78f changes
2026-01-14 22:36:22 +01:00

178 lines
4.6 KiB
TypeScript

/**
* File Operations tools - read and write files with path restrictions
*/
import { promises as fs } from 'fs';
import path from 'path';
import { Tool } from 'ollama';
import { ToolHandler, ToolResult } from './types';
// Configurable allowed directories (can be set via environment variable)
const ALLOWED_DIRECTORIES = (process.env.TOOL_FILE_ALLOWED_PATHS || '/tmp,./workspace')
.split(',')
.map((p) => path.resolve(p.trim()));
// Maximum file size for reading (5MB)
const MAX_READ_SIZE = 5 * 1024 * 1024;
// Maximum file size for writing (1MB)
const MAX_WRITE_SIZE = 1 * 1024 * 1024;
/**
* Check if a path is within allowed directories
*/
function isPathAllowed(filePath: string): boolean {
const resolved = path.resolve(filePath);
return ALLOWED_DIRECTORIES.some((allowed) => resolved.startsWith(allowed));
}
export const fileReadTool: Tool = {
type: 'function',
function: {
name: 'read_file',
description: `Read the contents of a file. For security, only files in these directories can be accessed: ${ALLOWED_DIRECTORIES.join(', ')}`,
parameters: {
type: 'object',
properties: {
path: {
type: 'string',
description: 'Path to the file to read',
},
encoding: {
type: 'string',
enum: ['utf-8', 'base64'],
description: 'File encoding (default: utf-8). Use base64 for binary files.',
},
},
required: ['path'],
},
},
};
export const fileWriteTool: Tool = {
type: 'function',
function: {
name: 'write_file',
description: `Write content to a file. For security, only files in these directories can be written: ${ALLOWED_DIRECTORIES.join(', ')}`,
parameters: {
type: 'object',
properties: {
path: {
type: 'string',
description: 'Path to the file to write',
},
content: {
type: 'string',
description: 'Content to write to the file',
},
append: {
type: 'boolean',
description: 'If true, append to file instead of overwriting (default: false)',
},
},
required: ['path', 'content'],
},
},
};
export const fileReadHandler: ToolHandler = async (args): Promise<ToolResult> => {
const filePath = args.path as string;
const encoding = (args.encoding as 'utf-8' | 'base64') || 'utf-8';
if (!filePath) {
return {
success: false,
error: 'No file path provided',
};
}
if (!isPathAllowed(filePath)) {
return {
success: false,
error: `Access denied. File must be in one of: ${ALLOWED_DIRECTORIES.join(', ')}`,
};
}
try {
const resolved = path.resolve(filePath);
// Check file size
const stats = await fs.stat(resolved);
if (stats.size > MAX_READ_SIZE) {
return {
success: false,
error: `File too large (${stats.size} bytes). Maximum: ${MAX_READ_SIZE} bytes`,
};
}
const content = await fs.readFile(resolved, encoding === 'base64' ? 'base64' : 'utf-8');
return {
success: true,
result: `Contents of ${filePath}:\n\n${content}`,
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to read file',
};
}
};
export const fileWriteHandler: ToolHandler = async (args): Promise<ToolResult> => {
const filePath = args.path as string;
const content = args.content as string;
const append = (args.append as boolean) || false;
if (!filePath) {
return {
success: false,
error: 'No file path provided',
};
}
if (content === undefined || content === null) {
return {
success: false,
error: 'No content provided',
};
}
if (!isPathAllowed(filePath)) {
return {
success: false,
error: `Access denied. File must be in one of: ${ALLOWED_DIRECTORIES.join(', ')}`,
};
}
if (content.length > MAX_WRITE_SIZE) {
return {
success: false,
error: `Content too large (${content.length} bytes). Maximum: ${MAX_WRITE_SIZE} bytes`,
};
}
try {
const resolved = path.resolve(filePath);
// Ensure directory exists
await fs.mkdir(path.dirname(resolved), { recursive: true });
if (append) {
await fs.appendFile(resolved, content, 'utf-8');
} else {
await fs.writeFile(resolved, content, 'utf-8');
}
return {
success: true,
result: `Successfully ${append ? 'appended to' : 'wrote'} file: ${filePath}`,
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to write file',
};
}
};