mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-03 16:34:31 -07:00
feat(core): populate sandbox forbidden paths with project ignore file contents (#24038)
This commit is contained in:
@@ -63,15 +63,12 @@ export interface SandboxModeConfig {
|
||||
* Global configuration options used to initialize a SandboxManager.
|
||||
*/
|
||||
export interface GlobalSandboxOptions {
|
||||
/**
|
||||
* The primary workspace path the sandbox is anchored to.
|
||||
* This directory is granted full read and write access.
|
||||
*/
|
||||
/** The absolute path to the primary workspace directory, granted full read/write access. */
|
||||
workspace: string;
|
||||
/** Absolute paths to explicitly include in the workspace context. */
|
||||
includeDirectories?: string[];
|
||||
/** Absolute paths to explicitly deny read/write access to (overrides allowlists). */
|
||||
forbiddenPaths?: string[];
|
||||
/** An optional asynchronous resolver function for paths that should be explicitly denied. */
|
||||
forbiddenPaths?: () => Promise<string[]>;
|
||||
/** The current sandbox mode behavior from config. */
|
||||
modeConfig?: SandboxModeConfig;
|
||||
/** The policy manager for persistent approvals. */
|
||||
@@ -298,29 +295,47 @@ export class LocalSandboxManager implements SandboxManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes an array of paths by deduplicating them and ensuring they are absolute.
|
||||
* Resolves sanitized allowed and forbidden paths for a request.
|
||||
* Filters the workspace from allowed paths and ensures forbidden paths take precedence.
|
||||
*/
|
||||
export function sanitizePaths(paths?: string[]): string[] | undefined {
|
||||
if (!paths) return undefined;
|
||||
export async function resolveSandboxPaths(
|
||||
options: GlobalSandboxOptions,
|
||||
req: SandboxRequest,
|
||||
): Promise<{
|
||||
allowed: string[];
|
||||
forbidden: string[];
|
||||
}> {
|
||||
const forbidden = sanitizePaths(await options.forbiddenPaths?.());
|
||||
const allowed = sanitizePaths(req.policy?.allowedPaths);
|
||||
|
||||
const workspaceIdentity = getPathIdentity(options.workspace);
|
||||
const forbiddenIdentities = new Set(forbidden.map(getPathIdentity));
|
||||
|
||||
const filteredAllowed = allowed.filter((p) => {
|
||||
const identity = getPathIdentity(p);
|
||||
return identity !== workspaceIdentity && !forbiddenIdentities.has(identity);
|
||||
});
|
||||
|
||||
return {
|
||||
allowed: filteredAllowed,
|
||||
forbidden,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes an array of paths by deduplicating them and ensuring they are absolute.
|
||||
* Always returns an array (empty if input is null/undefined).
|
||||
*/
|
||||
export function sanitizePaths(paths?: string[] | null): string[] {
|
||||
if (!paths || paths.length === 0) return [];
|
||||
|
||||
// We use a Map to deduplicate paths based on their normalized,
|
||||
// platform-specific identity e.g. handling case-insensitivity on Windows)
|
||||
// while preserving the original string casing.
|
||||
const uniquePathsMap = new Map<string, string>();
|
||||
for (const p of paths) {
|
||||
if (!path.isAbsolute(p)) {
|
||||
throw new Error(`Sandbox path must be absolute: ${p}`);
|
||||
}
|
||||
|
||||
// Normalize the path (resolves slashes and redundant components)
|
||||
let key = path.normalize(p);
|
||||
|
||||
// Windows file systems are case-insensitive, so we lowercase the key for
|
||||
// deduplication
|
||||
if (os.platform() === 'win32') {
|
||||
key = key.toLowerCase();
|
||||
}
|
||||
|
||||
const key = getPathIdentity(p);
|
||||
if (!uniquePathsMap.has(key)) {
|
||||
uniquePathsMap.set(key, p);
|
||||
}
|
||||
@@ -329,6 +344,20 @@ export function sanitizePaths(paths?: string[]): string[] | undefined {
|
||||
return Array.from(uniquePathsMap.values());
|
||||
}
|
||||
|
||||
/** Returns a normalized identity for a path, stripping trailing slashes and handling case sensitivity. */
|
||||
export function getPathIdentity(p: string): string {
|
||||
let norm = path.normalize(p);
|
||||
|
||||
// Strip trailing slashes (except for root paths)
|
||||
if (norm.length > 1 && (norm.endsWith('/') || norm.endsWith('\\'))) {
|
||||
norm = norm.slice(0, -1);
|
||||
}
|
||||
|
||||
const platform = os.platform();
|
||||
const isCaseInsensitive = platform === 'win32' || platform === 'darwin';
|
||||
return isCaseInsensitive ? norm.toLowerCase() : norm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves symlinks for a given path to prevent sandbox escapes.
|
||||
* If a file does not exist (ENOENT), it recursively resolves the parent directory.
|
||||
|
||||
Reference in New Issue
Block a user