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
@@ -212,8 +212,35 @@ export class WindowsSandboxManager implements SandboxManager {
// Reject override attempts in plan mode
verifySandboxOverrides(allowOverrides, req.policy);
let command = req.command;
let args = req.args;
let targetPathEnv: string | undefined;
// Translate virtual commands for sandboxed file system access
if (command === '__read') {
// Use PowerShell for safe argument passing via env var
targetPathEnv = args[0] || '';
command = 'PowerShell.exe';
args = [
'-NoProfile',
'-NonInteractive',
'-Command',
'& { Get-Content -LiteralPath $env:GEMINI_TARGET_PATH -Raw }',
];
} else if (command === '__write') {
// Use PowerShell for piping stdin to a file via env var
targetPathEnv = args[0] || '';
command = 'PowerShell.exe';
args = [
'-NoProfile',
'-NonInteractive',
'-Command',
'& { $Input | Out-File -FilePath $env:GEMINI_TARGET_PATH -Encoding utf8 }',
];
}
// Fetch persistent approvals for this command
const commandName = await getCommandName(req.command, req.args);
const commandName = await getCommandName(command, args);
const persistentPermissions = allowOverrides
? this.options.policyManager?.getCommandPermissions(commandName)
: undefined;
@@ -243,7 +270,7 @@ export class WindowsSandboxManager implements SandboxManager {
}
const defaultNetwork =
this.options.modeConfig?.network || req.policy?.networkAccess || false;
this.options.modeConfig?.network ?? req.policy?.networkAccess ?? false;
const networkAccess = defaultNetwork || mergedAdditional.network;
// 1. Handle filesystem permissions for Low Integrity
@@ -251,8 +278,8 @@ export class WindowsSandboxManager implements SandboxManager {
// If not in readonly mode OR it's a strictly approved pipeline, allow workspace writes
const isApproved = allowOverrides
? await isStrictlyApproved(
req.command,
req.args,
command,
args,
this.options.modeConfig?.approvedTools,
)
: false;
@@ -261,24 +288,48 @@ export class WindowsSandboxManager implements SandboxManager {
await this.grantLowIntegrityAccess(this.options.workspace);
}
// Grant "Low Mandatory Level" access to includeDirectories.
const includeDirs = sanitizePaths(this.options.includeDirectories) || [];
for (const includeDir of includeDirs) {
await this.grantLowIntegrityAccess(includeDir);
}
// Grant "Low Mandatory Level" read/write access to allowedPaths.
const allowedPaths = sanitizePaths(req.policy?.allowedPaths) || [];
for (const allowedPath of allowedPaths) {
await this.grantLowIntegrityAccess(allowedPath);
const resolved = await tryRealpath(allowedPath);
if (!fs.existsSync(resolved)) {
throw new Error(
`Sandbox request rejected: Allowed path does not exist: ${resolved}. ` +
'On Windows, granular sandbox access can only be granted to existing paths to avoid broad parent directory permissions.',
);
}
await this.grantLowIntegrityAccess(resolved);
}
// Grant "Low Mandatory Level" write access to additional permissions write paths.
const additionalWritePaths =
sanitizePaths(mergedAdditional.fileSystem?.write) || [];
for (const writePath of additionalWritePaths) {
await this.grantLowIntegrityAccess(writePath);
const resolved = await tryRealpath(writePath);
if (!fs.existsSync(resolved)) {
throw new Error(
`Sandbox request rejected: Additional write path does not exist: ${resolved}. ` +
'On Windows, granular sandbox access can only be granted to existing paths to avoid broad parent directory permissions.',
);
}
await this.grantLowIntegrityAccess(resolved);
}
// 2. Collect secret files and apply protective ACLs
// On Windows, we explicitly deny access to secret files for Low Integrity
// processes to ensure they cannot be read or written.
const secretsToBlock: string[] = [];
const searchDirs = new Set([this.options.workspace, ...allowedPaths]);
const searchDirs = new Set([
this.options.workspace,
...allowedPaths,
...includeDirs,
]);
for (const dir of searchDirs) {
try {
// We use maxDepth 3 to catch common nested secrets while keeping performance high.
@@ -352,19 +403,24 @@ export class WindowsSandboxManager implements SandboxManager {
// GeminiSandbox.exe <network:0|1> <cwd> --forbidden-manifest <path> <command> [args...]
const program = this.helperPath;
const args = [
const finalArgs = [
networkAccess ? '1' : '0',
req.cwd,
'--forbidden-manifest',
manifestPath,
req.command,
...req.args,
command,
...args,
];
const finalEnv = { ...sanitizedEnv };
if (targetPathEnv !== undefined) {
finalEnv['GEMINI_TARGET_PATH'] = targetPathEnv;
}
return {
program,
args,
env: sanitizedEnv,
args: finalArgs,
env: finalEnv,
cwd: req.cwd,
};
}