mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-30 16:00:41 -07:00
feat(core): implement windows sandbox expansion and denial detection (#24027)
This commit is contained in:
committed by
GitHub
parent
4034c030e7
commit
9e74a7ec18
@@ -34,6 +34,7 @@ import {
|
||||
isStrictlyApproved,
|
||||
} from './commandSafety.js';
|
||||
import { verifySandboxOverrides } from '../utils/commandUtils.js';
|
||||
import { parseWindowsSandboxDenials } from './windowsSandboxDenialUtils.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
@@ -66,8 +67,8 @@ export class WindowsSandboxManager implements SandboxManager {
|
||||
return isDangerousCommand(args);
|
||||
}
|
||||
|
||||
parseDenials(_result: ShellExecutionResult): ParsedSandboxDenial | undefined {
|
||||
return undefined; // TODO: Implement Windows-specific denial parsing
|
||||
parseDenials(result: ShellExecutionResult): ParsedSandboxDenial | undefined {
|
||||
return parseWindowsSandboxDenials(result);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { parseWindowsSandboxDenials } from './windowsSandboxDenialUtils.js';
|
||||
import type { ShellExecutionResult } from '../../services/shellExecutionService.js';
|
||||
|
||||
describe('parseWindowsSandboxDenials', () => {
|
||||
it('should detect CMD "Access is denied" and extract paths', () => {
|
||||
const parsed = parseWindowsSandboxDenials({
|
||||
output: 'Access is denied.\r\n',
|
||||
error: new Error('Command failed: dir C:\\Windows\\System32\\config'),
|
||||
} as unknown as ShellExecutionResult);
|
||||
|
||||
expect(parsed).toBeDefined();
|
||||
expect(parsed?.filePaths).toContain('C:\\Windows\\System32\\config');
|
||||
});
|
||||
|
||||
it('should detect PowerShell "Access to the path is denied"', () => {
|
||||
const parsed = parseWindowsSandboxDenials({
|
||||
output:
|
||||
"Set-Content : Access to the path 'C:\\test.txt' is denied.\r\nAt line:1 char:1\r\n",
|
||||
} as unknown as ShellExecutionResult);
|
||||
|
||||
expect(parsed).toBeDefined();
|
||||
expect(parsed?.filePaths).toContain('C:\\test.txt');
|
||||
});
|
||||
|
||||
it('should detect Node.js EPERM on Windows', () => {
|
||||
const parsed = parseWindowsSandboxDenials({
|
||||
error: {
|
||||
message:
|
||||
"Error: EPERM: operation not permitted, open 'D:\\project\\file.ts'",
|
||||
},
|
||||
} as unknown as ShellExecutionResult);
|
||||
|
||||
expect(parsed).toBeDefined();
|
||||
expect(parsed?.filePaths).toContain('D:\\project\\file.ts');
|
||||
});
|
||||
|
||||
it('should detect network denial (EACCES)', () => {
|
||||
const parsed = parseWindowsSandboxDenials({
|
||||
output: 'Error: listen EACCES: permission denied 0.0.0.0:3000',
|
||||
} as unknown as ShellExecutionResult);
|
||||
|
||||
expect(parsed).toBeDefined();
|
||||
expect(parsed?.network).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect native Windows error code 0x80070005', () => {
|
||||
const parsed = parseWindowsSandboxDenials({
|
||||
output: 'HRESULT: 0x80070005',
|
||||
} as unknown as ShellExecutionResult);
|
||||
|
||||
expect(parsed).toBeDefined();
|
||||
// No path in output, but recognized as denial
|
||||
});
|
||||
|
||||
it('should handle extended-length paths', () => {
|
||||
const parsed = parseWindowsSandboxDenials({
|
||||
output: 'Access is denied to \\\\?\\C:\\Very\\Long\\Path\\file.txt',
|
||||
} as unknown as ShellExecutionResult);
|
||||
|
||||
expect(parsed).toBeDefined();
|
||||
expect(parsed?.filePaths).toContain(
|
||||
'\\\\?\\C:\\Very\\Long\\Path\\file.txt',
|
||||
);
|
||||
});
|
||||
|
||||
it('should detect Windows paths with forward slashes', () => {
|
||||
const parsed = parseWindowsSandboxDenials({
|
||||
output:
|
||||
"Error: EPERM: operation not permitted, open 'C:/project/file.ts'",
|
||||
} as unknown as ShellExecutionResult);
|
||||
|
||||
expect(parsed).toBeDefined();
|
||||
expect(parsed?.filePaths).toContain('C:/project/file.ts');
|
||||
});
|
||||
|
||||
it('should return undefined if no denial detected', () => {
|
||||
const parsed = parseWindowsSandboxDenials({
|
||||
output:
|
||||
'Directory of C:\\Users\r\n03/26/2026 11:40 AM <DIR> .',
|
||||
} as unknown as ShellExecutionResult);
|
||||
|
||||
expect(parsed).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { type ParsedSandboxDenial } from '../../services/sandboxManager.js';
|
||||
import type { ShellExecutionResult } from '../../services/shellExecutionService.js';
|
||||
|
||||
/**
|
||||
* Windows-specific sandbox denial detection.
|
||||
* Extracts paths from "Access is denied" and related errors.
|
||||
*/
|
||||
export function parseWindowsSandboxDenials(
|
||||
result: ShellExecutionResult,
|
||||
): ParsedSandboxDenial | undefined {
|
||||
const output = result.output || '';
|
||||
const errorOutput = result.error?.message;
|
||||
const combined = (output + ' ' + (errorOutput || '')).toLowerCase();
|
||||
|
||||
const isFileDenial = [
|
||||
'access is denied',
|
||||
'access to the path',
|
||||
'unauthorizedaccessexception',
|
||||
'0x80070005',
|
||||
'eperm: operation not permitted',
|
||||
].some((keyword) => combined.includes(keyword));
|
||||
|
||||
const isNetworkDenial = [
|
||||
'eacces: permission denied',
|
||||
'an attempt was made to access a socket in a way forbidden by its access permissions',
|
||||
// 10013 is WSAEACCES
|
||||
'10013',
|
||||
].some((keyword) => combined.includes(keyword));
|
||||
|
||||
if (!isFileDenial && !isNetworkDenial) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const filePaths = new Set<string>();
|
||||
|
||||
// Regex for Windows absolute paths (e.g., C:\Path or \\?\C:\Path)
|
||||
// Handles drive letters and potentially quoted paths.
|
||||
// We use two passes: one for quoted paths (which can contain spaces)
|
||||
// and one for unquoted paths (which end at common separators).
|
||||
|
||||
// 1. Quoted paths: 'C:\Foo Bar' or "C:\Foo Bar"
|
||||
const quotedRegex = /['"]((?:\\\\(?:\?|\.)\\)?[a-zA-Z]:[\\/][^'"]+)['"]/g;
|
||||
for (const match of output.matchAll(quotedRegex)) {
|
||||
filePaths.add(match[1]);
|
||||
}
|
||||
if (errorOutput) {
|
||||
for (const match of errorOutput.matchAll(quotedRegex)) {
|
||||
filePaths.add(match[1]);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Unquoted paths or paths in PowerShell error format: PermissionDenied: (C:\path:String)
|
||||
const generalRegex =
|
||||
/(?:^|[\s(])((?:\\\\(?:\?|\.)\\)?[a-zA-Z]:[\\/][^"'\s()<>|?*]+)/g;
|
||||
for (const match of output.matchAll(generalRegex)) {
|
||||
// Clean up trailing colon which might be part of the error message rather than the path
|
||||
let p = match[1];
|
||||
if (p.endsWith(':')) p = p.slice(0, -1);
|
||||
filePaths.add(p);
|
||||
}
|
||||
if (errorOutput) {
|
||||
for (const match of errorOutput.matchAll(generalRegex)) {
|
||||
let p = match[1];
|
||||
if (p.endsWith(':')) p = p.slice(0, -1);
|
||||
filePaths.add(p);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
network: isNetworkDenial || undefined,
|
||||
filePaths: filePaths.size > 0 ? Array.from(filePaths) : undefined,
|
||||
};
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import fsPromises from 'node:fs/promises';
|
||||
import { afterEach, describe, expect, it, vi, beforeEach } from 'vitest';
|
||||
import {
|
||||
NoopSandboxManager,
|
||||
LocalSandboxManager,
|
||||
sanitizePaths,
|
||||
findSecretFiles,
|
||||
isSecretFile,
|
||||
@@ -374,6 +373,7 @@ describe('SandboxManager', () => {
|
||||
it.each([
|
||||
{ platform: 'linux', expected: LinuxSandboxManager },
|
||||
{ platform: 'darwin', expected: MacOsSandboxManager },
|
||||
{ platform: 'win32', expected: WindowsSandboxManager },
|
||||
] as const)(
|
||||
'should return $expected.name if sandboxing is enabled and platform is $platform',
|
||||
({ platform, expected }) => {
|
||||
@@ -385,23 +385,5 @@ describe('SandboxManager', () => {
|
||||
expect(manager).toBeInstanceOf(expected);
|
||||
},
|
||||
);
|
||||
|
||||
it("should return WindowsSandboxManager if sandboxing is enabled with 'windows-native' command on win32", () => {
|
||||
vi.spyOn(os, 'platform').mockReturnValue('win32');
|
||||
const manager = createSandboxManager(
|
||||
{ enabled: true, command: 'windows-native' },
|
||||
{ workspace: '/workspace' },
|
||||
);
|
||||
expect(manager).toBeInstanceOf(WindowsSandboxManager);
|
||||
});
|
||||
|
||||
it('should return LocalSandboxManager on win32 if command is not windows-native', () => {
|
||||
vi.spyOn(os, 'platform').mockReturnValue('win32');
|
||||
const manager = createSandboxManager(
|
||||
{ enabled: true, command: 'docker' as unknown as 'windows-native' },
|
||||
{ workspace: '/workspace' },
|
||||
);
|
||||
expect(manager).toBeInstanceOf(LocalSandboxManager);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,7 +33,7 @@ export function createSandboxManager(
|
||||
}
|
||||
|
||||
if (sandbox?.enabled) {
|
||||
if (os.platform() === 'win32' && sandbox?.command === 'windows-native') {
|
||||
if (os.platform() === 'win32') {
|
||||
return new WindowsSandboxManager(options);
|
||||
} else if (os.platform() === 'linux') {
|
||||
return new LinuxSandboxManager(options);
|
||||
|
||||
Reference in New Issue
Block a user