mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-25 04:24:51 -07:00
refactor(core): centralize path validation and allow temp dir access for tools (#17185)
Co-authored-by: Your Name <joshualitt@google.com>
This commit is contained in:
@@ -28,6 +28,7 @@ import type { Config } from '../config/config.js';
|
||||
import { ApprovalMode } from '../policy/types.js';
|
||||
import type { ToolRegistry } from './tool-registry.js';
|
||||
import path from 'node:path';
|
||||
import { isSubpath } from '../utils/paths.js';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import { GeminiClient } from '../core/client.js';
|
||||
@@ -59,6 +60,7 @@ vi.mock('../ide/ide-client.js', () => ({
|
||||
}));
|
||||
let mockGeminiClientInstance: Mocked<GeminiClient>;
|
||||
let mockBaseLlmClientInstance: Mocked<BaseLlmClient>;
|
||||
let mockConfig: Config;
|
||||
const mockEnsureCorrectEdit = vi.fn<typeof ensureCorrectEdit>();
|
||||
const mockEnsureCorrectFileContent = vi.fn<typeof ensureCorrectFileContent>();
|
||||
const mockIdeClient = {
|
||||
@@ -108,8 +110,10 @@ const mockConfigInternal = {
|
||||
}) as unknown as ToolRegistry,
|
||||
isInteractive: () => false,
|
||||
getDisableLLMCorrection: vi.fn(() => true),
|
||||
storage: {
|
||||
getProjectTempDir: vi.fn().mockReturnValue('/tmp/project'),
|
||||
},
|
||||
};
|
||||
const mockConfig = mockConfigInternal as unknown as Config;
|
||||
|
||||
vi.mock('../telemetry/loggers.js', () => ({
|
||||
logFileOperation: vi.fn(),
|
||||
@@ -135,6 +139,35 @@ describe('WriteFileTool', () => {
|
||||
fs.mkdirSync(plansDir, { recursive: true });
|
||||
}
|
||||
|
||||
const workspaceContext = new WorkspaceContext(rootDir, [plansDir]);
|
||||
const mockStorage = {
|
||||
getProjectTempDir: vi.fn().mockReturnValue('/tmp/project'),
|
||||
};
|
||||
|
||||
mockConfig = {
|
||||
...mockConfigInternal,
|
||||
getWorkspaceContext: () => workspaceContext,
|
||||
storage: mockStorage,
|
||||
isPathAllowed(this: Config, absolutePath: string): boolean {
|
||||
const workspaceContext = this.getWorkspaceContext();
|
||||
if (workspaceContext.isPathWithinWorkspace(absolutePath)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const projectTempDir = this.storage.getProjectTempDir();
|
||||
return isSubpath(path.resolve(projectTempDir), absolutePath);
|
||||
},
|
||||
validatePathAccess(this: Config, absolutePath: string): string | null {
|
||||
if (this.isPathAllowed(absolutePath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const workspaceDirs = this.getWorkspaceContext().getDirectories();
|
||||
const projectTempDir = this.storage.getProjectTempDir();
|
||||
return `Path not in workspace: Attempted path "${absolutePath}" resolves outside the allowed workspace directories: ${workspaceDirs.join(', ')} or the project temp directory: ${projectTempDir}`;
|
||||
},
|
||||
} as unknown as Config;
|
||||
|
||||
// Setup GeminiClient mock
|
||||
mockGeminiClientInstance = new (vi.mocked(GeminiClient))(
|
||||
mockConfig,
|
||||
@@ -243,9 +276,7 @@ describe('WriteFileTool', () => {
|
||||
file_path: outsidePath,
|
||||
content: 'hello',
|
||||
};
|
||||
expect(() => tool.build(params)).toThrow(
|
||||
/File path must be within one of the workspace directories/,
|
||||
);
|
||||
expect(() => tool.build(params)).toThrow(/Path not in workspace/);
|
||||
});
|
||||
|
||||
it('should throw an error if path is a directory', () => {
|
||||
@@ -816,9 +847,7 @@ describe('WriteFileTool', () => {
|
||||
file_path: '/etc/passwd',
|
||||
content: 'malicious',
|
||||
};
|
||||
expect(() => tool.build(params)).toThrow(
|
||||
/File path must be within one of the workspace directories/,
|
||||
);
|
||||
expect(() => tool.build(params)).toThrow(/Path not in workspace/);
|
||||
});
|
||||
|
||||
it('should allow paths within the plans directory', () => {
|
||||
@@ -834,9 +863,7 @@ describe('WriteFileTool', () => {
|
||||
file_path: path.join(plansDir, '..', 'escaped.txt'),
|
||||
content: 'malicious',
|
||||
};
|
||||
expect(() => tool.build(params)).toThrow(
|
||||
/File path must be within one of the workspace directories/,
|
||||
);
|
||||
expect(() => tool.build(params)).toThrow(/Path not in workspace/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -874,7 +901,6 @@ describe('WriteFileTool', () => {
|
||||
errorMessage: 'Generic write error',
|
||||
expectedMessagePrefix: 'Error writing to file',
|
||||
mockFsExistsSync: false,
|
||||
restoreAllMocks: true,
|
||||
},
|
||||
])(
|
||||
'should return $errorType error when write fails with $errorCode',
|
||||
@@ -884,25 +910,22 @@ describe('WriteFileTool', () => {
|
||||
errorMessage,
|
||||
expectedMessagePrefix,
|
||||
mockFsExistsSync,
|
||||
restoreAllMocks,
|
||||
}) => {
|
||||
const filePath = path.join(rootDir, `${errorType}_file.txt`);
|
||||
const content = 'test content';
|
||||
|
||||
if (restoreAllMocks) {
|
||||
vi.restoreAllMocks();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let existsSyncSpy: any;
|
||||
let existsSyncSpy: // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
ReturnType<typeof vi.spyOn<any, 'existsSync'>> | undefined = undefined;
|
||||
|
||||
try {
|
||||
if (mockFsExistsSync) {
|
||||
const originalExistsSync = fs.existsSync;
|
||||
existsSyncSpy = vi
|
||||
.spyOn(fs, 'existsSync')
|
||||
.mockImplementation((path) =>
|
||||
path === filePath ? false : originalExistsSync(path as string),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
.spyOn(fs as any, 'existsSync')
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
.mockImplementation((path: any) =>
|
||||
path === filePath ? false : originalExistsSync(path),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user