Skip to main content
Glama
orneryd

M.I.M.I.R - Multi-agent Intelligent Memory & Insight Repository

by orneryd
file-isolation.ts15.9 kB
/** * File Isolation Layer for Agent Testing * * Provides multiple strategies to prevent agents from accidentally modifying the repository: * 1. Virtual filesystem mode (in-memory, all operations logged) * 2. Folder whitelist mode (restrict to specific directories) * 3. Read-only mode (allow reads, block all writes) */ import fs from 'fs/promises'; import path from 'path'; export type IsolationMode = 'virtual' | 'restricted' | 'readonly' | 'disabled'; interface FileOperation { timestamp: Date; operation: 'read' | 'write' | 'delete' | 'command'; path: string; allowed: boolean; reason?: string; content?: string; // For writes result?: string; // For commands } interface VirtualFile { path: string; content: string; created: Date; modified: Date; } /** * Sandboxed filesystem for testing agents safely * * Provides multiple isolation strategies to prevent agents from accidentally * modifying the repository during testing: * * **Isolation Modes:** * - **virtual**: All operations in-memory, nothing touches disk * - **restricted**: Only allow operations in whitelisted directories * - **readonly**: Allow reads, block all writes/deletes * - **disabled**: No restrictions (use with caution) * * All operations are logged for analysis and debugging. * * @example * ```ts * // Virtual mode - safest for testing * const isolation = new FileIsolationManager('virtual'); * await isolation.writeFile('/test.txt', 'content'); * // File stored in memory, not on disk * * // Restricted mode - limit to specific directories * const isolation = new FileIsolationManager('restricted', ['/tmp/agent-test']); * await isolation.writeFile('/tmp/agent-test/file.txt', 'ok'); // Allowed * await isolation.writeFile('/etc/passwd', 'bad'); // Blocked * * // Get operation log * const summary = isolation.getSummary(); * console.log(`Blocked operations: ${summary.blocked}`); * ``` */ export class FileIsolationManager { private mode: IsolationMode; private virtualFS: Map<string, VirtualFile> = new Map(); private operations: FileOperation[] = []; private allowedDirs: Set<string> = new Set(); private blockedPatterns: RegExp[] = [ /node_modules/, /\.git/, /dist|build/, /\.env/, ]; constructor( mode: IsolationMode = 'virtual', allowedDirs: string[] = [] ) { this.mode = mode; this.allowedDirs = new Set(allowedDirs.map(d => path.resolve(d))); console.log(`📦 File Isolation: ${mode}${allowedDirs.length > 0 ? ` (${allowedDirs.length} allowed dirs)` : ''}`); } /** * Check if path is allowed based on current mode */ private isPathAllowed(filepath: string, operation: 'read' | 'write' | 'delete'): { allowed: boolean; reason?: string } { const resolved = path.resolve(filepath); // Check blocked patterns for (const pattern of this.blockedPatterns) { if (pattern.test(resolved)) { return { allowed: false, reason: `Path matches blocked pattern: ${pattern}`, }; } } if (this.mode === 'virtual') { // Virtual mode allows everything (logged in memory) return { allowed: true }; } if (this.mode === 'readonly') { // Readonly mode blocks all writes if (operation !== 'read') { return { allowed: false, reason: `Readonly mode: ${operation} operations blocked`, }; } return { allowed: true }; } if (this.mode === 'restricted') { // Restricted mode checks against whitelist if (this.allowedDirs.size === 0) { return { allowed: false, reason: 'Restricted mode: no allowed directories configured', }; } const isAllowed = Array.from(this.allowedDirs).some(dir => resolved.startsWith(dir) || resolved.startsWith(dir + path.sep) ); if (!isAllowed && operation !== 'read') { return { allowed: false, reason: `Restricted mode: path not in allowed directories`, }; } return { allowed: true }; } // Disabled mode allows everything return { allowed: true }; } /** * Log a file operation */ private logOperation( operation: FileOperation['operation'], filepath: string, allowed: boolean, reason?: string, content?: string ): void { this.operations.push({ timestamp: new Date(), operation, path: filepath, allowed, reason, content, }); } /** * Read file with isolation enforcement * * In virtual mode, checks in-memory filesystem first, then falls back to real FS. * In restricted/readonly modes, enforces access controls. * * @param filepath - Path to file to read * @returns File content as string * @throws Error if path is blocked or file doesn't exist * * @example * ```ts * const isolation = new FileIsolationManager('virtual'); * * // Read from virtual FS * await isolation.writeFile('/test.txt', 'hello'); * const content = await isolation.readFile('/test.txt'); * console.log(content); // 'hello' * * // Blocked patterns are rejected * try { * await isolation.readFile('/node_modules/package/index.js'); * } catch (error) { * console.log(error.message); // 'File read blocked: ...' * } * ``` */ async readFile(filepath: string): Promise<string> { const check = this.isPathAllowed(filepath, 'read'); if (this.mode === 'virtual') { // Check virtual filesystem first if (this.virtualFS.has(filepath)) { const vfile = this.virtualFS.get(filepath)!; this.logOperation('read', filepath, true, 'Virtual FS'); return vfile.content; } // Fall through to real filesystem } if (!check.allowed) { this.logOperation('read', filepath, false, check.reason); throw new Error(`File read blocked: ${check.reason}`); } try { const content = await fs.readFile(filepath, 'utf-8'); this.logOperation('read', filepath, true); return content; } catch (error: any) { this.logOperation('read', filepath, false, `FS Error: ${error.message}`); throw error; } } /** * Write file with isolation enforcement * * In virtual mode, stores in memory. In restricted mode, checks whitelist. * In readonly mode, blocks all writes. * * @param filepath - Path to file to write * @param content - Content to write * @throws Error if write is blocked by isolation mode * * @example * ```ts * // Virtual mode - safe testing * const isolation = new FileIsolationManager('virtual'); * await isolation.writeFile('/output.txt', 'result'); * // Stored in memory, not on disk * * // Readonly mode - blocks writes * const readonly = new FileIsolationManager('readonly'); * try { * await readonly.writeFile('/test.txt', 'data'); * } catch (error) { * console.log(error.message); // 'File write blocked: Readonly mode...' * } * ``` */ async writeFile(filepath: string, content: string): Promise<void> { const check = this.isPathAllowed(filepath, 'write'); if (!check.allowed) { this.logOperation('write', filepath, false, check.reason, content); throw new Error(`File write blocked: ${check.reason}`); } if (this.mode === 'virtual') { // Store in virtual filesystem this.virtualFS.set(filepath, { path: filepath, content, created: new Date(), modified: new Date(), }); this.logOperation('write', filepath, true, 'Virtual FS', content); return; } try { await fs.mkdir(path.dirname(filepath), { recursive: true }); await fs.writeFile(filepath, content, 'utf-8'); this.logOperation('write', filepath, true, 'Real FS', content); } catch (error: any) { this.logOperation('write', filepath, false, `FS Error: ${error.message}`, content); throw error; } } /** * Delete file (respects isolation mode) */ async deleteFile(filepath: string): Promise<void> { const check = this.isPathAllowed(filepath, 'delete'); if (!check.allowed) { this.logOperation('delete', filepath, false, check.reason); throw new Error(`File delete blocked: ${check.reason}`); } if (this.mode === 'virtual') { // Remove from virtual filesystem this.virtualFS.delete(filepath); this.logOperation('delete', filepath, true, 'Virtual FS'); return; } try { await fs.unlink(filepath); this.logOperation('delete', filepath, true, 'Real FS'); } catch (error: any) { this.logOperation('delete', filepath, false, `FS Error: ${error.message}`); throw error; } } /** * Get operations log */ getOperations(): FileOperation[] { return [...this.operations]; } /** * Get summary statistics of all file operations * * @returns Object with operation counts and statistics * * @example * ```ts * const isolation = new FileIsolationManager('virtual'); * await isolation.writeFile('/test1.txt', 'a'); * await isolation.writeFile('/test2.txt', 'b'); * await isolation.readFile('/test1.txt'); * * const summary = isolation.getSummary(); * console.log(summary); * // { * // totalOperations: 3, * // reads: 1, * // writes: 2, * // deletes: 0, * // blocked: 0, * // virtualFiles: 2 * // } * ``` */ getSummary(): { totalOperations: number; reads: number; writes: number; deletes: number; blocked: number; virtualFiles: number; } { return { totalOperations: this.operations.length, reads: this.operations.filter(op => op.operation === 'read').length, writes: this.operations.filter(op => op.operation === 'write').length, deletes: this.operations.filter(op => op.operation === 'delete').length, blocked: this.operations.filter(op => !op.allowed).length, virtualFiles: this.virtualFS.size, }; } /** * Generate detailed operations log in Markdown format * * Creates a comprehensive report including: * - Operation summary statistics * - Timeline of all operations * - List of virtual files in memory * - List of blocked operations * * @returns Markdown-formatted log string * * @example * ```ts * const isolation = new FileIsolationManager('restricted', ['/tmp/test']); * await isolation.writeFile('/tmp/test/ok.txt', 'allowed'); * try { * await isolation.writeFile('/etc/passwd', 'blocked'); * } catch {} * * const log = isolation.generateOperationsLog(); * console.log(log); * // # File Operations Log * // **Mode:** restricted * // **Total Operations:** 2 * // - Writes: 2 * // - Blocked: 1 * // ... * ``` */ generateOperationsLog(): string { const summary = this.getSummary(); let log = `# File Operations Log\n\n`; log += `**Mode:** ${this.mode}\n`; log += `**Total Operations:** ${summary.totalOperations}\n`; log += `- Reads: ${summary.reads}\n`; log += `- Writes: ${summary.writes}\n`; log += `- Deletes: ${summary.deletes}\n`; log += `- Blocked: ${summary.blocked}\n`; log += `- Virtual Files in Memory: ${summary.virtualFiles}\n\n`; if (this.operations.length === 0) { log += `No operations recorded.\n`; return log; } log += `## Operations Timeline\n\n`; log += `| Time | Operation | Path | Status | Reason |\n`; log += `|------|-----------|------|--------|--------|\n`; for (const op of this.operations) { const time = op.timestamp.toISOString().split('T')[1]; const status = op.allowed ? '✅ Allowed' : '🚫 Blocked'; const reason = op.reason || '-'; log += `| ${time} | ${op.operation} | ${op.path} | ${status} | ${reason} |\n`; } // List virtual files if (this.virtualFS.size > 0) { log += `\n## Virtual Files in Memory\n\n`; for (const [path, file] of this.virtualFS) { const lines = file.content.split('\n').length; log += `- \`${path}\` (${lines} lines, ${file.content.length} bytes)\n`; } } // List blocked operations const blocked = this.operations.filter(op => !op.allowed); if (blocked.length > 0) { log += `\n## Blocked Operations\n\n`; for (const op of blocked) { log += `- **${op.operation}** \`${op.path}\`: ${op.reason}\n`; } } return log; } /** * Get virtual file content */ getVirtualFile(filepath: string): VirtualFile | undefined { return this.virtualFS.get(filepath); } /** * Export all virtual files as JSON object * * @returns Object mapping file paths to their content * * @example * ```ts * const isolation = new FileIsolationManager('virtual'); * await isolation.writeFile('/test1.txt', 'content1'); * await isolation.writeFile('/test2.txt', 'content2'); * * const files = isolation.exportVirtualFiles(); * console.log(files); * // { * // '/test1.txt': 'content1', * // '/test2.txt': 'content2' * // } * ``` */ exportVirtualFiles(): Record<string, string> { const exported: Record<string, string> = {}; for (const [path, file] of this.virtualFS) { exported[path] = file.content; } return exported; } /** * Save all virtual files to disk after testing * * Writes all in-memory files to the specified output directory, * preserving the relative path structure. * * @param outputDir - Directory to save files to * * @example * ```ts * const isolation = new FileIsolationManager('virtual'); * await isolation.writeFile('/workspace/output.txt', 'result'); * await isolation.writeFile('/workspace/data.json', '{"key": "value"}'); * * // After testing, save to disk * await isolation.saveVirtualFiles('/tmp/test-results'); * // Creates: * // /tmp/test-results/workspace/output.txt * // /tmp/test-results/workspace/data.json * ``` */ async saveVirtualFiles(outputDir: string): Promise<void> { for (const [filepath, file] of this.virtualFS) { const outputPath = path.join(outputDir, path.relative(process.cwd(), filepath)); await fs.mkdir(path.dirname(outputPath), { recursive: true }); await fs.writeFile(outputPath, file.content, 'utf-8'); } } /** * Clear all virtual files and operation logs * * Resets the isolation manager to a clean state for the next test. * * @example * ```ts * const isolation = new FileIsolationManager('virtual'); * await isolation.writeFile('/test.txt', 'data'); * console.log(isolation.getSummary().virtualFiles); // 1 * * isolation.reset(); * console.log(isolation.getSummary().virtualFiles); // 0 * ``` */ reset(): void { this.virtualFS.clear(); this.operations = []; } } /** * Create isolated filesystem manager for agent testing * * Factory function to create a FileIsolationManager with the specified mode. * * @param mode - Isolation mode (virtual, restricted, readonly, disabled) * @param allowedDirs - Optional array of allowed directories (for restricted mode) * @returns Configured FileIsolationManager instance * * @example * ```ts * // Virtual mode for safe testing * const isolation = createFileIsolation('virtual'); * * // Restricted mode with whitelist * const restricted = createFileIsolation('restricted', [ * '/tmp/agent-sandbox', * '/workspace/output' * ]); * * // Readonly mode for analysis * const readonly = createFileIsolation('readonly'); * ``` */ export function createFileIsolation( mode: IsolationMode = 'virtual', allowedDirs?: string[] ): FileIsolationManager { return new FileIsolationManager(mode, allowedDirs); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/orneryd/Mimir'

If you have feedback or need assistance with the MCP directory API, please join our Discord server