forked from RooCodeInc/Roo-Code
-
Notifications
You must be signed in to change notification settings - Fork 12
Expand file tree
/
Copy pathRooIgnoreController.ts
More file actions
201 lines (183 loc) · 6 KB
/
RooIgnoreController.ts
File metadata and controls
201 lines (183 loc) · 6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
import path from "path"
import { fileExistsAtPath } from "../../utils/fs"
import fs from "fs/promises"
import ignore, { Ignore } from "ignore"
import * as vscode from "vscode"
import { AGENT_IGNORE_FILE_NAME } from "../../shared/constants"
export const LOCK_TEXT_SYMBOL = "\u{1F512}"
/**
* Controls LLM access to files by enforcing ignore patterns.
* Designed to be instantiated once in Cline.ts and passed to file manipulation services.
* Uses the 'ignore' library to support standard .gitignore syntax in .pearai-agent-ignore files.
*/
export class RooIgnoreController {
private cwd: string
private ignoreInstance: Ignore
private disposables: vscode.Disposable[] = []
rooIgnoreContent: string | undefined
constructor(cwd: string) {
this.cwd = cwd
this.ignoreInstance = ignore()
this.rooIgnoreContent = undefined
// Set up file watcher for .pearai-agent-ignore
this.setupFileWatcher()
}
/**
* Initialize the controller by loading custom patterns
* Must be called after construction and before using the controller
*/
async initialize(): Promise<void> {
await this.loadRooIgnore()
}
/**
* Set up the file watcher for .pearai-agent-ignore changes
*/
private setupFileWatcher(): void {
const rooignorePattern = new vscode.RelativePattern(this.cwd, AGENT_IGNORE_FILE_NAME)
const fileWatcher = vscode.workspace.createFileSystemWatcher(rooignorePattern)
// Watch for changes and updates
this.disposables.push(
fileWatcher.onDidChange(() => {
this.loadRooIgnore()
}),
fileWatcher.onDidCreate(() => {
this.loadRooIgnore()
}),
fileWatcher.onDidDelete(() => {
this.loadRooIgnore()
}),
)
// Add fileWatcher itself to disposables
this.disposables.push(fileWatcher)
}
/**
* Load custom patterns from .pearai-agent-ignore if it exists
*/
private async loadRooIgnore(): Promise<void> {
try {
// Reset ignore instance to prevent duplicate patterns
this.ignoreInstance = ignore()
const ignorePath = path.join(this.cwd, AGENT_IGNORE_FILE_NAME)
if (await fileExistsAtPath(ignorePath)) {
const content = await fs.readFile(ignorePath, "utf8")
this.rooIgnoreContent = content
this.ignoreInstance.add(content)
this.ignoreInstance.add(AGENT_IGNORE_FILE_NAME)
} else {
this.rooIgnoreContent = undefined
}
} catch (error) {
// Should never happen: reading file failed even though it exists
console.error(`Unexpected error loading ${AGENT_IGNORE_FILE_NAME}:`, error)
}
}
/**
* Check if a file should be accessible to the LLM
* @param filePath - Path to check (relative to cwd)
* @returns true if file is accessible, false if ignored
*/
validateAccess(filePath: string): boolean {
// Always allow access if .pearai-agent-ignore does not exist
if (!this.rooIgnoreContent) {
return true
}
try {
// Normalize path to be relative to cwd and use forward slashes
const absolutePath = path.resolve(this.cwd, filePath)
const relativePath = path.relative(this.cwd, absolutePath).toPosix()
// Ignore expects paths to be path.relative()'d
return !this.ignoreInstance.ignores(relativePath)
} catch (error) {
// console.error(`Error validating access for ${filePath}:`, error)
// Ignore is designed to work with relative file paths, so will throw error for paths outside cwd. We are allowing access to all files outside cwd.
return true
}
}
/**
* Check if a terminal command should be allowed to execute based on file access patterns
* @param command - Terminal command to validate
* @returns path of file that is being accessed if it is being accessed, undefined if command is allowed
*/
validateCommand(command: string): string | undefined {
// Always allow if no .pearai-agent-ignore exists
if (!this.rooIgnoreContent) {
return undefined
}
// Split command into parts and get the base command
const parts = command.trim().split(/\s+/)
const baseCommand = parts[0].toLowerCase()
// Commands that read file contents
const fileReadingCommands = [
// Unix commands
"cat",
"less",
"more",
"head",
"tail",
"grep",
"awk",
"sed",
// PowerShell commands and aliases
"get-content",
"gc",
"type",
"select-string",
"sls",
]
if (fileReadingCommands.includes(baseCommand)) {
// Check each argument that could be a file path
for (let i = 1; i < parts.length; i++) {
const arg = parts[i]
// Skip command flags/options (both Unix and PowerShell style)
if (arg.startsWith("-") || arg.startsWith("/")) {
continue
}
// Ignore PowerShell parameter names
if (arg.includes(":")) {
continue
}
// Validate file access
if (!this.validateAccess(arg)) {
return arg
}
}
}
return undefined
}
/**
* Filter an array of paths, removing those that should be ignored
* @param paths - Array of paths to filter (relative to cwd)
* @returns Array of allowed paths
*/
filterPaths(paths: string[]): string[] {
try {
return paths
.map((p) => ({
path: p,
allowed: this.validateAccess(p),
}))
.filter((x) => x.allowed)
.map((x) => x.path)
} catch (error) {
console.error("Error filtering paths:", error)
return [] // Fail closed for security
}
}
/**
* Clean up resources when the controller is no longer needed
*/
dispose(): void {
this.disposables.forEach((d) => d.dispose())
this.disposables = []
}
/**
* Get formatted instructions about the .pearai-agent-ignore file for the LLM
* @returns Formatted instructions or undefined if .pearai-agent-ignore doesn't exist
*/
getInstructions(): string | undefined {
if (!this.rooIgnoreContent) {
return undefined
}
return `# ${AGENT_IGNORE_FILE_NAME}\n\n(The following is provided by a root-level ${AGENT_IGNORE_FILE_NAME} file where the user has specified files and directories that should not be accessed. When using list_files, you'll notice a ${LOCK_TEXT_SYMBOL} next to files that are blocked. Attempting to access the file's contents e.g. through read_file will result in an error.)\n\n${this.rooIgnoreContent}\n${AGENT_IGNORE_FILE_NAME}`
}
}