mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-04 00:44:05 -07:00
fix(core): resolve Plan Mode deadlock during plan file creation due to sandbox restrictions (#24047)
This commit is contained in:
@@ -17,24 +17,29 @@ describe('MacOsSandboxManager', () => {
|
||||
const mockNetworkAccess = true;
|
||||
|
||||
let mockPolicy: ExecutionPolicy;
|
||||
let manager: MacOsSandboxManager | undefined;
|
||||
let manager: MacOsSandboxManager;
|
||||
|
||||
beforeEach(() => {
|
||||
mockWorkspace = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), 'gemini-cli-macos-test-'),
|
||||
mockWorkspace = fs.realpathSync(
|
||||
fs.mkdtempSync(path.join(os.tmpdir(), 'gemini-cli-macos-test-')),
|
||||
);
|
||||
mockAllowedPaths = [
|
||||
path.join(os.tmpdir(), 'gemini-cli-macos-test-allowed'),
|
||||
];
|
||||
if (!fs.existsSync(mockAllowedPaths[0])) {
|
||||
fs.mkdirSync(mockAllowedPaths[0]);
|
||||
|
||||
const allowedPathTemp = path.join(
|
||||
os.tmpdir(),
|
||||
'gemini-cli-macos-test-allowed-' + Math.random().toString(36).slice(2),
|
||||
);
|
||||
if (!fs.existsSync(allowedPathTemp)) {
|
||||
fs.mkdirSync(allowedPathTemp);
|
||||
}
|
||||
mockAllowedPaths = [fs.realpathSync(allowedPathTemp)];
|
||||
|
||||
mockPolicy = {
|
||||
allowedPaths: mockAllowedPaths,
|
||||
networkAccess: mockNetworkAccess,
|
||||
};
|
||||
|
||||
manager = new MacOsSandboxManager({ workspace: mockWorkspace });
|
||||
|
||||
// Mock the seatbelt args builder to isolate manager tests
|
||||
vi.spyOn(seatbeltArgsBuilder, 'buildSeatbeltProfile').mockReturnValue(
|
||||
'(mock profile)',
|
||||
@@ -51,7 +56,6 @@ describe('MacOsSandboxManager', () => {
|
||||
|
||||
describe('prepareCommand', () => {
|
||||
it('should correctly format the base command and args', async () => {
|
||||
manager = new MacOsSandboxManager({ workspace: mockWorkspace });
|
||||
const result = await manager.prepareCommand({
|
||||
command: 'echo',
|
||||
args: ['hello'],
|
||||
@@ -64,8 +68,8 @@ describe('MacOsSandboxManager', () => {
|
||||
workspace: mockWorkspace,
|
||||
allowedPaths: mockAllowedPaths,
|
||||
forbiddenPaths: undefined,
|
||||
networkAccess: true,
|
||||
workspaceWrite: true,
|
||||
networkAccess: mockNetworkAccess,
|
||||
workspaceWrite: false,
|
||||
additionalPermissions: {
|
||||
fileSystem: {
|
||||
read: [],
|
||||
@@ -92,7 +96,6 @@ describe('MacOsSandboxManager', () => {
|
||||
});
|
||||
|
||||
it('should correctly pass through the cwd to the resulting command', async () => {
|
||||
manager = new MacOsSandboxManager({ workspace: mockWorkspace });
|
||||
const result = await manager.prepareCommand({
|
||||
command: 'echo',
|
||||
args: ['hello'],
|
||||
@@ -105,7 +108,6 @@ describe('MacOsSandboxManager', () => {
|
||||
});
|
||||
|
||||
it('should apply environment sanitization via the default mechanisms', async () => {
|
||||
manager = new MacOsSandboxManager({ workspace: mockWorkspace });
|
||||
const result = await manager.prepareCommand({
|
||||
command: 'echo',
|
||||
args: ['hello'],
|
||||
@@ -125,7 +127,6 @@ describe('MacOsSandboxManager', () => {
|
||||
});
|
||||
|
||||
it('should allow network when networkAccess is true', async () => {
|
||||
manager = new MacOsSandboxManager({ workspace: mockWorkspace });
|
||||
await manager.prepareCommand({
|
||||
command: 'echo',
|
||||
args: ['hello'],
|
||||
@@ -139,9 +140,43 @@ describe('MacOsSandboxManager', () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe('virtual commands', () => {
|
||||
it('should translate __read to /bin/cat', async () => {
|
||||
const testFile = path.join(mockWorkspace, 'file.txt');
|
||||
const result = await manager.prepareCommand({
|
||||
command: '__read',
|
||||
args: [testFile],
|
||||
cwd: mockWorkspace,
|
||||
env: {},
|
||||
policy: mockPolicy,
|
||||
});
|
||||
|
||||
expect(result.args[result.args.length - 2]).toBe('/bin/cat');
|
||||
expect(result.args[result.args.length - 1]).toBe(testFile);
|
||||
});
|
||||
|
||||
it('should translate __write to /bin/sh -c tee ...', async () => {
|
||||
const testFile = path.join(mockWorkspace, 'file.txt');
|
||||
const result = await manager.prepareCommand({
|
||||
command: '__write',
|
||||
args: [testFile],
|
||||
cwd: mockWorkspace,
|
||||
env: {},
|
||||
policy: mockPolicy,
|
||||
});
|
||||
|
||||
expect(result.args[result.args.length - 5]).toBe('/bin/sh');
|
||||
expect(result.args[result.args.length - 4]).toBe('-c');
|
||||
expect(result.args[result.args.length - 3]).toBe(
|
||||
'tee -- "$@" > /dev/null',
|
||||
);
|
||||
expect(result.args[result.args.length - 2]).toBe('_');
|
||||
expect(result.args[result.args.length - 1]).toBe(testFile);
|
||||
});
|
||||
});
|
||||
|
||||
describe('governance files', () => {
|
||||
it('should ensure governance files exist', async () => {
|
||||
manager = new MacOsSandboxManager({ workspace: mockWorkspace });
|
||||
await manager.prepareCommand({
|
||||
command: 'echo',
|
||||
args: [],
|
||||
@@ -160,7 +195,6 @@ describe('MacOsSandboxManager', () => {
|
||||
|
||||
describe('allowedPaths', () => {
|
||||
it('should parameterize allowed paths and normalize them', async () => {
|
||||
manager = new MacOsSandboxManager({ workspace: mockWorkspace });
|
||||
await manager.prepareCommand({
|
||||
command: 'echo',
|
||||
args: [],
|
||||
@@ -182,11 +216,11 @@ describe('MacOsSandboxManager', () => {
|
||||
|
||||
describe('forbiddenPaths', () => {
|
||||
it('should parameterize forbidden paths and explicitly deny them', async () => {
|
||||
manager = new MacOsSandboxManager({
|
||||
const customManager = new MacOsSandboxManager({
|
||||
workspace: mockWorkspace,
|
||||
forbiddenPaths: ['/tmp/forbidden1'],
|
||||
});
|
||||
await manager.prepareCommand({
|
||||
await customManager.prepareCommand({
|
||||
command: 'echo',
|
||||
args: [],
|
||||
cwd: mockWorkspace,
|
||||
@@ -202,11 +236,11 @@ describe('MacOsSandboxManager', () => {
|
||||
});
|
||||
|
||||
it('explicitly denies non-existent forbidden paths to prevent creation', async () => {
|
||||
manager = new MacOsSandboxManager({
|
||||
const customManager = new MacOsSandboxManager({
|
||||
workspace: mockWorkspace,
|
||||
forbiddenPaths: ['/tmp/does-not-exist'],
|
||||
});
|
||||
await manager.prepareCommand({
|
||||
await customManager.prepareCommand({
|
||||
command: 'echo',
|
||||
args: [],
|
||||
cwd: mockWorkspace,
|
||||
@@ -222,11 +256,11 @@ describe('MacOsSandboxManager', () => {
|
||||
});
|
||||
|
||||
it('should override allowed paths if a path is also in forbidden paths', async () => {
|
||||
manager = new MacOsSandboxManager({
|
||||
const customManager = new MacOsSandboxManager({
|
||||
workspace: mockWorkspace,
|
||||
forbiddenPaths: ['/tmp/conflict'],
|
||||
});
|
||||
await manager.prepareCommand({
|
||||
await customManager.prepareCommand({
|
||||
command: 'echo',
|
||||
args: [],
|
||||
cwd: mockWorkspace,
|
||||
|
||||
@@ -21,16 +21,16 @@ import {
|
||||
getSecureSanitizationConfig,
|
||||
} from '../../services/environmentSanitization.js';
|
||||
import { buildSeatbeltProfile } from './seatbeltArgsBuilder.js';
|
||||
import {
|
||||
initializeShellParsers,
|
||||
getCommandName,
|
||||
} from '../../utils/shell-utils.js';
|
||||
import { initializeShellParsers } from '../../utils/shell-utils.js';
|
||||
import {
|
||||
isKnownSafeCommand,
|
||||
isDangerousCommand,
|
||||
isStrictlyApproved,
|
||||
} from '../utils/commandSafety.js';
|
||||
import { verifySandboxOverrides } from '../utils/commandUtils.js';
|
||||
import {
|
||||
verifySandboxOverrides,
|
||||
getCommandName as getFullCommandName,
|
||||
isStrictlyApproved,
|
||||
} from '../utils/commandUtils.js';
|
||||
import { parsePosixSandboxDenials } from '../utils/sandboxDenialUtils.js';
|
||||
import { handleReadWriteCommands } from '../utils/sandboxReadWriteUtils.js';
|
||||
|
||||
@@ -68,11 +68,23 @@ export class MacOsSandboxManager implements SandboxManager {
|
||||
// Reject override attempts in plan mode
|
||||
verifySandboxOverrides(allowOverrides, req.policy);
|
||||
|
||||
let command = req.command;
|
||||
let args = req.args;
|
||||
|
||||
// Translate virtual commands for sandboxed file system access
|
||||
if (command === '__read') {
|
||||
command = '/bin/cat';
|
||||
} else if (command === '__write') {
|
||||
command = '/bin/sh';
|
||||
args = ['-c', 'cat > "$1"', '_', ...args];
|
||||
}
|
||||
|
||||
const currentReq = { ...req, command, args };
|
||||
|
||||
// If not in readonly mode OR it's a strictly approved pipeline, allow workspace writes
|
||||
const isApproved = allowOverrides
|
||||
? await isStrictlyApproved(
|
||||
req.command,
|
||||
req.args,
|
||||
currentReq,
|
||||
this.options.modeConfig?.approvedTools,
|
||||
)
|
||||
: false;
|
||||
@@ -82,7 +94,7 @@ export class MacOsSandboxManager implements SandboxManager {
|
||||
this.options.modeConfig?.network || req.policy?.networkAccess || false;
|
||||
|
||||
// Fetch persistent approvals for this command
|
||||
const commandName = await getCommandName(req.command, req.args);
|
||||
const commandName = await getFullCommandName(currentReq);
|
||||
const persistentPermissions = allowOverrides
|
||||
? this.options.policyManager?.getCommandPermissions(commandName)
|
||||
: undefined;
|
||||
@@ -115,7 +127,10 @@ export class MacOsSandboxManager implements SandboxManager {
|
||||
|
||||
const sandboxArgs = buildSeatbeltProfile({
|
||||
workspace: this.options.workspace,
|
||||
allowedPaths: [...(req.policy?.allowedPaths || [])],
|
||||
allowedPaths: [
|
||||
...(req.policy?.allowedPaths || []),
|
||||
...(this.options.includeDirectories || []),
|
||||
],
|
||||
forbiddenPaths: this.options.forbiddenPaths,
|
||||
networkAccess: mergedAdditional.network,
|
||||
workspaceWrite,
|
||||
|
||||
Reference in New Issue
Block a user