-
Notifications
You must be signed in to change notification settings - Fork 3.2k
Expand file tree
/
Copy pathRooIgnoreController.ts
More file actions
225 lines (202 loc) · 6.47 KB
/
RooIgnoreController.ts
File metadata and controls
225 lines (202 loc) · 6.47 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
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
import path from "path"
import { fileExistsAtPath } from "../../utils/fs"
import fs from "fs/promises"
import fsSync from "fs"
import ignore, { Ignore } from "ignore"
import * as vscode from "vscode"
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 .rooignore 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 .rooignore
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 .rooignore changes
*/
private setupFileWatcher(): void {
const rooignorePattern = new vscode.RelativePattern(this.cwd, ".rooignore")
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 .rooignore 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, ".rooignore")
if (await fileExistsAtPath(ignorePath)) {
const content = await fs.readFile(ignorePath, "utf8")
this.rooIgnoreContent = content
this.ignoreInstance.add(content)
this.ignoreInstance.add(".rooignore")
} else {
this.rooIgnoreContent = undefined
}
} catch (error) {
// Should never happen: reading file failed even though it exists
console.error("Unexpected error loading .rooignore:", error)
}
}
/**
* Check if a file should be accessible to the LLM
* Automatically resolves symlinks
* @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 .rooignore does not exist
if (!this.rooIgnoreContent) {
return true
}
try {
const absolutePath = path.resolve(this.cwd, filePath)
// Follow symlinks to get the real path
let realPath: string
try {
realPath = fsSync.realpathSync(absolutePath)
} catch {
// If realpath fails (file doesn't exist, broken symlink, etc.),
// use the original path
realPath = absolutePath
}
// If realpath resolved outside cwd (e.g. symlinks, submodules),
// fall back to the original absolute path for relative computation.
// This prevents path.relative from producing "../" paths that the
// ignore library cannot match against .rooignore patterns.
const relativeFromReal = path.relative(this.cwd, realPath)
const effectivePath = relativeFromReal.startsWith("..") ? absolutePath : realPath
// Convert to relative for .rooignore checking
const relativePath = path.relative(this.cwd, effectivePath).toPosix()
// If the path is still outside cwd after fallback, deny access (fail closed)
if (relativePath.startsWith("..")) {
return false
}
// Check if the real path is ignored
return !this.ignoreInstance.ignores(relativePath)
} catch (error) {
// Fail closed: deny access on unexpected errors for security
return false
}
}
/**
* 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 .rooignore 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 .rooignore file for the LLM
* @returns Formatted instructions or undefined if .rooignore doesn't exist
*/
getInstructions(): string | undefined {
if (!this.rooIgnoreContent) {
return undefined
}
return `# .rooignore\n\n(The following is provided by a root-level .rooignore 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.rooignore`
}
}