aiwg
Version:
Cognitive architecture for AI-augmented software development with structured memory, ensemble validation, and closed-loop correction. FAIR-aligned artifacts, 84% cost reduction via human-in-the-loop, standards adopted by 100+ organizations.
360 lines (308 loc) • 9.82 kB
text/typescript
import * as fs from 'fs/promises';
import * as path from 'path';
import * as os from 'os';
export interface WriteOptions {
encoding?: BufferEncoding;
mode?: number;
flag?: string;
}
export interface FileStats {
size: number;
isFile: boolean;
isDirectory: boolean;
createdAt: Date;
modifiedAt: Date;
}
/**
* FilesystemSandbox provides isolated temporary filesystems for testing.
*
* Creates temporary directory structures that can be safely manipulated
* without affecting the real filesystem. Automatically cleans up on destruction.
*
* @example
* ```typescript
* const sandbox = new FilesystemSandbox();
* await sandbox.initialize();
* await sandbox.writeFile('test.txt', 'Hello World');
* const content = await sandbox.readFile('test.txt');
* await sandbox.cleanup();
* ```
*/
export class FilesystemSandbox {
private sandboxPath: string = '';
private initialized: boolean = false;
constructor() {
// Sandbox path will be set during initialization
}
/**
* Initialize the sandbox by creating a temporary directory.
* Must be called before any other operations.
*/
async initialize(): Promise<void> {
if (this.initialized) {
throw new Error('Sandbox already initialized');
}
const prefix = path.join(os.tmpdir(), 'aiwg-sandbox-');
this.sandboxPath = await fs.mkdtemp(prefix);
this.initialized = true;
}
/**
* Clean up the sandbox by removing the temporary directory.
* Safe to call multiple times.
*/
async cleanup(): Promise<void> {
if (!this.sandboxPath || !this.initialized) {
return;
}
try {
// Ensure we're cleaning up a temp directory (safety check)
if (!this.sandboxPath.startsWith(os.tmpdir())) {
throw new Error('Refusing to cleanup: path is not in temp directory');
}
await fs.rm(this.sandboxPath, { recursive: true, force: true });
this.initialized = false;
this.sandboxPath = '';
} catch (error) {
// Log error but don't throw - cleanup should be best-effort
console.error('Error during sandbox cleanup:', error);
}
}
/**
* Write a file to the sandbox.
*
* @param relativePath - Path relative to sandbox root
* @param content - File content (string or Buffer)
* @param options - Write options (encoding, mode, flag)
*/
async writeFile(
relativePath: string,
content: string | Buffer,
options?: WriteOptions
): Promise<void> {
this.ensureInitialized();
this.validatePath(relativePath);
const fullPath = this.resolvePath(relativePath);
const dir = path.dirname(fullPath);
// Create directory if it doesn't exist
await fs.mkdir(dir, { recursive: true });
// Write file with options
await fs.writeFile(fullPath, content, options);
}
/**
* Read a file from the sandbox.
*
* @param relativePath - Path relative to sandbox root
* @param encoding - Optional encoding (if not provided, returns Buffer)
* @returns File content as string or Buffer
*/
async readFile(relativePath: string): Promise<string>;
async readFile(relativePath: string, encoding: BufferEncoding): Promise<string>;
async readFile(relativePath: string, encoding: null): Promise<Buffer>;
async readFile(
relativePath: string,
encoding?: BufferEncoding | null
): Promise<string | Buffer> {
this.ensureInitialized();
this.validatePath(relativePath);
const fullPath = this.resolvePath(relativePath);
if (encoding === null) {
return await fs.readFile(fullPath);
}
return await fs.readFile(fullPath, encoding || 'utf-8');
}
/**
* Delete a file from the sandbox.
*
* @param relativePath - Path relative to sandbox root
*/
async deleteFile(relativePath: string): Promise<void> {
this.ensureInitialized();
this.validatePath(relativePath);
const fullPath = this.resolvePath(relativePath);
await fs.unlink(fullPath);
}
/**
* Check if a file exists.
*
* @param relativePath - Path relative to sandbox root
* @returns True if file exists
*/
async fileExists(relativePath: string): Promise<boolean> {
this.ensureInitialized();
this.validatePath(relativePath);
const fullPath = this.resolvePath(relativePath);
try {
const stat = await fs.stat(fullPath);
return stat.isFile();
} catch (error: any) {
if (error.code === 'ENOENT') {
return false;
}
throw error;
}
}
/**
* Get file statistics.
*
* @param relativePath - Path relative to sandbox root
* @returns File statistics
*/
async getFileStats(relativePath: string): Promise<FileStats> {
this.ensureInitialized();
this.validatePath(relativePath);
const fullPath = this.resolvePath(relativePath);
const stat = await fs.stat(fullPath);
return {
size: stat.size,
isFile: stat.isFile(),
isDirectory: stat.isDirectory(),
createdAt: stat.birthtime,
modifiedAt: stat.mtime
};
}
/**
* Create a directory in the sandbox.
*
* @param relativePath - Path relative to sandbox root
*/
async createDirectory(relativePath: string): Promise<void> {
this.ensureInitialized();
this.validatePath(relativePath);
const fullPath = this.resolvePath(relativePath);
await fs.mkdir(fullPath, { recursive: true });
}
/**
* Delete a directory from the sandbox.
*
* @param relativePath - Path relative to sandbox root
* @param recursive - Whether to delete recursively (default: false)
*/
async deleteDirectory(relativePath: string, recursive: boolean = false): Promise<void> {
this.ensureInitialized();
this.validatePath(relativePath);
const fullPath = this.resolvePath(relativePath);
if (recursive) {
await fs.rm(fullPath, { recursive: true, force: true });
} else {
await fs.rmdir(fullPath);
}
}
/**
* List directory contents.
*
* @param relativePath - Path relative to sandbox root (default: root)
* @returns Array of file/directory names
*/
async listDirectory(relativePath: string = '.'): Promise<string[]> {
this.ensureInitialized();
this.validatePath(relativePath);
const fullPath = this.resolvePath(relativePath);
return await fs.readdir(fullPath);
}
/**
* Check if a directory exists.
*
* @param relativePath - Path relative to sandbox root
* @returns True if directory exists
*/
async directoryExists(relativePath: string): Promise<boolean> {
this.ensureInitialized();
this.validatePath(relativePath);
const fullPath = this.resolvePath(relativePath);
try {
const stat = await fs.stat(fullPath);
return stat.isDirectory();
} catch (error: any) {
if (error.code === 'ENOENT') {
return false;
}
throw error;
}
}
/**
* Get the absolute path to a file/directory in the sandbox.
*
* @param relativePath - Optional relative path (default: sandbox root)
* @returns Absolute path
*/
getPath(relativePath?: string): string {
this.ensureInitialized();
if (!relativePath) {
return this.sandboxPath;
}
this.validatePath(relativePath);
return this.resolvePath(relativePath);
}
/**
* Copy a file from the real filesystem into the sandbox.
*
* @param realPath - Path in the real filesystem
* @param sandboxPath - Target path in sandbox (relative)
*/
async copyFromReal(realPath: string, sandboxPath: string): Promise<void> {
this.ensureInitialized();
this.validatePath(sandboxPath);
const sourcePath = path.resolve(realPath);
const targetPath = this.resolvePath(sandboxPath);
const targetDir = path.dirname(targetPath);
// Create target directory if needed
await fs.mkdir(targetDir, { recursive: true });
// Copy file
await fs.copyFile(sourcePath, targetPath);
}
/**
* Copy a file from the sandbox to the real filesystem.
*
* @param sandboxPath - Source path in sandbox (relative)
* @param realPath - Target path in the real filesystem
*/
async copyToReal(sandboxPath: string, realPath: string): Promise<void> {
this.ensureInitialized();
this.validatePath(sandboxPath);
const sourcePath = this.resolvePath(sandboxPath);
const targetPath = path.resolve(realPath);
const targetDir = path.dirname(targetPath);
// Create target directory if needed
await fs.mkdir(targetDir, { recursive: true });
// Copy file
await fs.copyFile(sourcePath, targetPath);
}
/**
* Ensure the sandbox is initialized before operations.
*/
private ensureInitialized(): void {
if (!this.initialized || !this.sandboxPath) {
throw new Error('Sandbox not initialized. Call initialize() first.');
}
}
/**
* Validate that a path doesn't escape the sandbox.
*
* @param relativePath - Path to validate
* @throws Error if path is invalid or escapes sandbox
*/
private validatePath(relativePath: string): void {
// Reject absolute paths
if (path.isAbsolute(relativePath)) {
throw new Error('Absolute paths are not allowed in sandbox');
}
// Reject paths with null bytes
if (relativePath.includes('\0')) {
throw new Error('Path contains null bytes');
}
// Resolve the full path and ensure it's within sandbox
const fullPath = path.resolve(this.sandboxPath, relativePath);
if (!fullPath.startsWith(this.sandboxPath)) {
throw new Error('Path escapes sandbox directory');
}
}
/**
* Resolve a relative path to an absolute path within the sandbox.
*
* @param relativePath - Relative path
* @returns Absolute path
*/
private resolvePath(relativePath: string): string {
return path.join(this.sandboxPath, relativePath);
}
}