fix(core): resolve Plan Mode deadlock during plan file creation due to sandbox restrictions (#24047)

This commit is contained in:
David Pierce
2026-03-31 22:06:50 +00:00
committed by GitHub
parent 9364dd8a49
commit 94f9480a3a
13 changed files with 555 additions and 97 deletions

View File

@@ -9,6 +9,7 @@ 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';
/**
* A FileSystemService implementation that performs operations through a sandbox.
@@ -19,12 +20,26 @@ export class SandboxedFileSystemService implements FileSystemService {
private cwd: string,
) {}
private sanitizeAndValidatePath(filePath: string): string {
const resolvedPath = resolveToRealPath(filePath);
if (!isSubpath(this.cwd, resolvedPath) && this.cwd !== resolvedPath) {
throw new Error(
`Access denied: Path '${filePath}' is outside the workspace.`,
);
}
return resolvedPath;
}
async readTextFile(filePath: string): Promise<string> {
const safePath = this.sanitizeAndValidatePath(filePath);
const prepared = await this.sandboxManager.prepareCommand({
command: '__read',
args: [filePath],
args: [safePath],
cwd: this.cwd,
env: process.env,
policy: {
allowedPaths: [safePath],
},
});
return new Promise((resolve, reject) => {
@@ -50,11 +65,18 @@ export class SandboxedFileSystemService implements FileSystemService {
if (code === 0) {
resolve(output);
} else {
reject(
new Error(
`Sandbox Error: read_file failed for '${filePath}'. Exit code ${code}. ${error ? 'Details: ' + error : ''}`,
),
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);
}
});
@@ -69,11 +91,20 @@ export class SandboxedFileSystemService implements FileSystemService {
}
async writeTextFile(filePath: string, content: string): Promise<void> {
const safePath = this.sanitizeAndValidatePath(filePath);
const prepared = await this.sandboxManager.prepareCommand({
command: '__write',
args: [filePath],
args: [safePath],
cwd: this.cwd,
env: process.env,
policy: {
allowedPaths: [safePath],
additionalPermissions: {
fileSystem: {
write: [safePath],
},
},
},
});
return new Promise((resolve, reject) => {