mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-18 01:51:20 -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:
@@ -75,15 +75,46 @@ describe('handleAtCommand', () => {
|
||||
getFileSystemService: () => new StandardFileSystemService(),
|
||||
getEnableRecursiveFileSearch: vi.fn(() => true),
|
||||
getWorkspaceContext: () => ({
|
||||
isPathWithinWorkspace: () => true,
|
||||
isPathWithinWorkspace: (p: string) =>
|
||||
p.startsWith(testRootDir) || p.startsWith('/private' + testRootDir),
|
||||
getDirectories: () => [testRootDir],
|
||||
}),
|
||||
storage: {
|
||||
getProjectTempDir: () => path.join(os.tmpdir(), 'gemini-cli-temp'),
|
||||
},
|
||||
isPathAllowed(this: Config, absolutePath: string): boolean {
|
||||
if (this.interactive && path.isAbsolute(absolutePath)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const workspaceContext = this.getWorkspaceContext();
|
||||
if (workspaceContext.isPathWithinWorkspace(absolutePath)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const projectTempDir = this.storage.getProjectTempDir();
|
||||
const resolvedProjectTempDir = path.resolve(projectTempDir);
|
||||
return (
|
||||
absolutePath.startsWith(resolvedProjectTempDir + path.sep) ||
|
||||
absolutePath === resolvedProjectTempDir
|
||||
);
|
||||
},
|
||||
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 validation failed: Attempted path "${absolutePath}" resolves outside the allowed workspace directories: ${workspaceDirs.join(', ')} or the project temp directory: ${projectTempDir}`;
|
||||
},
|
||||
getMcpServers: () => ({}),
|
||||
getMcpServerCommand: () => undefined,
|
||||
getPromptRegistry: () => ({
|
||||
getPromptsByServer: () => [],
|
||||
}),
|
||||
getDebugMode: () => false,
|
||||
getWorkingDir: () => '/working/dir',
|
||||
getFileExclusions: () => ({
|
||||
getCoreIgnorePatterns: () => COMMON_IGNORE_PATTERNS,
|
||||
getDefaultExcludePatterns: () => [],
|
||||
|
||||
@@ -230,8 +230,11 @@ export async function handleAtCommand({
|
||||
continue;
|
||||
}
|
||||
|
||||
const workspaceContext = config.getWorkspaceContext();
|
||||
if (!workspaceContext.isPathWithinWorkspace(pathName)) {
|
||||
const resolvedPathName = path.isAbsolute(pathName)
|
||||
? pathName
|
||||
: path.resolve(config.getTargetDir(), pathName);
|
||||
|
||||
if (!config.isPathAllowed(resolvedPathName)) {
|
||||
onDebugMessage(
|
||||
`Path ${pathName} is not in the workspace and will be skipped.`,
|
||||
);
|
||||
|
||||
@@ -77,15 +77,46 @@ describe('handleAtCommand with Agents', () => {
|
||||
getFileSystemService: () => new StandardFileSystemService(),
|
||||
getEnableRecursiveFileSearch: vi.fn(() => true),
|
||||
getWorkspaceContext: () => ({
|
||||
isPathWithinWorkspace: () => true,
|
||||
isPathWithinWorkspace: (p: string) =>
|
||||
p.startsWith(testRootDir) || p.startsWith('/private' + testRootDir),
|
||||
getDirectories: () => [testRootDir],
|
||||
}),
|
||||
storage: {
|
||||
getProjectTempDir: () => path.join(os.tmpdir(), 'gemini-cli-temp'),
|
||||
},
|
||||
isPathAllowed(this: Config, absolutePath: string): boolean {
|
||||
if (this.interactive && path.isAbsolute(absolutePath)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const workspaceContext = this.getWorkspaceContext();
|
||||
if (workspaceContext.isPathWithinWorkspace(absolutePath)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const projectTempDir = this.storage.getProjectTempDir();
|
||||
const resolvedProjectTempDir = path.resolve(projectTempDir);
|
||||
return (
|
||||
absolutePath.startsWith(resolvedProjectTempDir + path.sep) ||
|
||||
absolutePath === resolvedProjectTempDir
|
||||
);
|
||||
},
|
||||
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 validation failed: Attempted path "${absolutePath}" resolves outside the allowed workspace directories: ${workspaceDirs.join(', ')} or the project temp directory: ${projectTempDir}`;
|
||||
},
|
||||
getMcpServers: () => ({}),
|
||||
getMcpServerCommand: () => undefined,
|
||||
getPromptRegistry: () => ({
|
||||
getPromptsByServer: () => [],
|
||||
}),
|
||||
getDebugMode: () => false,
|
||||
getWorkingDir: () => '/working/dir',
|
||||
getFileExclusions: () => ({
|
||||
getCoreIgnorePatterns: () => COMMON_IGNORE_PATTERNS,
|
||||
getDefaultExcludePatterns: () => [],
|
||||
@@ -102,8 +133,9 @@ describe('handleAtCommand with Agents', () => {
|
||||
getMcpClientManager: () => ({
|
||||
getClient: () => undefined,
|
||||
}),
|
||||
getAgentRegistry: () => mockAgentRegistry,
|
||||
getMessageBus: () => mockMessageBus,
|
||||
interactive: true,
|
||||
getAgentRegistry: () => mockAgentRegistry,
|
||||
} as unknown as Config;
|
||||
|
||||
const registry = new ToolRegistry(mockConfig, mockMessageBus);
|
||||
|
||||
@@ -56,6 +56,7 @@ const MockedGeminiClientClass = vi.hoisted(() =>
|
||||
this.startChat = mockStartChat;
|
||||
this.sendMessageStream = mockSendMessageStream;
|
||||
this.addHistory = vi.fn();
|
||||
this.getCurrentSequenceModel = vi.fn();
|
||||
this.getChat = vi.fn().mockReturnValue({
|
||||
recordCompletedToolCalls: vi.fn(),
|
||||
});
|
||||
@@ -75,6 +76,13 @@ const MockedUserPromptEvent = vi.hoisted(() =>
|
||||
);
|
||||
const mockParseAndFormatApiError = vi.hoisted(() => vi.fn());
|
||||
|
||||
const MockValidationRequiredError = vi.hoisted(
|
||||
() =>
|
||||
class extends Error {
|
||||
userHandled = false;
|
||||
},
|
||||
);
|
||||
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const actualCoreModule = (await importOriginal()) as any;
|
||||
return {
|
||||
@@ -82,6 +90,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
GitService: vi.fn(),
|
||||
GeminiClient: MockedGeminiClientClass,
|
||||
UserPromptEvent: MockedUserPromptEvent,
|
||||
ValidationRequiredError: MockValidationRequiredError,
|
||||
parseAndFormatApiError: mockParseAndFormatApiError,
|
||||
tokenLimit: vi.fn().mockReturnValue(100), // Mock tokenLimit
|
||||
};
|
||||
@@ -221,6 +230,7 @@ describe('useGeminiStream', () => {
|
||||
getApprovalMode: () => ApprovalMode.DEFAULT,
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
getDebugMode: () => false,
|
||||
getWorkingDir: () => '/working/dir',
|
||||
addHistory: vi.fn(),
|
||||
getSessionId() {
|
||||
return 'test-session-id';
|
||||
@@ -228,6 +238,7 @@ describe('useGeminiStream', () => {
|
||||
setQuotaErrorOccurred: vi.fn(),
|
||||
getQuotaErrorOccurred: vi.fn(() => false),
|
||||
getModel: vi.fn(() => 'gemini-2.5-pro'),
|
||||
getContentGenerator: vi.fn(),
|
||||
getContentGeneratorConfig: vi
|
||||
.fn()
|
||||
.mockReturnValue(contentGeneratorConfig),
|
||||
@@ -1671,6 +1682,7 @@ describe('useGeminiStream', () => {
|
||||
|
||||
const testConfig = {
|
||||
...mockConfig,
|
||||
getContentGenerator: vi.fn(),
|
||||
getContentGeneratorConfig: vi.fn(() => ({
|
||||
authType: mockAuthType,
|
||||
})),
|
||||
|
||||
@@ -65,6 +65,7 @@ const mockConfig = {
|
||||
getSessionId: () => 'test-session-id',
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
getDebugMode: () => false,
|
||||
getWorkingDir: () => '/working/dir',
|
||||
storage: {
|
||||
getProjectTempDir: () => '/tmp',
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user