feat(sandbox): grant default write access to ~/.gemini/tmp

Updates sandbox managers for macOS, Linux, and Windows to grant full
read/write access to the Gemini temporary directory (~/.gemini/tmp) by
default. This ensures that sandboxed tools can reliably use this
directory for temporary storage across all platforms.
This commit is contained in:
galz10
2026-04-03 11:45:48 -07:00
parent 3f12c1d7c7
commit 0b591f39dc
5 changed files with 41 additions and 15 deletions
@@ -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();
@@ -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';
@@ -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);
@@ -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).
@@ -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);