mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-21 10:34:35 -07:00
fix(core): ensure sandbox approvals are correctly persisted and matched for proactive expansions (#24577)
This commit is contained in:
@@ -15,6 +15,7 @@ import {
|
||||
shortenPath,
|
||||
normalizePath,
|
||||
resolveToRealPath,
|
||||
makeRelative,
|
||||
} from './paths.js';
|
||||
|
||||
vi.mock('node:fs', async (importOriginal) => {
|
||||
@@ -215,7 +216,7 @@ describe('isSubpath', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSubpath on Windows', () => {
|
||||
describe.skipIf(process.platform !== 'win32')('isSubpath on Windows', () => {
|
||||
afterEach(() => vi.unstubAllGlobals());
|
||||
|
||||
beforeEach(() => mockPlatform('win32'));
|
||||
@@ -268,6 +269,20 @@ describe('isSubpath on Windows', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(process.platform !== 'darwin')('isSubpath on Darwin', () => {
|
||||
afterEach(() => vi.unstubAllGlobals());
|
||||
|
||||
beforeEach(() => mockPlatform('darwin'));
|
||||
|
||||
it('should be case-insensitive for path components on Darwin', () => {
|
||||
expect(isSubpath('/PROJECT', '/project/src')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for a direct subpath on Darwin', () => {
|
||||
expect(isSubpath('/Users/Test', '/Users/Test/file.txt')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('shortenPath', () => {
|
||||
describe.skipIf(process.platform === 'win32')('on POSIX', () => {
|
||||
it('should not shorten a path that is shorter than maxLen', () => {
|
||||
@@ -586,6 +601,54 @@ describe('resolveToRealPath', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('makeRelative', () => {
|
||||
describe.skipIf(process.platform === 'win32')('on POSIX', () => {
|
||||
it('should return relative path if targetPath is already relative', () => {
|
||||
expect(makeRelative('foo/bar', '/root')).toBe('foo/bar');
|
||||
});
|
||||
|
||||
it('should return relative path from root to target', () => {
|
||||
const root = '/Users/test/project';
|
||||
const target = '/Users/test/project/src/file.ts';
|
||||
expect(makeRelative(target, root)).toBe('src/file.ts');
|
||||
});
|
||||
|
||||
it('should return "." if target and root are the same', () => {
|
||||
const root = '/Users/test/project';
|
||||
expect(makeRelative(root, root)).toBe('.');
|
||||
});
|
||||
|
||||
it('should handle parent directories with ..', () => {
|
||||
const root = '/Users/test/project/src';
|
||||
const target = '/Users/test/project/docs/readme.md';
|
||||
expect(makeRelative(target, root)).toBe('../docs/readme.md');
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(process.platform !== 'win32')('on Windows', () => {
|
||||
it('should return relative path if targetPath is already relative', () => {
|
||||
expect(makeRelative('foo/bar', 'C:\\root')).toBe('foo/bar');
|
||||
});
|
||||
|
||||
it('should return relative path from root to target', () => {
|
||||
const root = 'C:\\Users\\test\\project';
|
||||
const target = 'C:\\Users\\test\\project\\src\\file.ts';
|
||||
expect(makeRelative(target, root)).toBe('src\\file.ts');
|
||||
});
|
||||
|
||||
it('should return "." if target and root are the same', () => {
|
||||
const root = 'C:\\Users\\test\\project';
|
||||
expect(makeRelative(root, root)).toBe('.');
|
||||
});
|
||||
|
||||
it('should handle parent directories with ..', () => {
|
||||
const root = 'C:\\Users\\test\\project\\src';
|
||||
const target = 'C:\\Users\\test\\project\\docs\\readme.md';
|
||||
expect(makeRelative(target, root)).toBe('..\\docs\\readme.md');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizePath', () => {
|
||||
it('should resolve a relative path to an absolute path', () => {
|
||||
const result = normalizePath('some/relative/path');
|
||||
@@ -615,7 +678,19 @@ describe('normalizePath', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(process.platform === 'win32')('on POSIX', () => {
|
||||
describe.skipIf(process.platform !== 'darwin')('on Darwin', () => {
|
||||
beforeEach(() => mockPlatform('darwin'));
|
||||
afterEach(() => vi.unstubAllGlobals());
|
||||
|
||||
it('should lowercase the entire path', () => {
|
||||
const result = normalizePath('/Users/TEST');
|
||||
expect(result).toBe('/users/test');
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(
|
||||
process.platform === 'win32' || process.platform === 'darwin',
|
||||
)('on Linux', () => {
|
||||
it('should preserve case', () => {
|
||||
const result = normalizePath('/usr/Local/Bin');
|
||||
expect(result).toContain('Local');
|
||||
|
||||
@@ -325,9 +325,14 @@ export function getProjectHash(projectRoot: string): string {
|
||||
* - On Windows, converts to lowercase for case-insensitivity.
|
||||
*/
|
||||
export function normalizePath(p: string): string {
|
||||
const resolved = path.resolve(p);
|
||||
const platform = process.platform;
|
||||
const isWindows = platform === 'win32';
|
||||
const pathModule = isWindows ? path.win32 : path;
|
||||
|
||||
const resolved = pathModule.resolve(p);
|
||||
const normalized = resolved.replace(/\\/g, '/');
|
||||
return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
|
||||
const isCaseInsensitive = isWindows || platform === 'darwin';
|
||||
return isCaseInsensitive ? normalized.toLowerCase() : normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -337,11 +342,25 @@ export function normalizePath(p: string): string {
|
||||
* @returns True if childPath is a subpath of parentPath, false otherwise.
|
||||
*/
|
||||
export function isSubpath(parentPath: string, childPath: string): boolean {
|
||||
const isWindows = process.platform === 'win32';
|
||||
const platform = process.platform;
|
||||
const isWindows = platform === 'win32';
|
||||
const isDarwin = platform === 'darwin';
|
||||
const pathModule = isWindows ? path.win32 : path;
|
||||
|
||||
// On Windows, path.relative is case-insensitive. On POSIX, it's case-sensitive.
|
||||
const relative = pathModule.relative(parentPath, childPath);
|
||||
// Resolve both paths to absolute to ensure consistent comparison,
|
||||
// especially when mixing relative and absolute paths or when casing differs.
|
||||
let p = pathModule.resolve(parentPath);
|
||||
let c = pathModule.resolve(childPath);
|
||||
|
||||
// On Windows, path.relative is case-insensitive.
|
||||
// On POSIX (including Darwin), path.relative is case-sensitive.
|
||||
// We want it to be case-insensitive on Darwin to match user expectation and sandbox policy.
|
||||
if (isDarwin) {
|
||||
p = p.toLowerCase();
|
||||
c = c.toLowerCase();
|
||||
}
|
||||
|
||||
const relative = pathModule.relative(p, c);
|
||||
|
||||
return (
|
||||
!relative.startsWith(`..${pathModule.sep}`) &&
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
parseCommandDetails,
|
||||
splitCommands,
|
||||
stripShellWrapper,
|
||||
normalizeCommand,
|
||||
hasRedirection,
|
||||
resolveExecutable,
|
||||
} from './shell-utils.js';
|
||||
@@ -115,6 +116,23 @@ const mockPowerShellResult = (
|
||||
});
|
||||
};
|
||||
|
||||
describe('normalizeCommand', () => {
|
||||
it('should lowercase the command', () => {
|
||||
expect(normalizeCommand('NPM')).toBe('npm');
|
||||
});
|
||||
|
||||
it('should remove .exe extension', () => {
|
||||
expect(normalizeCommand('node.exe')).toBe('node');
|
||||
});
|
||||
|
||||
it('should handle absolute paths', () => {
|
||||
expect(normalizeCommand('/usr/bin/npm')).toBe('npm');
|
||||
expect(normalizeCommand('C:\\Program Files\\nodejs\\node.exe')).toBe(
|
||||
'node',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCommandRoots', () => {
|
||||
it('should return a single command', () => {
|
||||
expect(getCommandRoots('ls -l')).toEqual(['ls']);
|
||||
|
||||
@@ -310,6 +310,20 @@ function normalizeCommandName(raw: string): string {
|
||||
return raw.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a command name for sandbox policy lookups.
|
||||
* Converts to lowercase and removes the .exe extension for cross-platform consistency.
|
||||
*
|
||||
* @param commandName - The command name to normalize.
|
||||
* @returns The normalized command name.
|
||||
*/
|
||||
export function normalizeCommand(commandName: string): string {
|
||||
// Split by both separators and get the last non-empty part
|
||||
const parts = commandName.split(/[\\/]/).filter(Boolean);
|
||||
const base = parts.length > 0 ? parts[parts.length - 1] : '';
|
||||
return base.toLowerCase().replace(/\.exe$/, '');
|
||||
}
|
||||
|
||||
function extractNameFromNode(node: Node): string | null {
|
||||
switch (node.type) {
|
||||
case 'command': {
|
||||
|
||||
Reference in New Issue
Block a user