@@ -44,26 +44,31 @@ describe("pathEscapePlugin", () => {
4444 [ "source" , "copy" , "/etc/passwd" ] ,
4545 [ "filename" , "write_file" , "../secret.txt" ] ,
4646 ] as const ) {
47- test ( `blocks escape via ${ key } key ( ${ tool } ) ` , async ( ) => {
47+ test ( `leaves an outside ${ key } unchanged; the gate denies ${ tool } ` , async ( ) => {
4848 const plugin = pathEscapePlugin ( "/project" ) ;
49- const handler = pluginHandler ( plugin , okHandler ) ;
49+ const handler = pluginHandler ( plugin , echoArgsHandler ) ;
5050 const result = await handler (
5151 makeToolCall ( tool , { [ key ] : value } ) ,
5252 neverAbort ( ) ,
5353 ) ;
54- expect ( result . isError ) . toBe ( true ) ;
54+ expect ( result . isError ) . not . toBe ( true ) ;
55+ const args = JSON . parse ( String ( result . content ) ) as Record < string , string > ;
56+ expect ( args [ key ] ) . toBe ( value ) ;
57+ expect (
58+ pathEscapeBlockReason ( { [ key ] : value } , "/project" , ( ) => [ ] , tool ) ,
59+ ) . toMatch ( / e s c a p e s w o r k i n g d i r e c t o r y / ) ;
5560 } ) ;
5661 }
5762
58- test ( "the block message tells the operator the path escapes the working directory" , async ( ) => {
59- const plugin = pathEscapePlugin ( "/project" ) ;
60- const handler = pluginHandler ( plugin , okHandler ) ;
61- const result = await handler (
62- makeToolCall ( "read_file" , { path : "../secret.txt" } ) ,
63- neverAbort ( ) ,
64- ) ;
65- expect ( result . isError ) . toBe ( true ) ;
66- expect ( result . content ) . toMatch ( / e s c a p e s w o r k i n g d i r e c t o r y / ) ;
63+ test ( "the gate denial tells the operator the path escapes the working directory" , ( ) => {
64+ expect (
65+ pathEscapeBlockReason (
66+ { path : "../secret.txt" } ,
67+ "/project" ,
68+ ( ) => [ ] ,
69+ "read_file" ,
70+ ) ,
71+ ) . toMatch ( / e s c a p e s w o r k i n g d i r e c t o r y / ) ;
6772 } ) ;
6873
6974 test ( "allows cwd path itself" , async ( ) => {
@@ -114,8 +119,17 @@ describe("pathEscapePlugin", () => {
114119 makeToolCall ( "read_file" , { path : "../other-repo/README.md" } ) ,
115120 neverAbort ( ) ,
116121 ) ;
117- expect ( blocked . isError ) . toBe ( true ) ;
118- expect ( blocked . content ) . toMatch ( / e s c a p e s w o r k i n g d i r e c t o r y / ) ;
122+ expect ( blocked . isError ) . not . toBe ( true ) ;
123+ const blockedArgs = JSON . parse ( String ( blocked . content ) ) as { path : string } ;
124+ expect ( blockedArgs . path ) . toBe ( "../other-repo/README.md" ) ;
125+ expect (
126+ pathEscapeBlockReason (
127+ { path : "../other-repo/README.md" } ,
128+ "/project" ,
129+ ( ) => [ ] ,
130+ "read_file" ,
131+ ) ,
132+ ) . toMatch ( / e s c a p e s w o r k i n g d i r e c t o r y / ) ;
119133
120134 allow = true ;
121135 const allowed = await handler (
@@ -204,15 +218,20 @@ describe("pathEscapePlugin", () => {
204218 return { next, seen : ( ) => seen } ;
205219 } ;
206220
207- test ( "blocks escape in a nested object under a path-like key" , async ( ) => {
221+ test ( "passes a nested escape through; the gate denies it" , async ( ) => {
222+ const args = { options : { path : "../secret.txt" } } ;
208223 const plugin = pathEscapePlugin ( "/project" ) ;
209- const handler = pluginHandler ( plugin , okHandler ) ;
224+ const { next, seen } = captureNext ( ) ;
225+ const handler = pluginHandler ( plugin , next ) ;
210226 const result = await handler (
211- makeToolCall ( "read_file" , { options : { path : "../secret.txt" } } ) ,
227+ makeToolCall ( "read_file" , args ) ,
212228 neverAbort ( ) ,
213229 ) ;
214- expect ( result . isError ) . toBe ( true ) ;
215- expect ( result . content ) . toMatch ( / e s c a p e s w o r k i n g d i r e c t o r y / ) ;
230+ expect ( result . isError ) . not . toBe ( true ) ;
231+ expect ( seen ( ) ) . toEqual ( args ) ;
232+ expect (
233+ pathEscapeBlockReason ( args , "/project" , ( ) => [ ] , "read_file" ) ,
234+ ) . toMatch ( / e s c a p e s w o r k i n g d i r e c t o r y / ) ;
216235 } ) ;
217236
218237 test ( "resolves nested in-bounds paths instead of passing them through" , async ( ) => {
@@ -229,28 +248,38 @@ describe("pathEscapePlugin", () => {
229248 } ) ;
230249 } ) ;
231250
232- test ( "blocks escape via the filepath spelling" , async ( ) => {
251+ test ( "passes an escape via the filepath spelling; the gate denies it" , async ( ) => {
252+ const args = { filepath : "../secret.txt" } ;
233253 const plugin = pathEscapePlugin ( "/project" ) ;
234- const handler = pluginHandler ( plugin , okHandler ) ;
254+ const { next, seen } = captureNext ( ) ;
255+ const handler = pluginHandler ( plugin , next ) ;
235256 const result = await handler (
236- makeToolCall ( "read_file" , { filepath : "../secret.txt" } ) ,
257+ makeToolCall ( "read_file" , args ) ,
237258 neverAbort ( ) ,
238259 ) ;
239- expect ( result . isError ) . toBe ( true ) ;
240- expect ( result . content ) . toMatch ( / e s c a p e s w o r k i n g d i r e c t o r y / ) ;
260+ expect ( result . isError ) . not . toBe ( true ) ;
261+ expect ( seen ( ) ) . toEqual ( args ) ;
262+ expect (
263+ pathEscapeBlockReason ( args , "/project" , ( ) => [ ] , "read_file" ) ,
264+ ) . toMatch ( / e s c a p e s w o r k i n g d i r e c t o r y / ) ;
241265 } ) ;
242266
243- test ( "blocks escape in a string array under a path-like key" , async ( ) => {
267+ test ( "passes an escape in a string array; the gate denies it" , async ( ) => {
268+ const args = { paths : [ "src/index.ts" , "../secret.txt" ] } ;
244269 const plugin = pathEscapePlugin ( "/project" ) ;
245- const handler = pluginHandler ( plugin , okHandler ) ;
270+ const { next, seen } = captureNext ( ) ;
271+ const handler = pluginHandler ( plugin , next ) ;
246272 const result = await handler (
247- makeToolCall ( "read_file" , {
248- paths : [ "src/index.ts" , "../secret.txt" ] ,
249- } ) ,
273+ makeToolCall ( "read_file" , args ) ,
250274 neverAbort ( ) ,
251275 ) ;
252- expect ( result . isError ) . toBe ( true ) ;
253- expect ( result . content ) . toMatch ( / e s c a p e s w o r k i n g d i r e c t o r y / ) ;
276+ expect ( result . isError ) . not . toBe ( true ) ;
277+ expect ( seen ( ) ) . toEqual ( {
278+ paths : [ "/project/src/index.ts" , "../secret.txt" ] ,
279+ } ) ;
280+ expect (
281+ pathEscapeBlockReason ( args , "/project" , ( ) => [ ] , "read_file" ) ,
282+ ) . toMatch ( / e s c a p e s w o r k i n g d i r e c t o r y / ) ;
254283 } ) ;
255284
256285 test ( "pathEscapeBlockReason agrees with execution time on nested escapes" , async ( ) => {
@@ -302,14 +331,15 @@ describe("pathEscapePlugin", () => {
302331
303332 test ( "FILE_PATH and file-path match case- and separator-insensitively" , async ( ) => {
304333 const plugin = pathEscapePlugin ( "/project" ) ;
305- const handler = pluginHandler ( plugin , okHandler ) ;
334+ const { next, seen } = captureNext ( ) ;
335+ const handler = pluginHandler ( plugin , next ) ;
306336 for ( const key of [ "FILE_PATH" , "file-path" ] ) {
307337 const result = await handler (
308338 makeToolCall ( "read_file" , { [ key ] : "../secret.txt" } ) ,
309339 neverAbort ( ) ,
310340 ) ;
311- expect ( result . isError ) . toBe ( true ) ;
312- expect ( result . content ) . toMatch ( / e s c a p e s w o r k i n g d i r e c t o r y / ) ;
341+ expect ( result . isError ) . not . toBe ( true ) ;
342+ expect ( seen ( ) ) . toEqual ( { [ key ] : "../secret.txt" } ) ;
313343 expect (
314344 pathEscapeBlockReason (
315345 { [ key ] : "../secret.txt" } ,
@@ -529,13 +559,14 @@ describe("pathEscapePlugin", () => {
529559 const plugin = pathEscapePlugin ( cwd , ( ) => [ ] , {
530560 trustedPluginRoots : ( ) => [ pluginDir ] ,
531561 } ) ;
532- const handler = pluginHandler ( plugin , okHandler ) ;
562+ const handler = pluginHandler ( plugin , echoArgsHandler ) ;
533563 const result = await handler (
534564 makeToolCall ( "write_file" , { path : target , content : "x" } ) ,
535565 neverAbort ( ) ,
536566 ) ;
537- expect ( result . isError ) . toBe ( true ) ;
538- expect ( result . content ) . toMatch ( / e s c a p e s w o r k i n g d i r e c t o r y / ) ;
567+ expect ( result . isError ) . not . toBe ( true ) ;
568+ const args = JSON . parse ( String ( result . content ) ) as { path : string } ;
569+ expect ( args . path ) . toBe ( target ) ;
539570 expect (
540571 pathEscapeBlockReason (
541572 { path : target , content : "x" } ,
0 commit comments