diff --git a/src/config-manager.ts b/src/config-manager.ts index fabac91e..dc065f6b 100644 --- a/src/config-manager.ts +++ b/src/config-manager.ts @@ -10,6 +10,9 @@ export interface ServerConfig { blockedCommands?: string[]; defaultShell?: string; allowedDirectories?: string[]; + readOnlyDirectories?: string[]; // Directories that can be read but not modified + requireExplicitPermission?: boolean; // Require explicit flag for destructive commands + allowedSudoCommands?: string[]; // Whitelist of allowed sudo commands with pattern support telemetryEnabled?: boolean; // New field for telemetry control fileWriteLineLimit?: number; // Line limit for file write operations fileReadLineLimit?: number; // Default line limit for file read operations (changed from character-based) @@ -131,6 +134,9 @@ class ConfigManager { ], defaultShell: os.platform() === 'win32' ? 'powershell.exe' : '/bin/sh', allowedDirectories: [], + readOnlyDirectories: [], // Empty by default - no directories are read-only + requireExplicitPermission: false, // Default to false for backward compatibility + allowedSudoCommands: [], // Empty array allows no sudo commands by default telemetryEnabled: true, // Default to opt-out approach (telemetry on by default) fileWriteLineLimit: 50, // Default line limit for file write operations (changed from 100) fileReadLineLimit: 1000 // Default line limit for file read operations (changed from character-based) diff --git a/src/tools/filesystem.ts b/src/tools/filesystem.ts index 5f36c0ca..4e3373d4 100644 --- a/src/tools/filesystem.ts +++ b/src/tools/filesystem.ts @@ -214,16 +214,47 @@ async function isPathAllowed(pathToCheck: string): Promise { return isAllowed; } +/** + * Check if a path is within a read-only directory + * @param checkPath The path to check + * @returns Promise True if the path is read-only + */ +async function isPathReadOnly(checkPath: string): Promise { + const config = await configManager.getConfig(); + const readOnlyDirs = config.readOnlyDirectories || []; + + if (readOnlyDirs.length === 0) { + return false; // No read-only directories configured + } + + const normalizedCheckPath = path.normalize(checkPath).toLowerCase(); + + for (const dir of readOnlyDirs) { + const expandedDir = expandHome(dir); + const normalizedDir = path.normalize(expandedDir).toLowerCase(); + + // Check if the path is within the read-only directory + if (normalizedCheckPath === normalizedDir || + normalizedCheckPath.startsWith(normalizedDir + path.sep)) { + return true; + } + } + + return false; +} + /** * Validates a path to ensure it can be accessed or created. * For existing paths, returns the real path (resolving symlinks). * For non-existent paths, validates parent directories to ensure they exist. + * For write operations, also checks if the path is read-only. * * @param requestedPath The path to validate + * @param isWriteOperation Whether this is a write operation (default: false) * @returns Promise The validated path - * @throws Error if the path or its parent directories don't exist or if the path is not allowed + * @throws Error if the path or its parent directories don't exist or if the path is not allowed or read-only */ -export async function validatePath(requestedPath: string): Promise { +export async function validatePath(requestedPath: string, isWriteOperation: boolean = false): Promise { const validationOperation = async (): Promise => { // Expand home directory if present const expandedPath = expandHome(requestedPath); @@ -243,6 +274,16 @@ export async function validatePath(requestedPath: string): Promise { throw new Error(`Path not allowed: ${requestedPath}. Must be within one of these directories: ${(await getAllowedDirs()).join(', ')}`); } + // Check if path is read-only for write operations + if (isWriteOperation && await isPathReadOnly(absolute)) { + capture('server_path_validation_error', { + error: 'Path is read-only', + operation: 'write' + }); + + throw new Error(`Path is read-only: ${requestedPath}. This directory is protected from modifications.`); + } + // Check if path exists try { const stats = await fs.stat(absolute); @@ -828,7 +869,7 @@ function splitLinesPreservingEndings(content: string): string[] { } export async function writeFile(filePath: string, content: string, mode: 'rewrite' | 'append' = 'rewrite'): Promise { - const validPath = await validatePath(filePath); + const validPath = await validatePath(filePath, true); // Mark as write operation // Get file extension for telemetry const fileExtension = getFileExtension(validPath); @@ -886,7 +927,7 @@ export async function readMultipleFiles(paths: string[]): Promise { - const validPath = await validatePath(dirPath); + const validPath = await validatePath(dirPath, true); // Creating directory is a write operation await fs.mkdir(validPath, { recursive: true }); } @@ -897,8 +938,8 @@ export async function listDirectory(dirPath: string): Promise { } export async function moveFile(sourcePath: string, destinationPath: string): Promise { - const validSourcePath = await validatePath(sourcePath); - const validDestPath = await validatePath(destinationPath); + const validSourcePath = await validatePath(sourcePath, true); // Source needs write permission (to delete) + const validDestPath = await validatePath(destinationPath, true); // Destination needs write permission await fs.rename(validSourcePath, validDestPath); }