Roo-Code/src/core/tools/CodebaseSearchTool.ts
Roo Code ba42cbdf87 fix: enforce .rooignore rules for codebase indexing, file reads, and file listing
- Harden validateAccess() to fall back to original path when realpath
  resolves outside cwd (fixes submodule/symlink bypass)
- Change error handling in validateAccess() to fail closed (deny access)
  instead of fail open
- Add .rooignore post-filtering in CodebaseSearchTool to exclude ignored
  files from search results even if they were previously indexed
- Pass RooIgnoreController from manager through service-factory to scanner
  so the scanner reuses the workspace-root controller instead of creating
  its own from the scan directory
- Fix FileWatcher to initialize fallback RooIgnoreController in
  initialize() so .rooignore rules load even when manager controller
  is not passed
- Add tests for realpath-outside-cwd fallback, fail-closed error handling,
  CodebaseSearchTool rooignore filtering, and scanner controller passthrough

Fixes #11797
2026-02-28 15:42:32 +00:00

159 lines
4.6 KiB
TypeScript

import * as vscode from "vscode"
import path from "path"
import { Task } from "../task/Task"
import { CodeIndexManager } from "../../services/code-index/manager"
import { getWorkspacePath } from "../../utils/path"
import { formatResponse } from "../prompts/responses"
import { VectorStoreSearchResult } from "../../services/code-index/interfaces"
import type { ToolUse } from "../../shared/tools"
import { BaseTool, ToolCallbacks } from "./BaseTool"
interface CodebaseSearchParams {
query: string
path?: string
}
export class CodebaseSearchTool extends BaseTool<"codebase_search"> {
readonly name = "codebase_search" as const
async execute(params: CodebaseSearchParams, task: Task, callbacks: ToolCallbacks): Promise<void> {
const { askApproval, handleError, pushToolResult } = callbacks
const { query, path: directoryPrefix } = params
const workspacePath = task.cwd && task.cwd.trim() !== "" ? task.cwd : getWorkspacePath()
if (!workspacePath) {
await handleError("codebase_search", new Error("Could not determine workspace path."))
return
}
if (!query) {
task.consecutiveMistakeCount++
task.didToolFailInCurrentTurn = true
pushToolResult(await task.sayAndCreateMissingParamError("codebase_search", "query"))
return
}
const sharedMessageProps = {
tool: "codebaseSearch",
query: query,
path: directoryPrefix,
isOutsideWorkspace: false,
}
const didApprove = await askApproval("tool", JSON.stringify(sharedMessageProps))
if (!didApprove) {
pushToolResult(formatResponse.toolDenied())
return
}
task.consecutiveMistakeCount = 0
try {
const context = task.providerRef.deref()?.context
if (!context) {
throw new Error("Extension context is not available.")
}
const manager = CodeIndexManager.getInstance(context)
if (!manager) {
throw new Error("CodeIndexManager is not available.")
}
if (!manager.isFeatureEnabled) {
throw new Error("Code Indexing is disabled in the settings.")
}
if (!manager.isFeatureConfigured) {
throw new Error("Code Indexing is not configured (Missing OpenAI Key or Qdrant URL).")
}
const searchResults: VectorStoreSearchResult[] = await manager.searchIndex(query, directoryPrefix)
// Post-filter results through .rooignore to exclude files that were
// indexed before being added to .rooignore, or that bypassed filtering
// during indexing (e.g. due to symlink/submodule path resolution).
const filteredResults = task.rooIgnoreController
? searchResults.filter((result) => {
// Guard clause: skip entries without a valid payload/filePath.
// These are structural no-ops (not an ignore decision).
if (!result.payload || !("filePath" in result.payload)) return false
const relativePath = vscode.workspace.asRelativePath(result.payload.filePath, false)
return task.rooIgnoreController!.validateAccess(relativePath)
})
: searchResults
if (!filteredResults || filteredResults.length === 0) {
pushToolResult(`No relevant code snippets found for the query: "${query}"`)
return
}
const jsonResult = {
query,
results: [],
} as {
query: string
results: Array<{
filePath: string
score: number
startLine: number
endLine: number
codeChunk: string
}>
}
filteredResults.forEach((result) => {
if (!result.payload) return
if (!("filePath" in result.payload)) return
const relativePath = vscode.workspace.asRelativePath(result.payload.filePath, false)
jsonResult.results.push({
filePath: relativePath,
score: result.score,
startLine: result.payload.startLine,
endLine: result.payload.endLine,
codeChunk: result.payload.codeChunk.trim(),
})
})
const payload = { tool: "codebaseSearch", content: jsonResult }
await task.say("codebase_search_result", JSON.stringify(payload))
const output = `Query: ${query}
Results:
${jsonResult.results
.map(
(result) => `File path: ${result.filePath}
Score: ${result.score}
Lines: ${result.startLine}-${result.endLine}
Code Chunk: ${result.codeChunk}
`,
)
.join("\n")}`
pushToolResult(output)
} catch (error: any) {
await handleError("codebase_search", error)
}
}
override async handlePartial(task: Task, block: ToolUse<"codebase_search">): Promise<void> {
const query: string | undefined = block.params.query
const directoryPrefix: string | undefined = block.params.path
const sharedMessageProps = {
tool: "codebaseSearch",
query: query,
path: directoryPrefix,
isOutsideWorkspace: false,
}
await task.ask("tool", JSON.stringify(sharedMessageProps), block.partial).catch(() => {})
}
}
export const codebaseSearchTool = new CodebaseSearchTool()