mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-04 00:44:05 -07:00
feat(core): populate sandbox forbidden paths with project ignore file contents (#24038)
This commit is contained in:
@@ -67,7 +67,7 @@ describe('MacOsSandboxManager', () => {
|
||||
expect(seatbeltArgsBuilder.buildSeatbeltProfile).toHaveBeenCalledWith({
|
||||
workspace: mockWorkspace,
|
||||
allowedPaths: mockAllowedPaths,
|
||||
forbiddenPaths: undefined,
|
||||
forbiddenPaths: [],
|
||||
networkAccess: mockNetworkAccess,
|
||||
workspaceWrite: false,
|
||||
additionalPermissions: {
|
||||
@@ -218,7 +218,7 @@ describe('MacOsSandboxManager', () => {
|
||||
it('should parameterize forbidden paths and explicitly deny them', async () => {
|
||||
const customManager = new MacOsSandboxManager({
|
||||
workspace: mockWorkspace,
|
||||
forbiddenPaths: ['/tmp/forbidden1'],
|
||||
forbiddenPaths: async () => ['/tmp/forbidden1'],
|
||||
});
|
||||
await customManager.prepareCommand({
|
||||
command: 'echo',
|
||||
@@ -238,7 +238,7 @@ describe('MacOsSandboxManager', () => {
|
||||
it('explicitly denies non-existent forbidden paths to prevent creation', async () => {
|
||||
const customManager = new MacOsSandboxManager({
|
||||
workspace: mockWorkspace,
|
||||
forbiddenPaths: ['/tmp/does-not-exist'],
|
||||
forbiddenPaths: async () => ['/tmp/does-not-exist'],
|
||||
});
|
||||
await customManager.prepareCommand({
|
||||
command: 'echo',
|
||||
@@ -258,7 +258,7 @@ describe('MacOsSandboxManager', () => {
|
||||
it('should override allowed paths if a path is also in forbidden paths', async () => {
|
||||
const customManager = new MacOsSandboxManager({
|
||||
workspace: mockWorkspace,
|
||||
forbiddenPaths: ['/tmp/conflict'],
|
||||
forbiddenPaths: async () => ['/tmp/conflict'],
|
||||
});
|
||||
await customManager.prepareCommand({
|
||||
command: 'echo',
|
||||
@@ -273,7 +273,7 @@ describe('MacOsSandboxManager', () => {
|
||||
|
||||
expect(seatbeltArgsBuilder.buildSeatbeltProfile).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
allowedPaths: ['/tmp/conflict'],
|
||||
allowedPaths: [],
|
||||
forbiddenPaths: ['/tmp/conflict'],
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
type SandboxPermissions,
|
||||
type GlobalSandboxOptions,
|
||||
type ParsedSandboxDenial,
|
||||
resolveSandboxPaths,
|
||||
} from '../../services/sandboxManager.js';
|
||||
import type { ShellExecutionResult } from '../../services/shellExecutionService.js';
|
||||
import {
|
||||
@@ -93,6 +94,9 @@ export class MacOsSandboxManager implements SandboxManager {
|
||||
const defaultNetwork =
|
||||
this.options.modeConfig?.network || req.policy?.networkAccess || false;
|
||||
|
||||
const { allowed: allowedPaths, forbidden: forbiddenPaths } =
|
||||
await resolveSandboxPaths(this.options, req);
|
||||
|
||||
// Fetch persistent approvals for this command
|
||||
const commandName = await getFullCommandName(currentReq);
|
||||
const persistentPermissions = allowOverrides
|
||||
@@ -128,10 +132,10 @@ export class MacOsSandboxManager implements SandboxManager {
|
||||
const sandboxArgs = buildSeatbeltProfile({
|
||||
workspace: this.options.workspace,
|
||||
allowedPaths: [
|
||||
...(req.policy?.allowedPaths || []),
|
||||
...allowedPaths,
|
||||
...(this.options.includeDirectories || []),
|
||||
],
|
||||
forbiddenPaths: this.options.forbiddenPaths,
|
||||
forbiddenPaths,
|
||||
networkAccess: mergedAdditional.network,
|
||||
workspaceWrite,
|
||||
additionalPermissions: mergedAdditional,
|
||||
|
||||
@@ -38,6 +38,8 @@ describe('seatbeltArgsBuilder', () => {
|
||||
|
||||
const profile = buildSeatbeltProfile({
|
||||
workspace: '/Users/test/workspace',
|
||||
allowedPaths: [],
|
||||
forbiddenPaths: [],
|
||||
});
|
||||
|
||||
expect(profile).toContain('(version 1)');
|
||||
@@ -51,6 +53,8 @@ describe('seatbeltArgsBuilder', () => {
|
||||
vi.mocked(fsUtils.tryRealpath).mockImplementation((p) => p);
|
||||
const profile = buildSeatbeltProfile({
|
||||
workspace: '/test',
|
||||
allowedPaths: [],
|
||||
forbiddenPaths: [],
|
||||
networkAccess: true,
|
||||
});
|
||||
expect(profile).toContain('(allow network-outbound)');
|
||||
@@ -70,6 +74,8 @@ describe('seatbeltArgsBuilder', () => {
|
||||
|
||||
const profile = buildSeatbeltProfile({
|
||||
workspace: '/test/workspace',
|
||||
allowedPaths: [],
|
||||
forbiddenPaths: [],
|
||||
});
|
||||
|
||||
expect(profile).toContain(
|
||||
@@ -96,7 +102,11 @@ describe('seatbeltArgsBuilder', () => {
|
||||
}) as unknown as fs.Stats,
|
||||
);
|
||||
|
||||
const profile = buildSeatbeltProfile({ workspace: '/test/workspace' });
|
||||
const profile = buildSeatbeltProfile({
|
||||
workspace: '/test/workspace',
|
||||
allowedPaths: [],
|
||||
forbiddenPaths: [],
|
||||
});
|
||||
|
||||
expect(profile).toContain(
|
||||
`(deny file-write* (literal "/test/workspace/.gitignore"))`,
|
||||
@@ -117,6 +127,7 @@ describe('seatbeltArgsBuilder', () => {
|
||||
const profile = buildSeatbeltProfile({
|
||||
workspace: '/test',
|
||||
allowedPaths: ['/custom/path1', '/test/symlink'],
|
||||
forbiddenPaths: [],
|
||||
});
|
||||
|
||||
expect(profile).toContain(`(subpath "/custom/path1")`);
|
||||
@@ -130,6 +141,7 @@ describe('seatbeltArgsBuilder', () => {
|
||||
|
||||
const profile = buildSeatbeltProfile({
|
||||
workspace: '/test',
|
||||
allowedPaths: [],
|
||||
forbiddenPaths: ['/secret/path'],
|
||||
});
|
||||
|
||||
@@ -148,6 +160,7 @@ describe('seatbeltArgsBuilder', () => {
|
||||
|
||||
const profile = buildSeatbeltProfile({
|
||||
workspace: '/test',
|
||||
allowedPaths: [],
|
||||
forbiddenPaths: ['/test/symlink'],
|
||||
});
|
||||
|
||||
@@ -161,6 +174,7 @@ describe('seatbeltArgsBuilder', () => {
|
||||
|
||||
const profile = buildSeatbeltProfile({
|
||||
workspace: '/test',
|
||||
allowedPaths: [],
|
||||
forbiddenPaths: ['/test/missing-dir/missing-file.txt'],
|
||||
});
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
} from './baseProfile.js';
|
||||
import {
|
||||
type SandboxPermissions,
|
||||
sanitizePaths,
|
||||
GOVERNANCE_FILES,
|
||||
SECRET_FILES,
|
||||
} from '../../services/sandboxManager.js';
|
||||
@@ -26,9 +25,9 @@ export interface SeatbeltArgsOptions {
|
||||
/** The primary workspace path to allow access to. */
|
||||
workspace: string;
|
||||
/** Additional paths to allow access to. */
|
||||
allowedPaths?: string[];
|
||||
allowedPaths: string[];
|
||||
/** Absolute paths to explicitly deny read/write access to (overrides allowlists). */
|
||||
forbiddenPaths?: string[];
|
||||
forbiddenPaths: string[];
|
||||
/** Whether to allow network access. */
|
||||
networkAccess?: boolean;
|
||||
/** Granular additional permissions. */
|
||||
@@ -92,10 +91,7 @@ export function buildSeatbeltProfile(options: SeatbeltArgsOptions): string {
|
||||
// Add explicit deny rules for secret files (.env, .env.*) in the workspace and allowed paths.
|
||||
// We use regex rules to avoid expensive file discovery scans.
|
||||
// Anchoring to workspace/allowed paths to avoid over-blocking.
|
||||
const searchPaths = sanitizePaths([
|
||||
options.workspace,
|
||||
...(options.allowedPaths || []),
|
||||
]) || [options.workspace];
|
||||
const searchPaths = [options.workspace, ...options.allowedPaths];
|
||||
|
||||
for (const basePath of searchPaths) {
|
||||
const resolvedBase = tryRealpath(basePath);
|
||||
@@ -159,7 +155,7 @@ export function buildSeatbeltProfile(options: SeatbeltArgsOptions): string {
|
||||
}
|
||||
|
||||
// Handle allowedPaths
|
||||
const allowedPaths = sanitizePaths(options.allowedPaths) || [];
|
||||
const allowedPaths = options.allowedPaths;
|
||||
for (let i = 0; i < allowedPaths.length; i++) {
|
||||
const allowedPath = tryRealpath(allowedPaths[i]);
|
||||
profile += `(allow file-read* file-write* (subpath "${escapeSchemeString(allowedPath)}"))\n`;
|
||||
@@ -203,7 +199,7 @@ export function buildSeatbeltProfile(options: SeatbeltArgsOptions): string {
|
||||
}
|
||||
|
||||
// Handle forbiddenPaths
|
||||
const forbiddenPaths = sanitizePaths(options.forbiddenPaths) || [];
|
||||
const forbiddenPaths = options.forbiddenPaths;
|
||||
for (let i = 0; i < forbiddenPaths.length; i++) {
|
||||
const forbiddenPath = tryRealpath(forbiddenPaths[i]);
|
||||
profile += `(deny file-read* file-write* (subpath "${escapeSchemeString(forbiddenPath)}"))\n`;
|
||||
|
||||
Reference in New Issue
Block a user