- Added Claude Code integration with full context compaction support - Added OpenClaw integration with deterministic pipeline support - Implemented parallel agent execution (4 projects x 3 roles pattern) - Added workspace isolation with permissions and quotas - Implemented Lobster-compatible YAML workflow parser - Added persistent memory store for cross-session context - Created comprehensive README with hero section This project was 100% autonomously built by Z.AI GLM-5
643 lines
16 KiB
TypeScript
643 lines
16 KiB
TypeScript
/**
|
|
* 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<string, string>;
|
|
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<ToolResult>;
|
|
}
|
|
|
|
export interface ToolContext {
|
|
workspace: WorkspaceManager;
|
|
agentId: string;
|
|
sessionId: string;
|
|
permissions: Permission[];
|
|
}
|
|
|
|
export interface ToolResult {
|
|
success: boolean;
|
|
output?: unknown;
|
|
error?: string;
|
|
metadata?: Record<string, unknown>;
|
|
}
|
|
|
|
export interface MemoryStore {
|
|
shortTerm: Map<string, unknown>;
|
|
longTerm: Map<string, unknown>;
|
|
session: Map<string, unknown>;
|
|
}
|
|
|
|
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<string, AgentTool> = new Map();
|
|
private fileHandles: Map<string, unknown> = 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<ToolResult> {
|
|
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<string, unknown> = {};
|
|
|
|
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<string, unknown>;
|
|
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<string, WorkspaceManager> = 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<ResourceLimits>;
|
|
}): 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<string, number>;
|
|
byRole: Record<string, number>;
|
|
} {
|
|
const byProject: Record<string, number> = {};
|
|
const byRole: Record<string, number> = {};
|
|
|
|
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();
|