| |
| |
| |
| |
| |
|
|
| import { spawn } from 'node:child_process'; |
| import { type FileSystemService } from './fileSystemService.js'; |
| import { type SandboxManager } from './sandboxManager.js'; |
| import { debugLogger } from '../utils/debugLogger.js'; |
| import { isNodeError } from '../utils/errors.js'; |
| import { resolveToRealPath, isSubpath } from '../utils/paths.js'; |
|
|
| |
| |
| |
| export class SandboxedFileSystemService implements FileSystemService { |
| constructor( |
| private sandboxManager: SandboxManager, |
| private cwd: string, |
| ) {} |
|
|
| private sanitizeAndValidatePath(filePath: string): string { |
| const resolvedPath = resolveToRealPath(filePath); |
| const workspace = resolveToRealPath(this.sandboxManager.getWorkspace()); |
|
|
| if (isSubpath(workspace, resolvedPath) || workspace === resolvedPath) { |
| return resolvedPath; |
| } |
|
|
| |
| const options = this.sandboxManager.getOptions(); |
| const allowedPaths = options?.includeDirectories ?? []; |
|
|
| for (const allowed of allowedPaths) { |
| const resolvedAllowed = resolveToRealPath(allowed); |
| if ( |
| isSubpath(resolvedAllowed, resolvedPath) || |
| resolvedAllowed === resolvedPath |
| ) { |
| return resolvedPath; |
| } |
| } |
|
|
| throw new Error( |
| `Access denied: Path '${filePath}' is outside the workspace and not in allowed paths.`, |
| ); |
| } |
|
|
| async readTextFile(filePath: string): Promise<string> { |
| const safePath = this.sanitizeAndValidatePath(filePath); |
| const prepared = await this.sandboxManager.prepareCommand({ |
| command: '__read', |
| args: [safePath], |
| cwd: this.cwd, |
| env: process.env, |
| policy: { |
| allowedPaths: [safePath], |
| }, |
| }); |
|
|
| try { |
| return await new Promise((resolve, reject) => { |
| |
|
|
| const child = spawn(prepared.program, prepared.args, { |
| cwd: this.cwd, |
| env: prepared.env, |
| }); |
|
|
| let output = ''; |
| let error = ''; |
|
|
| child.stdout?.on('data', (data) => { |
| output += data.toString(); |
| }); |
|
|
| child.stderr?.on('data', (data) => { |
| error += data.toString(); |
| }); |
|
|
| child.on('close', (code) => { |
| if (code === 0) { |
| resolve(output); |
| } else { |
| const isEnoent = |
| error.toLowerCase().includes('no such file or directory') || |
| error.toLowerCase().includes('enoent') || |
| error.toLowerCase().includes('could not find file') || |
| error.toLowerCase().includes('could not find a part of the path'); |
| const err = new Error( |
| `Sandbox Error: read_file failed for '${filePath}'. Exit code ${code}. ${error ? 'Details: ' + error : ''}`, |
| ); |
| if (isEnoent) { |
| Object.assign(err, { code: 'ENOENT' }); |
| } |
| reject(err); |
| } |
| }); |
|
|
| child.on('error', (err) => { |
| reject( |
| new Error( |
| `Sandbox Error: Failed to spawn read_file for '${filePath}': ${err.message}`, |
| ), |
| ); |
| }); |
| }); |
| } finally { |
| prepared.cleanup?.(); |
| } |
| } |
|
|
| async writeTextFile(filePath: string, content: string): Promise<void> { |
| const safePath = this.sanitizeAndValidatePath(filePath); |
| const prepared = await this.sandboxManager.prepareCommand({ |
| command: '__write', |
| args: [safePath], |
| cwd: this.cwd, |
| env: process.env, |
| policy: { |
| allowedPaths: [safePath], |
| additionalPermissions: { |
| fileSystem: { |
| write: [safePath], |
| }, |
| }, |
| }, |
| }); |
|
|
| try { |
| return await new Promise((resolve, reject) => { |
| |
|
|
| const child = spawn(prepared.program, prepared.args, { |
| cwd: this.cwd, |
| env: prepared.env, |
| }); |
|
|
| child.stdin?.on('error', (err) => { |
| |
| if (isNodeError(err) && err.code === 'EPIPE') { |
| return; |
| } |
| debugLogger.error( |
| `Sandbox Error: stdin error for '${filePath}': ${ |
| err instanceof Error ? err.message : String(err) |
| }`, |
| ); |
| }); |
|
|
| child.stdin?.write(content); |
| child.stdin?.end(); |
|
|
| let error = ''; |
| child.stderr?.on('data', (data) => { |
| error += data.toString(); |
| }); |
|
|
| child.on('close', (code) => { |
| if (code === 0) { |
| resolve(); |
| } else { |
| reject( |
| new Error( |
| `Sandbox Error: write_file failed for '${filePath}'. Exit code ${code}. ${error ? 'Details: ' + error : ''}`, |
| ), |
| ); |
| } |
| }); |
|
|
| child.on('error', (err) => { |
| reject( |
| new Error( |
| `Sandbox Error: Failed to spawn write_file for '${filePath}': ${err.message}`, |
| ), |
| ); |
| }); |
| }); |
| } finally { |
| prepared.cleanup?.(); |
| } |
| } |
| } |
|
|