UNPKG

@synet/fs-ai

Version:

AI-safe filesystem with path protection, audit trails, and consciousness integration

220 lines (219 loc) 7.62 kB
/** * @synet/fs-ai - AI-Safe Filesystem Adapter * * Simple, stateless adapter that provides AI safety through: * - Home path for simplified AI navigation * - Path allowlist/blocklist enforcement * - Operation restrictions * - Path traversal protection * - Read-only mode support */ import { resolve, relative } from "node:path"; /** * Default AI safety configuration */ const DEFAULT_AI_CONFIG = { homePath: process.cwd(), // Default to current working directory allowedPaths: [], // Empty = allow all except forbidden forbiddenPaths: [ "/etc/", "/var/", "/usr/", "/sys/", "/proc/", "/bin/", "/sbin/", ], maxDepth: 10, allowedOperations: [ "readFile", "writeFile", "exists", "deleteFile", "ensureDir", "readDir", "deleteDir", "chmod", ], readOnly: false, }; /** * AI-Safe Filesystem Adapter * * Stateless adapter that wraps any IAsyncFileSystem with AI safety restrictions. * Can be composed with other filesystem adapters (caching, audit, etc.). */ export class AIFileSystem { constructor(baseFileSystem, config = {}) { this.baseFileSystem = baseFileSystem; // Merge configuration with proper array handling this.config = { homePath: config.homePath || DEFAULT_AI_CONFIG.homePath, allowedPaths: config.allowedPaths || DEFAULT_AI_CONFIG.allowedPaths, forbiddenPaths: [ ...DEFAULT_AI_CONFIG.forbiddenPaths, ...(config.forbiddenPaths || []), ], maxDepth: config.maxDepth || DEFAULT_AI_CONFIG.maxDepth, allowedOperations: config.allowedOperations || DEFAULT_AI_CONFIG.allowedOperations, readOnly: config.readOnly || DEFAULT_AI_CONFIG.readOnly, }; } // ========================================== // AI SAFETY VALIDATION // ========================================== /** * Resolve a path relative to the home directory */ resolveFromHome(path) { // If path is absolute, use it as-is if (path.startsWith("/")) { return resolve(path); } // Otherwise, resolve relative to homePath return resolve(this.config.homePath, path); } /** * Validate path is safe for AI access */ validatePath(path) { // Resolve the full path from home directory const resolvedPath = this.resolveFromHome(path); const homePath = resolve(this.config.homePath); // Check against forbidden paths first for (const forbidden of this.config.forbiddenPaths) { const resolvedForbidden = this.resolveFromHome(forbidden); if (resolvedPath.startsWith(resolvedForbidden)) { throw new Error("Path not allowed"); } } // Check against allowed paths (if specified) if (this.config.allowedPaths.length > 0) { const isAllowed = this.config.allowedPaths.some((allowed) => { const resolvedAllowed = this.resolveFromHome(allowed); return resolvedPath.startsWith(resolvedAllowed); }); if (!isAllowed) { throw new Error("Path not allowed"); } } else { // If no allowedPaths are specified, only check that relative paths stay within home // Absolute paths are allowed unless forbidden if (!path.startsWith("/")) { const relativePath = relative(homePath, resolvedPath); if (relativePath.startsWith("..")) { throw new Error("Path not allowed"); } } } // Check directory depth from home (only for relative paths) if (!path.startsWith("/")) { const relativePath = relative(homePath, resolvedPath); const depth = relativePath === "" ? 0 : relativePath.split("/").filter((segment) => segment.length > 0).length; if (depth > this.config.maxDepth) { throw new Error("Path exceeds maximum depth"); } } // Return the resolved path for the base filesystem return resolvedPath; } /** * Validate operation is allowed */ validateOperation(operation) { // Check read-only mode if (this.config.readOnly && !["readFile", "exists", "readDir"].includes(operation)) { throw new Error("Operation not allowed"); } // Check allowed operations if (!this.config.allowedOperations.includes(operation)) { throw new Error(`Operation not allowed: ${operation}`); } } // ========================================== // AI-SAFE FILESYSTEM OPERATIONS // ========================================== async readFile(path) { this.validateOperation("readFile"); const resolvedPath = this.validatePath(path); return this.baseFileSystem.readFile(resolvedPath); } async writeFile(path, content) { this.validateOperation("writeFile"); const resolvedPath = this.validatePath(path); return this.baseFileSystem.writeFile(resolvedPath, content); } async exists(path) { this.validateOperation("exists"); const resolvedPath = this.validatePath(path); return this.baseFileSystem.exists(resolvedPath); } async deleteFile(path) { this.validateOperation("deleteFile"); const resolvedPath = this.validatePath(path); return this.baseFileSystem.deleteFile(resolvedPath); } async createDir(path) { this.validateOperation("ensureDir"); const resolvedPath = this.validatePath(path); return this.baseFileSystem.ensureDir(resolvedPath); } async ensureDir(path) { this.validateOperation("ensureDir"); const resolvedPath = this.validatePath(path); return this.baseFileSystem.ensureDir(resolvedPath); } async deleteDir(path) { this.validateOperation("deleteDir"); const resolvedPath = this.validatePath(path); return this.baseFileSystem.deleteDir(resolvedPath); } async chmod(path, mode) { this.validateOperation("chmod"); const resolvedPath = this.validatePath(path); return this.baseFileSystem.chmod(resolvedPath, mode); } async readDir(path) { this.validateOperation("readDir"); const resolvedPath = this.validatePath(path); return this.baseFileSystem.readDir(resolvedPath); } // ========================================== // CONFIGURATION ACCESS // ========================================== /** * Get current AI safety configuration */ getSafetyConfig() { return { ...this.config }; } /** * Check if operation is allowed */ isOperationAllowed(operation) { if (this.config.readOnly && !["readFile", "exists", "readDir"].includes(operation)) { return false; } return this.config.allowedOperations.includes(operation); } /** * Check if path is allowed (without throwing) */ isPathAllowed(path) { try { this.validatePath(path); return true; } catch { return false; } } } /** * Factory function to create AI-safe filesystem */ export function createAIFileSystem(baseFileSystem, config) { return new AIFileSystem(baseFileSystem, config); }