/** * Agent Workspace Isolation * * Each agent gets its own tools, memory, identity, and workspace. * Provides isolation and resource management for parallel agents. */ import { randomUUID } from 'crypto'; import { EventEmitter } from 'events'; import { mkdirSync, rmSync, existsSync, writeFileSync, readFileSync, readdirSync, statSync } from 'fs'; import { join, resolve, relative } from 'path'; // ============================================================================ // Types // ============================================================================ export type Permission = 'read' | 'write' | 'execute' | 'delete' | 'network' | 'git'; export interface WorkspaceConfig { id: string; projectId: string; agentId: string; role: string; basePath: string; permissions: Permission[]; resourceLimits: ResourceLimits; environment: Record; mountPoints: MountPoint[]; } export interface ResourceLimits { maxMemoryMB: number; maxCpuPercent: number; maxFileSizeMB: number; maxExecutionTimeMs: number; maxFileCount: number; } export interface MountPoint { source: string; target: string; readOnly: boolean; } export interface AgentTool { name: string; description: string; permissions: Permission[]; execute: (params: unknown, context: ToolContext) => Promise; } export interface ToolContext { workspace: WorkspaceManager; agentId: string; sessionId: string; permissions: Permission[]; } export interface ToolResult { success: boolean; output?: unknown; error?: string; metadata?: Record; } export interface MemoryStore { shortTerm: Map; longTerm: Map; session: Map; } export interface AgentIdentity { id: string; name: string; role: string; description: string; personality: string; systemPrompt: string; capabilities: string[]; constraints: string[]; } // ============================================================================ // Workspace Manager // ============================================================================ /** * WorkspaceManager - Isolated workspace for an agent */ export class WorkspaceManager extends EventEmitter { private config: WorkspaceConfig; private workspacePath: string; private memory: MemoryStore; private identity: AgentIdentity; private tools: Map = new Map(); private fileHandles: Map = new Map(); private active = true; constructor(config: WorkspaceConfig) { super(); this.config = config; this.workspacePath = resolve(config.basePath, config.projectId, config.agentId); this.memory = { shortTerm: new Map(), longTerm: new Map(), session: new Map() }; this.initializeWorkspace(); } /** * Initialize the workspace directory */ private initializeWorkspace(): void { if (!existsSync(this.workspacePath)) { mkdirSync(this.workspacePath, { recursive: true }); } // Create subdirectories const subdirs = ['memory', 'output', 'cache', 'logs']; for (const dir of subdirs) { const path = join(this.workspacePath, dir); if (!existsSync(path)) { mkdirSync(path, { recursive: true }); } } this.emit('workspaceInitialized', { path: this.workspacePath }); } /** * Set agent identity */ setIdentity(identity: AgentIdentity): void { this.identity = identity; this.emit('identitySet', { identity }); } /** * Get agent identity */ getIdentity(): AgentIdentity | undefined { return this.identity; } /** * Register a tool */ registerTool(tool: AgentTool): void { // Check if agent has required permissions const hasPermission = tool.permissions.every(p => this.config.permissions.includes(p) ); if (!hasPermission) { throw new Error(`Agent does not have required permissions for tool: ${tool.name}`); } this.tools.set(tool.name, tool); this.emit('toolRegistered', { tool }); } /** * Unregister a tool */ unregisterTool(name: string): boolean { return this.tools.delete(name); } /** * Execute a tool */ async executeTool(name: string, params: unknown): Promise { const tool = this.tools.get(name); if (!tool) { return { success: false, error: `Tool not found: ${name}` }; } const context: ToolContext = { workspace: this, agentId: this.config.agentId, sessionId: this.config.id, permissions: this.config.permissions }; try { const result = await tool.execute(params, context); this.emit('toolExecuted', { name, params, result }); return result; } catch (error) { const result: ToolResult = { success: false, error: error instanceof Error ? error.message : String(error) }; this.emit('toolError', { name, params, error: result.error }); return result; } } /** * Get available tools */ getAvailableTools(): AgentTool[] { return Array.from(this.tools.values()); } // ============================================================================ // Memory Management // ============================================================================ /** * Store value in short-term memory */ remember(key: string, value: unknown): void { this.memory.shortTerm.set(key, value); this.emit('memoryStored', { type: 'shortTerm', key }); } /** * Store value in long-term memory */ memorize(key: string, value: unknown): void { this.memory.longTerm.set(key, value); this.saveMemoryToFile(key, value, 'longTerm'); this.emit('memoryStored', { type: 'longTerm', key }); } /** * Store value in session memory */ storeSession(key: string, value: unknown): void { this.memory.session.set(key, value); this.emit('memoryStored', { type: 'session', key }); } /** * Retrieve value from memory */ recall(key: string): unknown | undefined { return ( this.memory.shortTerm.get(key) || this.memory.longTerm.get(key) || this.memory.session.get(key) ); } /** * Check if memory exists */ hasMemory(key: string): boolean { return ( this.memory.shortTerm.has(key) || this.memory.longTerm.has(key) || this.memory.session.has(key) ); } /** * Forget a memory */ forget(key: string): boolean { return ( this.memory.shortTerm.delete(key) || this.memory.longTerm.delete(key) || this.memory.session.delete(key) ); } /** * Clear all short-term memory */ clearShortTerm(): void { this.memory.shortTerm.clear(); this.emit('memoryCleared', { type: 'shortTerm' }); } /** * Clear session memory */ clearSession(): void { this.memory.session.clear(); this.emit('memoryCleared', { type: 'session' }); } /** * Save memory to file */ private saveMemoryToFile(key: string, value: unknown, type: string): void { const memoryPath = join(this.workspacePath, 'memory', `${type}.json`); let data: Record = {}; if (existsSync(memoryPath)) { try { data = JSON.parse(readFileSync(memoryPath, 'utf-8')); } catch { data = {}; } } data[key] = value; writeFileSync(memoryPath, JSON.stringify(data, null, 2), 'utf-8'); } /** * Load long-term memory from file */ loadLongTermMemory(): void { const memoryPath = join(this.workspacePath, 'memory', 'longTerm.json'); if (existsSync(memoryPath)) { try { const data = JSON.parse(readFileSync(memoryPath, 'utf-8')); for (const [key, value] of Object.entries(data)) { this.memory.longTerm.set(key, value); } } catch { // Ignore errors } } } // ============================================================================ // File Operations // ============================================================================ /** * Read a file */ readFile(path: string): string { this.checkPermission('read'); const fullPath = this.resolvePath(path); this.checkPathInWorkspace(fullPath); return readFileSync(fullPath, 'utf-8'); } /** * Write a file */ writeFile(path: string, content: string): void { this.checkPermission('write'); const fullPath = this.resolvePath(path); this.checkPathInWorkspace(fullPath); this.checkFileSize(content.length); writeFileSync(fullPath, content, 'utf-8'); this.emit('fileWritten', { path: fullPath }); } /** * Delete a file */ deleteFile(path: string): void { this.checkPermission('delete'); const fullPath = this.resolvePath(path); this.checkPathInWorkspace(fullPath); rmSync(fullPath, { force: true }); this.emit('fileDeleted', { path: fullPath }); } /** * List files in a directory */ listFiles(path: string = ''): string[] { this.checkPermission('read'); const fullPath = this.resolvePath(path); this.checkPathInWorkspace(fullPath); if (!existsSync(fullPath)) return []; return readdirSync(fullPath).map(name => join(path, name)); } /** * Check if file exists */ fileExists(path: string): boolean { const fullPath = this.resolvePath(path); this.checkPathInWorkspace(fullPath); return existsSync(fullPath); } /** * Get file stats */ getFileStats(path: string): { size: number; modified: Date; isDirectory: boolean } | null { const fullPath = this.resolvePath(path); this.checkPathInWorkspace(fullPath); if (!existsSync(fullPath)) return null; const stats = statSync(fullPath); return { size: stats.size, modified: stats.mtime, isDirectory: stats.isDirectory() }; } // ============================================================================ // Permission & Security // ============================================================================ /** * Check if agent has a permission */ hasPermission(permission: Permission): boolean { return this.config.permissions.includes(permission); } /** * Check permission and throw if missing */ private checkPermission(permission: Permission): void { if (!this.hasPermission(permission)) { throw new Error(`Permission denied: ${permission}`); } } /** * Resolve path relative to workspace */ private resolvePath(path: string): string { return resolve(this.workspacePath, path); } /** * Check if path is within workspace */ private checkPathInWorkspace(fullPath: string): void { const relativePath = relative(this.workspacePath, fullPath); if (relativePath.startsWith('..') || relativePath.startsWith('/')) { throw new Error('Path is outside workspace boundaries'); } } /** * Check file size limit */ private checkFileSize(size: number): void { const maxBytes = this.config.resourceLimits.maxFileSizeMB * 1024 * 1024; if (size > maxBytes) { throw new Error(`File size exceeds limit: ${this.config.resourceLimits.maxFileSizeMB}MB`); } } // ============================================================================ // Lifecycle // ============================================================================ /** * Get workspace path */ getPath(): string { return this.workspacePath; } /** * Get workspace config */ getConfig(): WorkspaceConfig { return { ...this.config }; } /** * Clean up workspace */ cleanup(): void { this.active = false; this.clearSession(); this.emit('workspaceCleanup', { path: this.workspacePath }); } /** * Destroy workspace (delete files) */ destroy(): void { this.cleanup(); if (existsSync(this.workspacePath)) { rmSync(this.workspacePath, { recursive: true, force: true }); } this.emit('workspaceDestroyed', { path: this.workspacePath }); } /** * Export workspace state */ exportState(): { config: WorkspaceConfig; memory: Record; identity?: AgentIdentity; tools: string[]; } { return { config: this.getConfig(), memory: { shortTerm: Object.fromEntries(this.memory.shortTerm), longTerm: Object.fromEntries(this.memory.longTerm), session: Object.fromEntries(this.memory.session) }, identity: this.identity, tools: Array.from(this.tools.keys()) }; } } // ============================================================================ // Workspace Factory // ============================================================================ /** * WorkspaceFactory - Creates and manages workspaces */ export class WorkspaceFactory { private basePath: string; private workspaces: Map = new Map(); constructor(basePath: string = './workspaces') { this.basePath = resolve(basePath); if (!existsSync(this.basePath)) { mkdirSync(this.basePath, { recursive: true }); } } /** * Create a new workspace */ createWorkspace(config: { projectId: string; agentId: string; role: string; permissions?: Permission[]; resourceLimits?: Partial; }): WorkspaceManager { const id = `ws-${randomUUID().substring(0, 8)}`; const fullConfig: WorkspaceConfig = { id, projectId: config.projectId, agentId: config.agentId, role: config.role, basePath: this.basePath, permissions: config.permissions || ['read'], resourceLimits: { maxMemoryMB: 512, maxCpuPercent: 50, maxFileSizeMB: 10, maxExecutionTimeMs: 60000, maxFileCount: 1000, ...config.resourceLimits }, environment: {}, mountPoints: [] }; const workspace = new WorkspaceManager(fullConfig); this.workspaces.set(id, workspace); return workspace; } /** * Get a workspace by ID */ getWorkspace(id: string): WorkspaceManager | undefined { return this.workspaces.get(id); } /** * Get workspaces by project */ getWorkspacesByProject(projectId: string): WorkspaceManager[] { return Array.from(this.workspaces.values()) .filter(w => w.getConfig().projectId === projectId); } /** * Get all workspaces */ getAllWorkspaces(): WorkspaceManager[] { return Array.from(this.workspaces.values()); } /** * Destroy a workspace */ destroyWorkspace(id: string): boolean { const workspace = this.workspaces.get(id); if (workspace) { workspace.destroy(); return this.workspaces.delete(id); } return false; } /** * Destroy all workspaces for a project */ destroyProjectWorkspaces(projectId: string): number { const projectWorkspaces = this.getWorkspacesByProject(projectId); let count = 0; for (const workspace of projectWorkspaces) { workspace.destroy(); this.workspaces.delete(workspace.getConfig().id); count++; } return count; } /** * Get factory stats */ getStats(): { totalWorkspaces: number; byProject: Record; byRole: Record; } { const byProject: Record = {}; const byRole: Record = {}; for (const workspace of this.workspaces.values()) { const config = workspace.getConfig(); byProject[config.projectId] = (byProject[config.projectId] || 0) + 1; byRole[config.role] = (byRole[config.role] || 0) + 1; } return { totalWorkspaces: this.workspaces.size, byProject, byRole }; } } // Default factory instance export const defaultWorkspaceFactory = new WorkspaceFactory();