@@ -94,56 +94,99 @@ describe("isToolAllowedForMode", () => {
9494 } )
9595
9696 it ( "handles partial streaming cases (path only, no content/diff)" , ( ) => {
97- // Should allow path-only for matching files (no validation yet since content/diff not provided)
98- expect (
97+ // Should enforce file restrictions even when only path is provided (streaming scenario)
98+ // This prevents bypassing restrictions when parameters arrive separately
99+
100+ // Non-matching files should be rejected immediately when path is provided
101+ expect ( ( ) =>
99102 isToolAllowedForMode ( "write_to_file" , "markdown-editor" , customModes , undefined , {
100103 path : "test.js" ,
101104 } ) ,
105+ ) . toThrow ( FileRestrictionError )
106+
107+ expect ( ( ) =>
108+ isToolAllowedForMode ( "apply_diff" , "markdown-editor" , customModes , undefined , {
109+ path : "test.js" ,
110+ } ) ,
111+ ) . toThrow ( FileRestrictionError )
112+
113+ // Matching files should be allowed even with path-only
114+ expect (
115+ isToolAllowedForMode ( "write_to_file" , "markdown-editor" , customModes , undefined , {
116+ path : "test.md" ,
117+ } ) ,
102118 ) . toBe ( true )
103119
104120 expect (
105121 isToolAllowedForMode ( "apply_diff" , "markdown-editor" , customModes , undefined , {
106- path : "test.js " ,
122+ path : "test.md " ,
107123 } ) ,
108124 ) . toBe ( true )
109125
110- // Should allow path-only for architect mode too
126+ // Architect mode (built-in) allows markdown files
111127 expect (
112128 isToolAllowedForMode ( "write_to_file" , "architect" , [ ] , undefined , {
113- path : "test.js " ,
129+ path : "test.md " ,
114130 } ) ,
115131 ) . toBe ( true )
116132 } )
117133
118- it ( "applies restrictions to both write_to_file and apply_diff" , ( ) => {
119- // Test write_to_file
120- const writeResult = isToolAllowedForMode ( "write_to_file" , "markdown-editor" , customModes , undefined , {
121- path : "test.md" ,
122- content : "# Test" ,
134+ it ( "applies restrictions to all edit tools" , ( ) => {
135+ // Test all edit tools with matching files
136+ const editTools = [ "write_to_file" , "apply_diff" , "insert_content" , "search_and_replace" ]
137+
138+ editTools . forEach ( ( tool ) => {
139+ // Matching file should be allowed
140+ const params : any = { path : "test.md" }
141+ if ( tool === "write_to_file" ) params . content = "# Test"
142+ if ( tool === "apply_diff" ) params . diff = "- old\n+ new"
143+ if ( tool === "insert_content" ) params . content = "new line"
144+ if ( tool === "search_and_replace" ) {
145+ params . search = "old"
146+ params . replace = "new"
147+ }
148+
149+ expect ( isToolAllowedForMode ( tool as any , "markdown-editor" , customModes , undefined , params ) ) . toBe ( true )
150+
151+ // Non-matching file should be rejected
152+ params . path = "test.js"
153+ expect ( ( ) =>
154+ isToolAllowedForMode ( tool as any , "markdown-editor" , customModes , undefined , params ) ,
155+ ) . toThrow ( FileRestrictionError )
123156 } )
124- expect ( writeResult ) . toBe ( true )
157+ } )
125158
126- // Test apply_diff
127- const diffResult = isToolAllowedForMode ( "apply_diff" , "markdown-editor" , customModes , undefined , {
128- path : "test.md" ,
129- diff : "- old\n+ new" ,
130- } )
131- expect ( diffResult ) . toBe ( true )
159+ it ( "prevents bypassing restrictions via parameter streaming" , ( ) => {
160+ // This test specifically addresses the bug where apply_diff could bypass
161+ // file restrictions when path arrives before diff content
132162
133- // Test both with non-matching file
163+ // Step 1: Path arrives first (should be rejected for non-matching files)
134164 expect ( ( ) =>
135- isToolAllowedForMode ( "write_to_file" , "markdown-editor" , customModes , undefined , {
136- path : "test.js" ,
137- content : "console.log('test')" ,
165+ isToolAllowedForMode ( "apply_diff" , "markdown-editor" , customModes , undefined , {
166+ path : "test.js" , // Non-matching file
138167 } ) ,
139168 ) . toThrow ( FileRestrictionError )
140169
170+ // Step 2: Even when diff arrives later, it should still be rejected
141171 expect ( ( ) =>
142172 isToolAllowedForMode ( "apply_diff" , "markdown-editor" , customModes , undefined , {
143173 path : "test.js" ,
144174 diff : "- old\n+ new" ,
145175 } ) ,
146176 ) . toThrow ( FileRestrictionError )
177+
178+ // Same for other edit tools
179+ expect ( ( ) =>
180+ isToolAllowedForMode ( "insert_content" , "markdown-editor" , customModes , undefined , {
181+ path : "test.js" ,
182+ } ) ,
183+ ) . toThrow ( FileRestrictionError )
184+
185+ expect ( ( ) =>
186+ isToolAllowedForMode ( "search_and_replace" , "markdown-editor" , customModes , undefined , {
187+ path : "test.js" ,
188+ } ) ,
189+ ) . toThrow ( FileRestrictionError )
147190 } )
148191
149192 it ( "uses description in file restriction error for custom modes" , ( ) => {
@@ -203,11 +246,18 @@ describe("isToolAllowedForMode", () => {
203246 } ) ,
204247 ) . toBe ( true )
205248
206- // Test partial streaming cases
207- expect (
249+ // Test partial streaming cases - should now reject non-matching files immediately
250+ expect ( ( ) =>
208251 isToolAllowedForMode ( "write_to_file" , "docs-editor" , customModesWithDescription , undefined , {
209252 path : "test.js" ,
210253 } ) ,
254+ ) . toThrow ( FileRestrictionError )
255+
256+ // But should allow matching files even with path-only
257+ expect (
258+ isToolAllowedForMode ( "write_to_file" , "docs-editor" , customModesWithDescription , undefined , {
259+ path : "test.md" ,
260+ } ) ,
211261 ) . toBe ( true )
212262 } )
213263
@@ -247,6 +297,72 @@ describe("isToolAllowedForMode", () => {
247297 expect ( isToolAllowedForMode ( "browser_action" , "architect" , [ ] ) ) . toBe ( true )
248298 expect ( isToolAllowedForMode ( "use_mcp_tool" , "architect" , [ ] ) ) . toBe ( true )
249299 } )
300+ it ( "validates paths in multi-file apply_diff args parameter" , ( ) => {
301+ // Test multi-file apply_diff with XML args containing non-matching files
302+ const argsWithNonMatchingFile = `<args>
303+ <file>
304+ <path>test.js</path>
305+ <diff>
306+ <content>- old
307+ + new</content>
308+ </diff>
309+ </file>
310+ </args>`
311+
312+ expect ( ( ) =>
313+ isToolAllowedForMode ( "apply_diff" , "markdown-editor" , customModes , undefined , {
314+ args : argsWithNonMatchingFile ,
315+ } ) ,
316+ ) . toThrow ( FileRestrictionError )
317+
318+ // Test with multiple files, one non-matching
319+ const argsWithMixedFiles = `<args>
320+ <file>
321+ <path>doc.md</path>
322+ <diff>
323+ <content>- old
324+ + new</content>
325+ </diff>
326+ </file>
327+ <file>
328+ <path>script.js</path>
329+ <diff>
330+ <content>- old
331+ + new</content>
332+ </diff>
333+ </file>
334+ </args>`
335+
336+ expect ( ( ) =>
337+ isToolAllowedForMode ( "apply_diff" , "markdown-editor" , customModes , undefined , {
338+ args : argsWithMixedFiles ,
339+ } ) ,
340+ ) . toThrow ( FileRestrictionError )
341+
342+ // Test with only matching files (should pass)
343+ const argsWithMatchingFiles = `<args>
344+ <file>
345+ <path>doc1.md</path>
346+ <diff>
347+ <content>- old
348+ + new</content>
349+ </diff>
350+ </file>
351+ <file>
352+ <path>doc2.md</path>
353+ <diff>
354+ <content>- old
355+ + new</content>
356+ </diff>
357+ </file>
358+ </args>`
359+
360+ expect (
361+ isToolAllowedForMode ( "apply_diff" , "markdown-editor" , customModes , undefined , {
362+ args : argsWithMatchingFiles ,
363+ } ) ,
364+ ) . toBe ( true )
365+ } )
250366 } )
251367
252368 it ( "handles non-existent modes" , ( ) => {
0 commit comments