Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/config-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Comment on lines +137 to +139
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Default disables the safety rails for new installs; contradicts PR intent

getDefaultConfig() sets requireExplicitPermission to false. Combined with TerminalManager’s check (config.requireExplicitPermission !== false), fresh installs will have destructive-guarding OFF (because the default explicitly writes false). The PR description says the rails are “mandatory” and tests assume protection “enabled by default.” Set the default to true to make new configs safe by default.

Apply this diff:

-      requireExplicitPermission: false, // Default to false for backward compatibility
+      requireExplicitPermission: true, // Enabled by default for safety rails

Optionally, add a one-time migration in init(): if requireExplicitPermission is undefined in an existing config, set it to true to adopt the safe default without overriding explicit user choice.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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
readOnlyDirectories: [], // Empty by default - no directories are read-only
requireExplicitPermission: true, // Enabled by default for safety rails
allowedSudoCommands: [], // Empty array allows no sudo commands by default
🤖 Prompt for AI Agents
In src/config-manager.ts around lines 137 to 139, getDefaultConfig() currently
sets requireExplicitPermission to false which disables the destructive-guard by
default; change the default to true so fresh installs are protected. Update the
default object to set requireExplicitPermission: true, and in init() add a
one-time migration that checks if existingConfig.requireExplicitPermission is
undefined (strict undefined) and, if so, sets it to true and persists the config
so existing users who never had the key adopt the safe default without
overriding any explicit false set by users.

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)
Expand Down
61 changes: 58 additions & 3 deletions src/terminal-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,32 @@ import { configManager } from './config-manager.js';
import {capture} from "./utils/capture.js";
import { analyzeProcessState } from './utils/process-detection.js';

// The permission flag for destructive commands
const PERMISSION_FLAG = '--i-have-explicit-permission-from-user';

// Patterns for destructive commands
const DESTRUCTIVE_PATTERNS = [
/\brm\s+(-rf?|-fr?)\s+/, // rm -rf or rm -fr
/\brm\s+.*\*/, // rm with wildcards
/\bfind\s+.*-delete/, // find with -delete
/\bfind\s+.*-exec\s+rm/, // find with -exec rm
/\b(dd|mkfs|format|fdisk)\b/, // Disk operations
];
Comment on lines +12 to +18
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Destructive regexes are under-matching and can be bypassed

Current rm pattern only matches when -rf/-fr immediately follow rm. Commands like rm --flag -rf path or rm -r -f path won’t match; mixed/combined flags later in the command can bypass the guard. Strengthen patterns to detect -r and -f anywhere before operands (order-insensitive), and cover spaced flags.

Apply this diff to broaden coverage (case-insensitive and more flexible):

-const DESTRUCTIVE_PATTERNS = [
-    /\brm\s+(-rf?|-fr?)\s+/,       // rm -rf or rm -fr
-    /\brm\s+.*\*/,                  // rm with wildcards
-    /\bfind\s+.*-delete/,           // find with -delete
-    /\bfind\s+.*-exec\s+rm/,        // find with -exec rm
-    /\b(dd|mkfs|format|fdisk)\b/,   // Disk operations
-];
+const DESTRUCTIVE_PATTERNS = [
+    // rm where both -r and -f are present anywhere before operands (order-insensitive)
+    /\brm\b[^\n]*\s-(?:[^\s]*r[^\s]*f[^\s]*|[^\s]*f[^\s]*r[^\s]*)/i,
+    // rm with separated flags (e.g., -r ... -f or -f ... -r)
+    /\brm\b[^\n]*\s-r\b[^\n]*\s-f\b/i,
+    /\brm\b[^\n]*\s-f\b[^\n]*\s-r\b/i,
+    // rm with wildcards
+    /\brm\b[^\n]*\*/,
+    // find with -delete or -exec rm
+    /\bfind\b[^\n]*-delete\b/i,
+    /\bfind\b[^\n]*-exec\b[^\n]*\brm\b/i,
+    // Disk operations
+    /\b(dd|mkfs|format|fdisk)\b/i,
+];
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const DESTRUCTIVE_PATTERNS = [
/\brm\s+(-rf?|-fr?)\s+/, // rm -rf or rm -fr
/\brm\s+.*\*/, // rm with wildcards
/\bfind\s+.*-delete/, // find with -delete
/\bfind\s+.*-exec\s+rm/, // find with -exec rm
/\b(dd|mkfs|format|fdisk)\b/, // Disk operations
];
const DESTRUCTIVE_PATTERNS = [
// rm where both -r and -f are present anywhere before operands (order-insensitive)
/\brm\b[^\n]*\s-(?:[^\s]*r[^\s]*f[^\s]*|[^\s]*f[^\s]*r[^\s]*)/i,
// rm with separated flags (e.g., -r ... -f or -f ... -r)
/\brm\b[^\n]*\s-r\b[^\n]*\s-f\b/i,
/\brm\b[^\n]*\s-f\b[^\n]*\s-r\b/i,
// rm with wildcards
/\brm\b[^\n]*\*/,
// find with -delete or -exec rm
/\bfind\b[^\n]*-delete\b/i,
/\bfind\b[^\n]*-exec\b[^\n]*\brm\b/i,
// Disk operations
/\b(dd|mkfs|format|fdisk)\b/i,
];
🤖 Prompt for AI Agents
In src/terminal-manager.ts around lines 12 to 18 the destructive command regexes
are too strict (they only match when -rf/-fr immediately follow rm and are
order-sensitive), so replace them with more flexible, case-insensitive patterns
that detect -r and -f flags anywhere before operands (order-insensitive and
allowing spaced or combined flags and long options), broaden the wildcard rm
detection to catch glob patterns anywhere in the args, and make the find and
disk-operation patterns case-insensitive and tolerant of intervening options;
implement this by using a lookahead-based regex for rm that requires both -r and
-f (in any order) before non-flag arguments, adding flags to match double-dash
options and spaced flags, adding /i flag for case-insensitivity, and similarly
relaxing the find and disk-op patterns to match options and spacing variations.


/**
* Check if a command is destructive and needs explicit permission
*/
function isDestructiveCommand(command: string): boolean {
return DESTRUCTIVE_PATTERNS.some(pattern => pattern.test(command));
}

/**
* Check if command has permission flag
*/
function hasPermissionFlag(command: string): boolean {
return command.includes(PERMISSION_FLAG);
}

interface CompletedSession {
pid: number;
output: string;
Expand Down Expand Up @@ -44,6 +70,35 @@ export class TerminalManager {
}

async executeCommand(command: string, timeoutMs: number = DEFAULT_COMMAND_TIMEOUT, shell?: string): Promise<CommandExecutionResult> {
// Check for destructive commands if protection is enabled
try {
const config = await configManager.getConfig();
if (config.requireExplicitPermission !== false) { // Default to true if not set
if (isDestructiveCommand(command) && !hasPermissionFlag(command)) {
return {
pid: -1,
output: `🚨 DESTRUCTIVE OPERATION BLOCKED! 🚨

This command requires explicit permission.
To execute, you MUST:
1. Ask the user what specifically they want deleted
2. Show them what will be affected
3. Get explicit confirmation
4. Add flag: ${PERMISSION_FLAG}

Example: rm ${PERMISSION_FLAG} -rf /path/to/delete`,
isBlocked: false
};
}
Comment on lines 72 to +92
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Blocked-case sets isBlocked to false; flip to true

When the guard blocks execution, isBlocked should be true so callers/tests can rely on structured state.

-          return {
+          return {
             pid: -1,
             output: `🚨 DESTRUCTIVE OPERATION BLOCKED! 🚨
@@
-Example: rm ${PERMISSION_FLAG} -rf /path/to/delete`,
-            isBlocked: false
+Example: rm ${PERMISSION_FLAG} -rf /path/to/delete`,
+            isBlocked: true
           };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async executeCommand(command: string, timeoutMs: number = DEFAULT_COMMAND_TIMEOUT, shell?: string): Promise<CommandExecutionResult> {
// Check for destructive commands if protection is enabled
try {
const config = await configManager.getConfig();
if (config.requireExplicitPermission !== false) { // Default to true if not set
if (isDestructiveCommand(command) && !hasPermissionFlag(command)) {
return {
pid: -1,
output: `🚨 DESTRUCTIVE OPERATION BLOCKED! 🚨
This command requires explicit permission.
To execute, you MUST:
1. Ask the user what specifically they want deleted
2. Show them what will be affected
3. Get explicit confirmation
4. Add flag: ${PERMISSION_FLAG}
Example: rm ${PERMISSION_FLAG} -rf /path/to/delete`,
isBlocked: false
};
}
async executeCommand(command: string, timeoutMs: number = DEFAULT_COMMAND_TIMEOUT, shell?: string): Promise<CommandExecutionResult> {
// Check for destructive commands if protection is enabled
try {
const config = await configManager.getConfig();
if (config.requireExplicitPermission !== false) { // Default to true if not set
if (isDestructiveCommand(command) && !hasPermissionFlag(command)) {
return {
pid: -1,
output: `🚨 DESTRUCTIVE OPERATION BLOCKED! 🚨
This command requires explicit permission.
To execute, you MUST:
1. Ask the user what specifically they want deleted
2. Show them what will be affected
3. Get explicit confirmation
4. Add flag: ${PERMISSION_FLAG}
Example: rm ${PERMISSION_FLAG} -rf /path/to/delete`,
isBlocked: true
};
}
🤖 Prompt for AI Agents
In src/terminal-manager.ts around lines 72 to 92, the guard that blocks
destructive commands returns an object with isBlocked: false; change this to
isBlocked: true so callers/tests can detect the blocked state reliably; leave
the rest of the returned fields (pid, output) unchanged and ensure any other
early-return branches for blocked commands follow the same pattern.

}
} catch (error) {
console.error('Error checking destructive command protection:', error);
// Continue execution if config check fails
}

// Remove the permission flag before executing
Comment on lines 72 to +99
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Also respect blockedCommands via CommandManager before spawning

executeCommand currently doesn’t consult blockedCommands, which can bypass existing policy (see test/test-blocked-commands.js). Validate with CommandManager first.

   async executeCommand(command: string, timeoutMs: number = DEFAULT_COMMAND_TIMEOUT, shell?: string): Promise<CommandExecutionResult> {
+    // Enforce static blockedCommands first
+    try {
+      const { default: CommandManagerMod } = await import('./command-manager.js');
+      const commandManager = new CommandManagerMod.default?.constructor === Function
+        ? new CommandManagerMod.default()
+        : new CommandManagerMod.CommandManager?.constructor === Function
+          ? new CommandManagerMod.CommandManager()
+          : null;
+      if (commandManager && !(await commandManager.validateCommand(command))) {
+        return {
+          pid: -1,
+          output: 'Command blocked by policy (blockedCommands).',
+          isBlocked: true
+        };
+      }
+    } catch (e) {
+      // Non-fatal: if validation fails, continue to the destructive guard
+    }

If the module shape is stable (named export), prefer a direct import:

-import { spawn } from 'child_process';
+import { spawn } from 'child_process';
+import { CommandManager } from './command-manager.js';

And then instantiate/use it directly.

Committable suggestion skipped: line range outside the PR's diff.

const cleanCommand = command.replace(PERMISSION_FLAG, '').trim();

// Get the shell from config if not specified
let shellToUse: string | boolean | undefined = shell;
if (!shellToUse) {
Expand All @@ -60,9 +115,9 @@ export class TerminalManager {
// Note: No special stdio options needed here, Node.js handles pipes by default

// Enhance SSH commands automatically
let enhancedCommand = command;
if (command.trim().startsWith('ssh ') && !command.includes(' -t')) {
enhancedCommand = command.replace(/^ssh /, 'ssh -t ');
let enhancedCommand = cleanCommand;
if (cleanCommand.trim().startsWith('ssh ') && !cleanCommand.includes(' -t')) {
enhancedCommand = cleanCommand.replace(/^ssh /, 'ssh -t ');
console.log(`Enhanced SSH command: ${enhancedCommand}`);
}

Expand Down
53 changes: 47 additions & 6 deletions src/tools/filesystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,16 +214,47 @@ async function isPathAllowed(pathToCheck: string): Promise<boolean> {
return isAllowed;
}

/**
* Check if a path is within a read-only directory
* @param checkPath The path to check
* @returns Promise<boolean> True if the path is read-only
*/
async function isPathReadOnly(checkPath: string): Promise<boolean> {
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;
}

Comment on lines +217 to +245
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Read-only check is symlink-bypassable for write operations

isPathReadOnly compares string-normalized paths. For writes to non-existent targets via a symlinked parent (e.g., /tmp/link → /protected; writing /tmp/link/file.txt), fs.writeFile will traverse the symlink and write inside a protected directory. Since validatePath checks read-only before realpath resolution, this bypasses the guard. Use the parent directory’s realpath for write checks.

Apply this refactor:

 async function isPathReadOnly(checkPath: string): Promise<boolean> {
-    const config = await configManager.getConfig();
-    const readOnlyDirs = config.readOnlyDirectories || [];
+    const config = await configManager.getConfig();
+    const readOnlyDirs = config.readOnlyDirectories || [];
@@
-    const normalizedCheckPath = path.normalize(checkPath).toLowerCase();
+    const normalizedCheckPath = path.normalize(checkPath).toLowerCase();
@@
     for (const dir of readOnlyDirs) {
         const expandedDir = expandHome(dir);
         const normalizedDir = path.normalize(expandedDir).toLowerCase();
@@
     }
 
     return false;
 }

And in validatePath(), derive a canonical target for write checks by resolving the parent directory:

-        // Check if path is read-only for write operations
-        if (isWriteOperation && await isPathReadOnly(absolute)) {
+        // Check if path is read-only for write operations (resolve parent symlinks)
+        if (isWriteOperation) {
+            const parent = path.dirname(absolute);
+            let canonicalParent = parent;
+            try {
+                canonicalParent = await fs.realpath(parent);
+            } catch { /* parent may not exist; keep as-is */ }
+            const canonicalTarget = path.join(canonicalParent, path.basename(absolute));
+            if (await isPathReadOnly(canonicalTarget)) {
                 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.`);
+                throw new Error(`Path is read-only: ${requestedPath}. This directory is protected from modifications.`);
+            }
         }

This closes the symlink hole for create/append/rename into protected trees.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/tools/filesystem.ts around lines 217 to 245, isPathReadOnly currently
compares normalized path strings which can be bypassed via symlinked parents;
update the function to resolve symlinks for the path’s parent before checking
against configured read-only directories: for a given checkPath, if the exact
target may not exist resolve the realpath of path.dirname(checkPath) (using
fs.promises.realpath) and fall back to path.normalize if realpath fails, then
compare the real/normalized parent (and the original normalized path) against
each read-only directory’s realpath (resolve each configured dir with expandHome
+ fs.promises.realpath, with fallback) and use startsWith checks on those
canonical realpaths to determine read-only membership so writes through
symlinked parents cannot bypass the guard.

/**
* 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<string> 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<string> {
export async function validatePath(requestedPath: string, isWriteOperation: boolean = false): Promise<string> {
const validationOperation = async (): Promise<string> => {
// Expand home directory if present
const expandedPath = expandHome(requestedPath);
Expand All @@ -243,6 +274,16 @@ export async function validatePath(requestedPath: string): Promise<string> {
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);
Expand Down Expand Up @@ -828,7 +869,7 @@ function splitLinesPreservingEndings(content: string): string[] {
}

export async function writeFile(filePath: string, content: string, mode: 'rewrite' | 'append' = 'rewrite'): Promise<void> {
const validPath = await validatePath(filePath);
const validPath = await validatePath(filePath, true); // Mark as write operation

// Get file extension for telemetry
const fileExtension = getFileExtension(validPath);
Expand Down Expand Up @@ -886,7 +927,7 @@ export async function readMultipleFiles(paths: string[]): Promise<MultiFileResul
}

export async function createDirectory(dirPath: string): Promise<void> {
const validPath = await validatePath(dirPath);
const validPath = await validatePath(dirPath, true); // Creating directory is a write operation
await fs.mkdir(validPath, { recursive: true });
}

Expand All @@ -897,8 +938,8 @@ export async function listDirectory(dirPath: string): Promise<string[]> {
}

export async function moveFile(sourcePath: string, destinationPath: string): Promise<void> {
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);
}

Expand Down
61 changes: 61 additions & 0 deletions test-destructive.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#!/usr/bin/env node

// Test script for destructive command safety rails

import { TerminalManager } from './dist/terminal-manager.js';
import { configManager } from './dist/config-manager.js';

async function testDestructiveCommandProtection() {
console.log('Testing destructive command safety rails...\n');

const manager = new TerminalManager();

// Enable protection (it's enabled by default)
await configManager.init();
await configManager.setValue('requireExplicitPermission', true);

console.log('✅ Protection enabled\n');

// Test 1: Try rm -rf without permission flag
console.log('Test 1: rm -rf without permission flag');
const result1 = await manager.executeCommand('rm -rf /tmp/test-dir', 1000);
if (result1.output.includes('DESTRUCTIVE OPERATION BLOCKED')) {
console.log('✅ Command correctly blocked\n');
} else {
console.log('❌ ERROR: Command was not blocked!\n');
}

// Test 2: Try rm -rf WITH permission flag
console.log('Test 2: rm -rf WITH permission flag');
const result2 = await manager.executeCommand('rm --i-have-explicit-permission-from-user -rf /tmp/test-dir', 1000);
if (!result2.output.includes('DESTRUCTIVE OPERATION BLOCKED')) {
console.log('✅ Command allowed with permission flag\n');
} else {
console.log('❌ ERROR: Command was blocked even with flag!\n');
}

// Test 3: Try find with -delete
console.log('Test 3: find with -delete');
const result3 = await manager.executeCommand('find /tmp -name "*.log" -delete', 1000);
if (result3.output.includes('DESTRUCTIVE OPERATION BLOCKED')) {
console.log('✅ find -delete correctly blocked\n');
} else {
console.log('❌ ERROR: find -delete was not blocked!\n');
}

// Test 4: Normal commands should work
console.log('Test 4: Normal command (ls)');
const result4 = await manager.executeCommand('ls /tmp', 1000);
if (!result4.output.includes('DESTRUCTIVE OPERATION BLOCKED')) {
console.log('✅ Normal command allowed\n');
} else {
console.log('❌ ERROR: Normal command was blocked!\n');
}

console.log('✅ Destructive command protection test complete!');

// Clean up
manager.forceTerminate();
}

testDestructiveCommandProtection().catch(console.error);
38 changes: 38 additions & 0 deletions test-readonly.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/usr/bin/env node

// Test script for read-only directory protection

import { configManager } from './dist/config-manager.js';
import { writeFile } from './dist/tools/filesystem.js';

async function testReadOnlyProtection() {
console.log('Testing read-only directory protection...\n');

// Set up a read-only directory within allowed paths
await configManager.init();
await configManager.setValue('readOnlyDirectories', ['/home/konverts/projects2/test-readonly']);

console.log('✅ Configuration set: /home/konverts/projects2/test-readonly is now read-only\n');

// Try to write to a read-only directory
try {
console.log('Attempting to write to /home/konverts/projects2/test-readonly/test.txt...');
await writeFile('/home/konverts/projects2/test-readonly/test.txt', 'This should fail');
console.log('❌ ERROR: Write succeeded when it should have failed!');
} catch (error) {
console.log('✅ Write correctly blocked:', error.message);
}
Comment on lines +13 to +24
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Make the test portable; avoid hardcoded absolute paths

The test uses absolute Linux-specific paths (/home/konverts/...). This will fail on other machines/CI and on Windows. Use os.tmpdir() + path.join() to create ephemeral directories under a temp root.

-import { configManager } from './dist/config-manager.js';
-import { writeFile } from './dist/tools/filesystem.js';
+import { configManager } from './dist/config-manager.js';
+import { writeFile, createDirectory } from './dist/tools/filesystem.js';
+import os from 'os';
+import path from 'path';
@@
-    await configManager.setValue('readOnlyDirectories', ['/home/konverts/projects2/test-readonly']);
+    const TMP = os.tmpdir();
+    const RO_DIR = path.join(TMP, 'dcmd-readonly');
+    const OK_DIR = path.join(TMP, 'dcmd-allowed');
+    await createDirectory(RO_DIR);
+    await createDirectory(OK_DIR);
+    await configManager.setValue('readOnlyDirectories', [RO_DIR]);
@@
-        console.log('Attempting to write to /home/konverts/projects2/test-readonly/test.txt...');
-        await writeFile('/home/konverts/projects2/test-readonly/test.txt', 'This should fail');
+        const roFile = path.join(RO_DIR, 'test.txt');
+        console.log(`Attempting to write to ${roFile}...`);
+        await writeFile(roFile, 'This should fail');
@@
-        console.log('\nAttempting to write to /home/konverts/projects2/test-allowed/test.txt...');
-        await writeFile('/home/konverts/projects2/test-allowed/test.txt', 'This should work');
+        const okFile = path.join(OK_DIR, 'test.txt');
+        console.log(`\nAttempting to write to ${okFile}...`);
+        await writeFile(okFile, 'This should work');

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In test-readonly.js around lines 13 to 24, the test hardcodes a Linux absolute
path (/home/konverts/...), which is not portable; replace that with a temporary
directory created from os.tmpdir() and path.join (or fs.promises.mkdtemp with
path.join(os.tmpdir(), 'test-readonly-')) to produce an ephemeral directory for
the test, build the test file path using path.join(tempDir, 'test.txt'), update
the configManager.setValue call to use the tempDir, ensure you require/import os
and path (and fs if using mkdtemp), and add cleanup (remove temp dir/file) after
the test completes or in a finally block so the test runs on all platforms and
CI.


// Try to write to a non-protected directory
try {
console.log('\nAttempting to write to /home/konverts/projects2/test-allowed/test.txt...');
await writeFile('/home/konverts/projects2/test-allowed/test.txt', 'This should work');
console.log('✅ Write succeeded to non-protected directory');
} catch (error) {
console.log('❌ ERROR: Write failed:', error.message);
}
Comment on lines +26 to +33
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Create parent directories for the “allowed” write

writeFile doesn’t create parent directories. The current test can fail with ENOENT. Ensure the OK_DIR exists before writing (see diff above adding createDirectory(OK_DIR)).

🤖 Prompt for AI Agents
In test-readonly.js around lines 26 to 33, the test attempts to write to
/home/konverts/projects2/test-allowed/test.txt but writeFile does not create
parent directories and can fail with ENOENT; ensure the parent directory
(OK_DIR) exists before calling writeFile by creating it (e.g., call
createDirectory(OK_DIR) or use fs.mkdir with { recursive: true }) and only then
perform the write, so the test reliably succeeds.


console.log('\n✅ Read-only protection test complete!');
}

testReadOnlyProtection().catch(console.error);