diff --git a/packages/core/src/sandbox/linux/LinuxSandboxManager.ts b/packages/core/src/sandbox/linux/LinuxSandboxManager.ts index d91ab1a836..989680dbc2 100644 --- a/packages/core/src/sandbox/linux/LinuxSandboxManager.ts +++ b/packages/core/src/sandbox/linux/LinuxSandboxManager.ts @@ -256,6 +256,7 @@ export class LinuxSandboxManager implements SandboxManager { includeDirectories: this.options.includeDirectories || [], maskFilePath: this.getMaskFilePath(), isWriteCommand: req.command === '__write', + geminiTmpPath: join(os.homedir(), '.gemini', 'tmp'), }); const bpfPath = getSeccompBpfPath(); diff --git a/packages/core/src/sandbox/linux/bwrapArgsBuilder.ts b/packages/core/src/sandbox/linux/bwrapArgsBuilder.ts index e5e6ebf014..96053d23ff 100644 --- a/packages/core/src/sandbox/linux/bwrapArgsBuilder.ts +++ b/packages/core/src/sandbox/linux/bwrapArgsBuilder.ts @@ -33,6 +33,7 @@ export interface BwrapArgsOptions { includeDirectories: string[]; maskFilePath: string; isWriteCommand: boolean; + geminiTmpPath?: string; } /** @@ -63,6 +64,15 @@ export async function buildBwrapArgs( '/tmp', ); + // Allow read/write access to the Gemini temporary directory if provided + if (options.geminiTmpPath) { + const geminiTmp = tryRealpath(options.geminiTmpPath); + bwrapArgs.push('--bind-try', options.geminiTmpPath, options.geminiTmpPath); + if (geminiTmp !== options.geminiTmpPath) { + bwrapArgs.push('--bind-try', geminiTmp, geminiTmp); + } + } + const workspacePath = tryRealpath(options.workspace); const bindFlag = options.workspaceWrite ? '--bind-try' : '--ro-bind-try'; diff --git a/packages/core/src/sandbox/macos/MacOsSandboxManager.ts b/packages/core/src/sandbox/macos/MacOsSandboxManager.ts index 497bf30c31..4feafb6bd6 100644 --- a/packages/core/src/sandbox/macos/MacOsSandboxManager.ts +++ b/packages/core/src/sandbox/macos/MacOsSandboxManager.ts @@ -144,6 +144,7 @@ export class MacOsSandboxManager implements SandboxManager { networkAccess: mergedAdditional.network, workspaceWrite, additionalPermissions: mergedAdditional, + geminiTmpPath: path.join(os.homedir(), '.gemini', 'tmp'), }); const tempFile = this.writeProfileToTempFile(sandboxArgs); diff --git a/packages/core/src/sandbox/macos/seatbeltArgsBuilder.ts b/packages/core/src/sandbox/macos/seatbeltArgsBuilder.ts index e5430d1471..2a301637c8 100644 --- a/packages/core/src/sandbox/macos/seatbeltArgsBuilder.ts +++ b/packages/core/src/sandbox/macos/seatbeltArgsBuilder.ts @@ -34,6 +34,8 @@ export interface SeatbeltArgsOptions { additionalPermissions?: SandboxPermissions; /** Whether to allow write access to the workspace. */ workspaceWrite?: boolean; + /** The path to the Gemini temporary directory (~/.gemini/tmp). */ + geminiTmpPath?: string; } /** @@ -61,6 +63,15 @@ export function buildSeatbeltProfile(options: SeatbeltArgsOptions): string { const tmpPath = tryRealpath(os.tmpdir()); profile += `(allow file-read* file-write* (subpath "${escapeSchemeString(tmpPath)}"))\n`; + // Allow read/write access to the Gemini temporary directory if provided + if (options.geminiTmpPath) { + const geminiTmp = tryRealpath(options.geminiTmpPath); + profile += `(allow file-read* file-write* (subpath "${escapeSchemeString(options.geminiTmpPath)}"))\n`; + if (geminiTmp !== options.geminiTmpPath) { + profile += `(allow file-read* file-write* (subpath "${escapeSchemeString(geminiTmp)}"))\n`; + } + } + // Add explicit deny rules for governance files in the workspace. // These are added after the workspace allow rule to ensure they take precedence // (Seatbelt evaluates rules in order, later rules win for same path). diff --git a/packages/core/src/sandbox/windows/WindowsSandboxManager.ts b/packages/core/src/sandbox/windows/WindowsSandboxManager.ts index 3328c2b918..62ee8f4c17 100644 --- a/packages/core/src/sandbox/windows/WindowsSandboxManager.ts +++ b/packages/core/src/sandbox/windows/WindowsSandboxManager.ts @@ -222,7 +222,25 @@ export class WindowsSandboxManager implements SandboxManager { // Native commands __read and __write are passed directly to GeminiSandbox.exe + const isApproved = allowOverrides + ? await isStrictlyApproved( + command, + args, + this.options.modeConfig?.approvedTools, + ) + : false; + const isYolo = this.options.modeConfig?.yolo ?? false; + const workspaceWrite = !isReadonlyMode || isApproved || isYolo; + + if (workspaceWrite) { + await this.grantLowIntegrityAccess(this.options.workspace); + } + + // Grant write access to the Gemini temporary directory + await this.grantLowIntegrityAccess( + path.join(os.homedir(), '.gemini', 'tmp'), + ); // Fetch persistent approvals for this command const commandName = await getCommandName(command, args); @@ -259,21 +277,6 @@ export class WindowsSandboxManager implements SandboxManager { this.options.modeConfig?.network ?? req.policy?.networkAccess ?? false; const networkAccess = defaultNetwork || mergedAdditional.network; - // 1. Handle filesystem permissions for Low Integrity - // Grant "Low Mandatory Level" write access to the workspace. - // If not in readonly mode OR it's a strictly approved pipeline, allow workspace writes - const isApproved = allowOverrides - ? await isStrictlyApproved( - command, - args, - this.options.modeConfig?.approvedTools, - ) - : false; - - if (!isReadonlyMode || isApproved) { - await this.grantLowIntegrityAccess(this.options.workspace); - } - const { allowed: allowedPaths, forbidden: forbiddenPaths } = await resolveSandboxPaths(this.options, req);