Throw FileRestrictionErrors sooner

This commit is contained in:
Matt Rubens
2025-01-26 02:43:40 -05:00
parent 813c607440
commit 5d20477785
4 changed files with 113 additions and 36 deletions

View File

@@ -0,0 +1,5 @@
---
"roo-cline": patch
---
Throw errors sooner when a mode tries to write a restricted file

View File

@@ -1144,25 +1144,22 @@ export class Cline {
await this.browserSession.closeBrowser() await this.browserSession.closeBrowser()
} }
// Only validate complete tool uses // Validate tool use before execution
if (!block.partial) { const { mode, customModes } = (await this.providerRef.deref()?.getState()) ?? {}
const { mode } = (await this.providerRef.deref()?.getState()) ?? {} try {
const { customModes } = (await this.providerRef.deref()?.getState()) ?? {} validateToolUse(
try { block.name as ToolName,
validateToolUse( mode ?? defaultModeSlug,
block.name as ToolName, customModes ?? [],
mode ?? defaultModeSlug, {
customModes ?? [], apply_diff: this.diffEnabled,
{ },
apply_diff: this.diffEnabled, block.params,
}, )
block.params, } catch (error) {
) this.consecutiveMistakeCount++
} catch (error) { pushToolResult(formatResponse.toolError(error.message))
this.consecutiveMistakeCount++ break
pushToolResult(formatResponse.toolError(error.message))
break
}
} }
switch (block.name) { switch (block.name) {

View File

@@ -31,12 +31,14 @@ describe("isToolAllowedForMode", () => {
// Test markdown editor mode // Test markdown editor mode
const mdResult = isToolAllowedForMode("write_to_file", "markdown-editor", customModes, undefined, { const mdResult = isToolAllowedForMode("write_to_file", "markdown-editor", customModes, undefined, {
path: "test.md", path: "test.md",
content: "# Test",
}) })
expect(mdResult).toBe(true) expect(mdResult).toBe(true)
// Test CSS editor mode // Test CSS editor mode
const cssResult = isToolAllowedForMode("write_to_file", "css-editor", customModes, undefined, { const cssResult = isToolAllowedForMode("write_to_file", "css-editor", customModes, undefined, {
path: "styles.css", path: "styles.css",
content: ".test { color: red; }",
}) })
expect(cssResult).toBe(true) expect(cssResult).toBe(true)
}) })
@@ -44,41 +46,83 @@ describe("isToolAllowedForMode", () => {
it("rejects editing non-matching files", () => { it("rejects editing non-matching files", () => {
// Test markdown editor mode with non-markdown file // Test markdown editor mode with non-markdown file
expect(() => expect(() =>
isToolAllowedForMode("write_to_file", "markdown-editor", customModes, undefined, { path: "test.js" }), isToolAllowedForMode("write_to_file", "markdown-editor", customModes, undefined, {
path: "test.js",
content: "console.log('test')",
}),
).toThrow(FileRestrictionError) ).toThrow(FileRestrictionError)
expect(() => expect(() =>
isToolAllowedForMode("write_to_file", "markdown-editor", customModes, undefined, { path: "test.js" }), isToolAllowedForMode("write_to_file", "markdown-editor", customModes, undefined, {
path: "test.js",
content: "console.log('test')",
}),
).toThrow(/\\.md\$/) ).toThrow(/\\.md\$/)
// Test CSS editor mode with non-CSS file // Test CSS editor mode with non-CSS file
expect(() => expect(() =>
isToolAllowedForMode("write_to_file", "css-editor", customModes, undefined, { path: "test.js" }), isToolAllowedForMode("write_to_file", "css-editor", customModes, undefined, {
path: "test.js",
content: "console.log('test')",
}),
).toThrow(FileRestrictionError) ).toThrow(FileRestrictionError)
expect(() => expect(() =>
isToolAllowedForMode("write_to_file", "css-editor", customModes, undefined, { path: "test.js" }), isToolAllowedForMode("write_to_file", "css-editor", customModes, undefined, {
path: "test.js",
content: "console.log('test')",
}),
).toThrow(/\\.css\$/) ).toThrow(/\\.css\$/)
}) })
it("handles partial streaming cases (path only, no content/diff)", () => {
// Should allow path-only for matching files (no validation yet since content/diff not provided)
expect(
isToolAllowedForMode("write_to_file", "markdown-editor", customModes, undefined, {
path: "test.js",
}),
).toBe(true)
expect(
isToolAllowedForMode("apply_diff", "markdown-editor", customModes, undefined, {
path: "test.js",
}),
).toBe(true)
// Should allow path-only for ask mode too
expect(
isToolAllowedForMode("write_to_file", "ask", [], undefined, {
path: "test.js",
}),
).toBe(true)
})
it("applies restrictions to both write_to_file and apply_diff", () => { it("applies restrictions to both write_to_file and apply_diff", () => {
// Test write_to_file // Test write_to_file
const writeResult = isToolAllowedForMode("write_to_file", "markdown-editor", customModes, undefined, { const writeResult = isToolAllowedForMode("write_to_file", "markdown-editor", customModes, undefined, {
path: "test.md", path: "test.md",
content: "# Test",
}) })
expect(writeResult).toBe(true) expect(writeResult).toBe(true)
// Test apply_diff // Test apply_diff
const diffResult = isToolAllowedForMode("apply_diff", "markdown-editor", customModes, undefined, { const diffResult = isToolAllowedForMode("apply_diff", "markdown-editor", customModes, undefined, {
path: "test.md", path: "test.md",
diff: "- old\n+ new",
}) })
expect(diffResult).toBe(true) expect(diffResult).toBe(true)
// Test both with non-matching file // Test both with non-matching file
expect(() => expect(() =>
isToolAllowedForMode("write_to_file", "markdown-editor", customModes, undefined, { path: "test.js" }), isToolAllowedForMode("write_to_file", "markdown-editor", customModes, undefined, {
path: "test.js",
content: "console.log('test')",
}),
).toThrow(FileRestrictionError) ).toThrow(FileRestrictionError)
expect(() => expect(() =>
isToolAllowedForMode("apply_diff", "markdown-editor", customModes, undefined, { path: "test.js" }), isToolAllowedForMode("apply_diff", "markdown-editor", customModes, undefined, {
path: "test.js",
diff: "- old\n+ new",
}),
).toThrow(FileRestrictionError) ).toThrow(FileRestrictionError)
}) })
@@ -100,11 +144,13 @@ describe("isToolAllowedForMode", () => {
expect(() => expect(() =>
isToolAllowedForMode("write_to_file", "docs-editor", customModesWithDescription, undefined, { isToolAllowedForMode("write_to_file", "docs-editor", customModesWithDescription, undefined, {
path: "test.js", path: "test.js",
content: "console.log('test')",
}), }),
).toThrow(FileRestrictionError) ).toThrow(FileRestrictionError)
expect(() => expect(() =>
isToolAllowedForMode("write_to_file", "docs-editor", customModesWithDescription, undefined, { isToolAllowedForMode("write_to_file", "docs-editor", customModesWithDescription, undefined, {
path: "test.js", path: "test.js",
content: "console.log('test')",
}), }),
).toThrow(/Documentation files only/) ).toThrow(/Documentation files only/)
@@ -112,11 +158,13 @@ describe("isToolAllowedForMode", () => {
expect(() => expect(() =>
isToolAllowedForMode("apply_diff", "docs-editor", customModesWithDescription, undefined, { isToolAllowedForMode("apply_diff", "docs-editor", customModesWithDescription, undefined, {
path: "test.js", path: "test.js",
diff: "- old\n+ new",
}), }),
).toThrow(FileRestrictionError) ).toThrow(FileRestrictionError)
expect(() => expect(() =>
isToolAllowedForMode("apply_diff", "docs-editor", customModesWithDescription, undefined, { isToolAllowedForMode("apply_diff", "docs-editor", customModesWithDescription, undefined, {
path: "test.js", path: "test.js",
diff: "- old\n+ new",
}), }),
).toThrow(/Documentation files only/) ).toThrow(/Documentation files only/)
@@ -124,30 +172,55 @@ describe("isToolAllowedForMode", () => {
expect( expect(
isToolAllowedForMode("write_to_file", "docs-editor", customModesWithDescription, undefined, { isToolAllowedForMode("write_to_file", "docs-editor", customModesWithDescription, undefined, {
path: "test.md", path: "test.md",
content: "# Test",
}), }),
).toBe(true) ).toBe(true)
expect( expect(
isToolAllowedForMode("write_to_file", "docs-editor", customModesWithDescription, undefined, { isToolAllowedForMode("write_to_file", "docs-editor", customModesWithDescription, undefined, {
path: "test.txt", path: "test.txt",
content: "Test content",
}),
).toBe(true)
// Test partial streaming cases
expect(
isToolAllowedForMode("write_to_file", "docs-editor", customModesWithDescription, undefined, {
path: "test.js",
}), }),
).toBe(true) ).toBe(true)
}) })
it("allows ask mode to edit markdown files only", () => { it("allows ask mode to edit markdown files only", () => {
// Should allow editing markdown files // Should allow editing markdown files
expect(isToolAllowedForMode("write_to_file", "ask", [], undefined, { path: "test.md" })).toBe(true) expect(
isToolAllowedForMode("write_to_file", "ask", [], undefined, {
path: "test.md",
content: "# Test",
}),
).toBe(true)
// Should allow applying diffs to markdown files // Should allow applying diffs to markdown files
expect(isToolAllowedForMode("apply_diff", "ask", [], undefined, { path: "readme.md" })).toBe(true) expect(
isToolAllowedForMode("apply_diff", "ask", [], undefined, {
path: "readme.md",
diff: "- old\n+ new",
}),
).toBe(true)
// Should reject non-markdown files // Should reject non-markdown files
expect(() => isToolAllowedForMode("write_to_file", "ask", [], undefined, { path: "test.js" })).toThrow( expect(() =>
FileRestrictionError, isToolAllowedForMode("write_to_file", "ask", [], undefined, {
) path: "test.js",
expect(() => isToolAllowedForMode("write_to_file", "ask", [], undefined, { path: "test.js" })).toThrow( content: "console.log('test')",
/Markdown files only/, }),
) ).toThrow(FileRestrictionError)
expect(() =>
isToolAllowedForMode("write_to_file", "ask", [], undefined, {
path: "test.js",
content: "console.log('test')",
}),
).toThrow(/Markdown files only/)
// Should maintain read capabilities // Should maintain read capabilities
expect(isToolAllowedForMode("read_file", "ask", [])).toBe(true) expect(isToolAllowedForMode("read_file", "ask", [])).toBe(true)

View File

@@ -196,11 +196,13 @@ export function isToolAllowedForMode(
// For the edit group, check file regex if specified // For the edit group, check file regex if specified
if (groupName === "edit" && options.fileRegex) { if (groupName === "edit" && options.fileRegex) {
const filePath = toolParams?.path const filePath = toolParams?.path
// Only validate regex if a path is provided if (
if (filePath && !doesFileMatchRegex(filePath, options.fileRegex)) { filePath &&
(toolParams.diff || toolParams.content) &&
!doesFileMatchRegex(filePath, options.fileRegex)
) {
throw new FileRestrictionError(mode.name, options.fileRegex, options.description, filePath) throw new FileRestrictionError(mode.name, options.fileRegex, options.description, filePath)
} }
return true
} }
return true return true