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:
N. Taylor Mullen
2026-01-27 13:17:40 -08:00
committed by GitHub
parent c9340a9c6f
commit 5f569fa103
26 changed files with 1149 additions and 609 deletions
+150 -29
View File
@@ -16,6 +16,7 @@ import {
import type { RipGrepToolParams } from './ripGrep.js';
import { canUseRipgrep, RipGrepTool, ensureRgPath } from './ripGrep.js';
import path from 'node:path';
import { isSubpath } from '../utils/paths.js';
import fs from 'node:fs/promises';
import os from 'node:os';
import type { Config } from '../config/config.js';
@@ -246,13 +247,7 @@ describe('RipGrepTool', () => {
let ripgrepBinaryPath: string;
let grepTool: RipGrepTool;
const abortSignal = new AbortController().signal;
const mockConfig = {
getTargetDir: () => tempRootDir,
getWorkspaceContext: () => createMockWorkspaceContext(tempRootDir),
getDebugMode: () => false,
getFileFilteringRespectGeminiIgnore: () => true,
} as unknown as Config;
let mockConfig: Config;
beforeEach(async () => {
downloadRipGrepMock.mockReset();
@@ -266,6 +261,35 @@ describe('RipGrepTool', () => {
await fs.writeFile(ripgrepBinaryPath, '');
storageSpy.mockImplementation(() => binDir);
tempRootDir = await fs.mkdtemp(path.join(os.tmpdir(), 'grep-tool-root-'));
mockConfig = {
getTargetDir: () => tempRootDir,
getWorkspaceContext: () => createMockWorkspaceContext(tempRootDir),
getDebugMode: () => false,
getFileFilteringRespectGeminiIgnore: () => true,
storage: {
getProjectTempDir: vi.fn().mockReturnValue('/tmp/project'),
},
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;
grepTool = new RipGrepTool(mockConfig, createMockMessageBus());
// Create some test files and directories
@@ -311,11 +335,6 @@ describe('RipGrepTool', () => {
params: { pattern: 'hello', dir_path: '.', include: '*.txt' },
expected: null,
},
{
name: 'invalid regex pattern',
params: { pattern: '[[' },
expected: null,
},
])(
'should return null for valid params ($name)',
({ params, expected }) => {
@@ -323,6 +342,13 @@ describe('RipGrepTool', () => {
},
);
it('should throw error for invalid regex pattern', () => {
const params: RipGrepToolParams = { pattern: '[[' };
expect(grepTool.validateToolParams(params)).toMatch(
/Invalid regular expression pattern provided/,
);
});
it('should return error if pattern is missing', () => {
const params = { dir_path: '.' } as unknown as RipGrepToolParams;
expect(grepTool.validateToolParams(params)).toBe(
@@ -336,10 +362,9 @@ describe('RipGrepTool', () => {
dir_path: 'nonexistent',
};
// Check for the core error message, as the full path might vary
expect(grepTool.validateToolParams(params)).toContain(
'Path does not exist',
);
expect(grepTool.validateToolParams(params)).toContain('nonexistent');
const result = grepTool.validateToolParams(params);
expect(result).toMatch(/Path does not exist/);
expect(result).toMatch(/nonexistent/);
});
it('should allow path to be a file', async () => {
@@ -550,19 +575,10 @@ describe('RipGrepTool', () => {
expect(result.returnDisplay).toBe('No matches found');
});
it('should return an error from ripgrep for invalid regex pattern', async () => {
mockSpawn.mockImplementationOnce(
createMockSpawn({
exitCode: 2,
}),
);
it('should throw error for invalid regex pattern during build', async () => {
const params: RipGrepToolParams = { pattern: '[[' };
const invocation = grepTool.build(params);
const result = await invocation.execute(abortSignal);
expect(result.llmContent).toContain('Process exited with code 2');
expect(result.returnDisplay).toContain(
'Error: Process exited with code 2',
expect(() => grepTool.build(params)).toThrow(
/Invalid regular expression pattern provided/,
);
});
@@ -763,6 +779,27 @@ describe('RipGrepTool', () => {
createMockWorkspaceContext(tempRootDir, [secondDir]),
getDebugMode: () => false,
getFileFilteringRespectGeminiIgnore: () => true,
storage: {
getProjectTempDir: vi.fn().mockReturnValue('/tmp/project'),
},
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 specific mock for this test - multi-directory search for 'world'
@@ -850,6 +887,27 @@ describe('RipGrepTool', () => {
createMockWorkspaceContext(tempRootDir, [secondDir]),
getDebugMode: () => false,
getFileFilteringRespectGeminiIgnore: () => true,
storage: {
getProjectTempDir: vi.fn().mockReturnValue('/tmp/project'),
},
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 specific mock for this test - searching in 'sub' should only return matches from that directory
@@ -931,7 +989,7 @@ describe('RipGrepTool', () => {
pattern: 'test',
dir_path: '../outside',
};
expect(() => grepTool.build(params)).toThrow(/Path validation failed/);
expect(() => grepTool.build(params)).toThrow(/Path not in workspace/);
});
it.each([
@@ -1353,6 +1411,27 @@ describe('RipGrepTool', () => {
getWorkspaceContext: () => createMockWorkspaceContext(tempRootDir),
getDebugMode: () => false,
getFileFilteringRespectGeminiIgnore: () => true,
storage: {
getProjectTempDir: vi.fn().mockReturnValue('/tmp/project'),
},
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;
const geminiIgnoreTool = new RipGrepTool(
configWithGeminiIgnore,
@@ -1393,6 +1472,27 @@ describe('RipGrepTool', () => {
getWorkspaceContext: () => createMockWorkspaceContext(tempRootDir),
getDebugMode: () => false,
getFileFilteringRespectGeminiIgnore: () => false,
storage: {
getProjectTempDir: vi.fn().mockReturnValue('/tmp/project'),
},
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;
const geminiIgnoreTool = new RipGrepTool(
configWithoutGeminiIgnore,
@@ -1518,6 +1618,27 @@ describe('RipGrepTool', () => {
getWorkspaceContext: () =>
createMockWorkspaceContext(tempRootDir, ['/another/dir']),
getDebugMode: () => false,
storage: {
getProjectTempDir: vi.fn().mockReturnValue('/tmp/project'),
},
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;
const multiDirGrepTool = new RipGrepTool(