Skip to content

Commit fa67db7

Browse files
Add clineignore class into cline file (RooCodeInc#1623)
1 parent c97fb24 commit fa67db7

File tree

3 files changed

+89
-31
lines changed

3 files changed

+89
-31
lines changed

src/core/Cline.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import { HistoryItem } from "../shared/HistoryItem"
4646
import { ClineAskResponse, ClineCheckpointRestore } from "../shared/WebviewMessage"
4747
import { calculateApiCost } from "../utils/cost"
4848
import { fileExistsAtPath } from "../utils/fs"
49+
import { LLMFileAccessController } from "../services/llm-access-control/LLMFileAccessController"
4950
import { arePathsEqual, getReadablePath } from "../utils/path"
5051
import { fixModelHtmlEscaping, removeInvalidChars } from "../utils/string"
5152
import { AssistantMessageContent, parseAssistantMessage, ToolParamName, ToolUseName } from "./assistant-message"
@@ -80,6 +81,7 @@ export class Cline {
8081
private chatSettings: ChatSettings
8182
apiConversationHistory: Anthropic.MessageParam[] = []
8283
clineMessages: ClineMessage[] = []
84+
private llmAccessController: LLMFileAccessController
8385
private askResponse?: ClineAskResponse
8486
private askResponseText?: string
8587
private askResponseImages?: string[]
@@ -123,6 +125,10 @@ export class Cline {
123125
images?: string[],
124126
historyItem?: HistoryItem,
125127
) {
128+
this.llmAccessController = new LLMFileAccessController(cwd)
129+
this.llmAccessController.initialize().catch((error) => {
130+
console.error("Failed to initialize LLMFileAccessController:", error)
131+
})
126132
this.providerRef = new WeakRef(provider)
127133
this.api = buildApiHandler(apiConfiguration)
128134
this.terminalManager = new TerminalManager()
@@ -748,6 +754,7 @@ export class Cline {
748754
// if the extension process were killed, then on restart the clineMessages might not be empty, so we need to set it to [] when we create a new Cline client (otherwise webview would show stale messages from previous session)
749755
this.clineMessages = []
750756
this.apiConversationHistory = []
757+
751758
await this.providerRef.deref()?.postStateToWebview()
752759

753760
await this.say("text", task, images)
@@ -1050,6 +1057,7 @@ export class Cline {
10501057
this.terminalManager.disposeAll()
10511058
this.urlContentFetcher.closeBrowser()
10521059
this.browserSession.closeBrowser()
1060+
this.llmAccessController.dispose()
10531061
await this.diffViewProvider.revertChanges() // need to await for when we want to make sure directories/files are reverted before re-starting the task from a checkpoint
10541062
}
10551063

src/services/llm-access-control/LLMFileAccessController.test.ts

Lines changed: 26 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -33,44 +33,44 @@ describe("LLMFileAccessController", () => {
3333

3434
describe("Default Patterns", () => {
3535
// it("should block access to common ignored files", async () => {
36-
// const results = await Promise.all([
36+
// const results = [
3737
// controller.validateAccess(".env"),
3838
// controller.validateAccess(".git/config"),
3939
// controller.validateAccess("node_modules/package.json"),
40-
// ])
40+
// ]
4141
// results.forEach((result) => result.should.be.false())
4242
// })
4343

4444
it("should allow access to regular files", async () => {
45-
const results = await Promise.all([
45+
const results = [
4646
controller.validateAccess("src/index.ts"),
4747
controller.validateAccess("README.md"),
4848
controller.validateAccess("package.json"),
49-
])
49+
]
5050
results.forEach((result) => result.should.be.true())
5151
})
5252
})
5353

5454
describe("Custom Patterns", () => {
5555
it("should block access to custom ignored patterns", async () => {
56-
const results = await Promise.all([
56+
const results = [
5757
controller.validateAccess("config.secret"),
5858
controller.validateAccess("private/data.txt"),
5959
controller.validateAccess("temp.json"),
6060
controller.validateAccess("nested/deep/file.secret"),
6161
controller.validateAccess("private/nested/deep/file.txt"),
62-
])
62+
]
6363
results.forEach((result) => result.should.be.false())
6464
})
6565

6666
it("should allow access to non-ignored files", async () => {
67-
const results = await Promise.all([
67+
const results = [
6868
controller.validateAccess("public/data.txt"),
6969
controller.validateAccess("config.json"),
7070
controller.validateAccess("src/temp/file.ts"),
7171
controller.validateAccess("nested/deep/file.txt"),
7272
controller.validateAccess("not-private/data.txt"),
73-
])
73+
]
7474
results.forEach((result) => result.should.be.true())
7575
})
7676

@@ -83,11 +83,11 @@ describe("LLMFileAccessController", () => {
8383
controller = new LLMFileAccessController(tempDir)
8484
await controller.initialize()
8585

86-
const results = await Promise.all([
86+
const results = [
8787
controller.validateAccess("data-123.json"), // Should be false (wildcard)
8888
controller.validateAccess("data.json"), // Should be true (doesn't match pattern)
8989
controller.validateAccess("script.tmp"), // Should be false (extension match)
90-
])
90+
]
9191

9292
results[0].should.be.false() // data-123.json
9393
results[1].should.be.true() // data.json
@@ -112,9 +112,8 @@ describe("LLMFileAccessController", () => {
112112
// )
113113

114114
// controller = new LLMFileAccessController(tempDir)
115-
// await controller.initialize()
116115

117-
// const results = await Promise.all([
116+
// const results = [
118117
// // Basic negation
119118
// controller.validateAccess("temp/file.txt"), // Should be false (in temp/)
120119
// controller.validateAccess("temp/allowed/file.txt"), // Should be true (negated)
@@ -130,7 +129,7 @@ describe("LLMFileAccessController", () => {
130129
// controller.validateAccess("assets/logo.png"), // Should be false (in assets/)
131130
// controller.validateAccess("assets/public/logo.png"), // Should be true (negated and matches *.png)
132131
// controller.validateAccess("assets/public/data.json"), // Should be true (in negated public/)
133-
// ])
132+
// ]
134133

135134
// results[0].should.be.false() // temp/file.txt
136135
// results[1].should.be.true() // temp/allowed/file.txt
@@ -154,7 +153,7 @@ describe("LLMFileAccessController", () => {
154153
controller = new LLMFileAccessController(tempDir)
155154
await controller.initialize()
156155

157-
const result = await controller.validateAccess("test.secret")
156+
const result = controller.validateAccess("test.secret")
158157
result.should.be.false()
159158
})
160159
})
@@ -163,55 +162,55 @@ describe("LLMFileAccessController", () => {
163162
it("should handle absolute paths and match ignore patterns", async () => {
164163
// Test absolute path that should be allowed
165164
const allowedPath = path.join(tempDir, "src/file.ts")
166-
const allowedResult = await controller.validateAccess(allowedPath)
165+
const allowedResult = controller.validateAccess(allowedPath)
167166
allowedResult.should.be.true()
168167

169168
// Test absolute path that matches an ignore pattern (*.secret)
170169
const ignoredPath = path.join(tempDir, "config.secret")
171-
const ignoredResult = await controller.validateAccess(ignoredPath)
170+
const ignoredResult = controller.validateAccess(ignoredPath)
172171
ignoredResult.should.be.false()
173172

174173
// Test absolute path in ignored directory (private/)
175174
const ignoredDirPath = path.join(tempDir, "private/data.txt")
176-
const ignoredDirResult = await controller.validateAccess(ignoredDirPath)
175+
const ignoredDirResult = controller.validateAccess(ignoredDirPath)
177176
ignoredDirResult.should.be.false()
178177
})
179178

180179
it("should handle relative paths and match ignore patterns", async () => {
181180
// Test relative path that should be allowed
182-
const allowedResult = await controller.validateAccess("./src/file.ts")
181+
const allowedResult = controller.validateAccess("./src/file.ts")
183182
allowedResult.should.be.true()
184183

185184
// Test relative path that matches an ignore pattern (*.secret)
186-
const ignoredResult = await controller.validateAccess("./config.secret")
185+
const ignoredResult = controller.validateAccess("./config.secret")
187186
ignoredResult.should.be.false()
188187

189188
// Test relative path in ignored directory (private/)
190-
const ignoredDirResult = await controller.validateAccess("./private/data.txt")
189+
const ignoredDirResult = controller.validateAccess("./private/data.txt")
191190
ignoredDirResult.should.be.false()
192191
})
193192

194193
it("should normalize paths with backslashes", async () => {
195-
const result = await controller.validateAccess("src\\file.ts")
194+
const result = controller.validateAccess("src\\file.ts")
196195
result.should.be.true()
197196
})
198197

199198
it("should handle paths outside cwd", async () => {
200199
// Create a path that points to parent directory of cwd
201200
const outsidePath = path.join(path.dirname(tempDir), "outside.txt")
202-
const result = await controller.validateAccess(outsidePath)
201+
const result = controller.validateAccess(outsidePath)
203202

204203
// Should return false for security since path is outside cwd
205204
result.should.be.false()
206205

207206
// Test with a deeply nested path outside cwd
208207
const deepOutsidePath = path.join(path.dirname(tempDir), "deep", "nested", "outside.secret")
209-
const deepResult = await controller.validateAccess(deepOutsidePath)
208+
const deepResult = controller.validateAccess(deepOutsidePath)
210209
deepResult.should.be.false()
211210

212211
// Test with a path that tries to escape using ../
213212
const escapeAttemptPath = path.join(tempDir, "..", "escape-attempt.txt")
214-
const escapeResult = await controller.validateAccess(escapeAttemptPath)
213+
const escapeResult = controller.validateAccess(escapeAttemptPath)
215214
escapeResult.should.be.false()
216215
})
217216
})
@@ -228,7 +227,7 @@ describe("LLMFileAccessController", () => {
228227
describe("Error Handling", () => {
229228
it("should handle invalid paths", async () => {
230229
// Test with an invalid path containing null byte
231-
const result = await controller.validateAccess("\0invalid")
230+
const result = controller.validateAccess("\0invalid")
232231
result.should.be.true()
233232
})
234233

@@ -240,7 +239,7 @@ describe("LLMFileAccessController", () => {
240239
try {
241240
const controller = new LLMFileAccessController(emptyDir)
242241
await controller.initialize()
243-
const result = await controller.validateAccess("file.txt")
242+
const result = controller.validateAccess("file.txt")
244243
result.should.be.true()
245244
} finally {
246245
await fs.rm(emptyDir, { recursive: true, force: true })
@@ -253,7 +252,7 @@ describe("LLMFileAccessController", () => {
253252
controller = new LLMFileAccessController(tempDir)
254253
await controller.initialize()
255254

256-
const result = await controller.validateAccess("regular-file.txt")
255+
const result = controller.validateAccess("regular-file.txt")
257256
result.should.be.true()
258257
})
259258
})

src/services/llm-access-control/LLMFileAccessController.ts

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import path from "path"
22
import { fileExistsAtPath } from "../../utils/fs"
33
import fs from "fs/promises"
44
import ignore, { Ignore } from "ignore"
5+
import * as vscode from "vscode"
56

67
/**
78
* Controls LLM access to files by enforcing ignore patterns.
@@ -11,6 +12,8 @@ import ignore, { Ignore } from "ignore"
1112
export class LLMFileAccessController {
1213
private cwd: string
1314
private ignoreInstance: Ignore
15+
private fileWatcher: vscode.FileSystemWatcher | null
16+
private disposables: vscode.Disposable[] = []
1417

1518
/**
1619
* Default patterns that are always ignored for security
@@ -20,26 +23,58 @@ export class LLMFileAccessController {
2023
constructor(cwd: string) {
2124
this.cwd = cwd
2225
this.ignoreInstance = ignore()
23-
24-
// Add default patterns immediately
2526
this.ignoreInstance.add(LLMFileAccessController.DEFAULT_PATTERNS)
27+
this.fileWatcher = null
28+
29+
// Set up file watcher for .clineignore
30+
this.setupFileWatcher()
2631
}
2732

2833
/**
2934
* Initialize the controller by loading custom patterns
30-
* This must be called and awaited before using the controller
35+
* Must be called after construction and before using the controller
3136
*/
3237
async initialize(): Promise<void> {
3338
await this.loadCustomPatterns()
3439
}
3540

41+
/**
42+
* Set up the file watcher for .clineignore changes
43+
*/
44+
private setupFileWatcher(): void {
45+
const clineignorePattern = new vscode.RelativePattern(this.cwd, ".clineignore")
46+
this.fileWatcher = vscode.workspace.createFileSystemWatcher(clineignorePattern)
47+
48+
// Watch for changes and updates
49+
this.disposables.push(
50+
this.fileWatcher.onDidChange(() => {
51+
this.loadCustomPatterns().catch((error) => {
52+
console.error("Failed to load updated .clineignore patterns:", error)
53+
})
54+
}),
55+
this.fileWatcher.onDidCreate(() => {
56+
this.loadCustomPatterns().catch((error) => {
57+
console.error("Failed to load new .clineignore patterns:", error)
58+
})
59+
}),
60+
this.fileWatcher.onDidDelete(() => {
61+
this.resetToDefaultPatterns()
62+
}),
63+
)
64+
65+
// Add fileWatcher itself to disposables
66+
this.disposables.push(this.fileWatcher)
67+
}
68+
3669
/**
3770
* Load custom patterns from .clineignore if it exists
3871
*/
3972
private async loadCustomPatterns(): Promise<void> {
4073
try {
4174
const ignorePath = path.join(this.cwd, ".clineignore")
4275
if (await fileExistsAtPath(ignorePath)) {
76+
// Reset ignore instance to prevent duplicate patterns
77+
this.resetToDefaultPatterns()
4378
const content = await fs.readFile(ignorePath, "utf8")
4479
const customPatterns = content
4580
.split("\n")
@@ -49,11 +84,18 @@ export class LLMFileAccessController {
4984
this.ignoreInstance.add(customPatterns)
5085
}
5186
} catch (error) {
52-
console.error("Failed to load .clineignore:", error)
5387
// Continue with default patterns
5488
}
5589
}
5690

91+
/**
92+
* Reset ignore patterns to defaults
93+
*/
94+
private resetToDefaultPatterns(): void {
95+
this.ignoreInstance = ignore()
96+
this.ignoreInstance.add(LLMFileAccessController.DEFAULT_PATTERNS)
97+
}
98+
5799
/**
58100
* Check if a file should be accessible to the LLM
59101
* @param filePath - Path to check (relative to cwd)
@@ -97,4 +139,13 @@ export class LLMFileAccessController {
97139
return [] // Fail closed for security
98140
}
99141
}
142+
143+
/**
144+
* Clean up resources when the controller is no longer needed
145+
*/
146+
dispose(): void {
147+
this.disposables.forEach((d) => d.dispose())
148+
this.disposables = []
149+
this.fileWatcher = null
150+
}
100151
}

0 commit comments

Comments
 (0)