178 lines
4.6 KiB
TypeScript
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',
|
|
};
|
|
}
|
|
};
|