@@ -153,3 +153,151 @@ it('refuses oversized import files before any workspace mutation or bulk allocat
153153 )
154154 ) . toMatchObject ( { ok : false , error : expect . stringContaining ( '64 MB' ) } )
155155} )
156+
157+ it . each ( [ 'file' , 'directory' ] as const ) (
158+ 'rejects %s symlinks outside the import source during manifest and chunk reads' ,
159+ async ( kind ) => {
160+ const source = join ( root , 'selected' )
161+ const outside = join ( root , 'selected-other' )
162+ await mkdir ( source )
163+ await mkdir ( outside )
164+ await writeFile ( join ( outside , 'private.txt' ) , 'outside content' )
165+ const target = kind === 'file' ? join ( outside , 'private.txt' ) : outside
166+ await symlink ( target , join ( source , 'link' ) )
167+ const authorization = {
168+ toolName : 'import_local_files' ,
169+ args : { path : source , targetWorkspaceId : 'target' } ,
170+ }
171+ expect ( await executeLocalFileRequest ( { operation : 'manifest' } , authorization ) ) . toMatchObject ( {
172+ ok : false ,
173+ error : expect . stringContaining ( 'outside this import source' ) ,
174+ } )
175+ const outsideManifest = await executeLocalFileRequest (
176+ { operation : 'manifest' } ,
177+ { toolName : 'import_local_files' , args : { path : target , targetWorkspaceId : 'target' } }
178+ )
179+ if ( ! outsideManifest . ok || outsideManifest . data . kind !== 'manifest' )
180+ throw new Error ( 'Expected manifest' )
181+ const entry = outsideManifest . data . entries . find ( ( item ) => item . kind === 'file' ) !
182+ expect (
183+ await executeLocalFileRequest (
184+ {
185+ operation : 'chunk' ,
186+ relativePath : kind === 'file' ? 'link' : 'link/private.txt' ,
187+ revision : entry . revision ,
188+ offset : 0 ,
189+ } ,
190+ authorization
191+ )
192+ ) . toMatchObject ( { ok : false , error : expect . stringContaining ( 'outside this import source' ) } )
193+ }
194+ )
195+
196+ it ( 'supports internal symlinks and an explicitly selected symlink root, but rejects a retargeted child' , async ( ) => {
197+ const source = join ( root , 'selected' )
198+ await mkdir ( source )
199+ await writeFile ( join ( source , 'notes.txt' ) , 'inside' )
200+ await symlink ( join ( source , 'notes.txt' ) , join ( source , 'alias.txt' ) )
201+ const selectedAlias = join ( root , 'selected-alias' )
202+ await symlink ( source , selectedAlias )
203+ const authorization = {
204+ toolName : 'import_local_files' ,
205+ args : { path : selectedAlias , targetWorkspaceId : 'target' } ,
206+ }
207+ const manifest = await executeLocalFileRequest ( { operation : 'manifest' } , authorization )
208+ if ( ! manifest . ok || manifest . data . kind !== 'manifest' ) throw new Error ( 'Expected manifest' )
209+ const entry = manifest . data . entries . find ( ( item ) => item . relativePath === 'alias.txt' ) !
210+ const request = {
211+ operation : 'chunk' ,
212+ relativePath : 'alias.txt' ,
213+ revision : entry . revision ,
214+ offset : 0 ,
215+ }
216+ expect ( await executeLocalFileRequest ( request , authorization ) ) . toEqual ( {
217+ ok : true ,
218+ data : { kind : 'chunk' , bytes : new Uint8Array ( Buffer . from ( 'inside' ) ) , eof : true } ,
219+ } )
220+ await writeFile ( join ( root , 'outside.txt' ) , 'outside' )
221+ await rm ( join ( source , 'alias.txt' ) )
222+ await symlink ( join ( root , 'outside.txt' ) , join ( source , 'alias.txt' ) )
223+ expect ( await executeLocalFileRequest ( request , authorization ) ) . toMatchObject ( {
224+ ok : false ,
225+ error : expect . stringContaining ( 'outside this import source' ) ,
226+ } )
227+ } )
228+
229+ it . each ( [ 'é' , '界' , '😀' , '\uFEFF' ] ) (
230+ 'preserves %s across the default text-page boundary' ,
231+ async ( character ) => {
232+ const path = join ( root , 'unicode.txt' )
233+ const prefix = 'a' . repeat ( 63_999 )
234+ const content = `${ prefix } ${ character } end`
235+ await writeFile ( path , content )
236+ const first = await executeLocalFileRequest (
237+ { operation : 'read' } ,
238+ {
239+ toolName : 'read_local_file' ,
240+ args : { path } ,
241+ }
242+ )
243+ expect ( first ) . toMatchObject ( {
244+ ok : true ,
245+ data : {
246+ representation : 'text' ,
247+ text : prefix ,
248+ offset : 0 ,
249+ nextOffset : 63_999 ,
250+ truncated : true ,
251+ } ,
252+ } )
253+ if ( ! first . ok || first . data . kind !== 'read' ) throw new Error ( 'Expected text' )
254+ const second = await executeLocalFileRequest (
255+ { operation : 'read' } ,
256+ {
257+ toolName : 'read_local_file' ,
258+ args : { path, offset : first . data . nextOffset } ,
259+ }
260+ )
261+ expect ( second ) . toMatchObject ( {
262+ ok : true ,
263+ data : {
264+ representation : 'text' ,
265+ text : `${ character } end` ,
266+ nextOffset : Buffer . byteLength ( content ) ,
267+ truncated : false ,
268+ } ,
269+ } )
270+ }
271+ )
272+
273+ it ( 'preserves a UTF-8 BOM and rejects split offsets and limits that cannot fit a character' , async ( ) => {
274+ const path = join ( root , 'unicode.txt' )
275+ await writeFile ( path , '\uFEFF😀end' )
276+ expect (
277+ await executeLocalFileRequest (
278+ { operation : 'read' } ,
279+ {
280+ toolName : 'read_local_file' ,
281+ args : { path, limit : 4 } ,
282+ }
283+ )
284+ ) . toMatchObject ( { ok : true , data : { text : '\uFEFF' , nextOffset : 3 , truncated : true } } )
285+ expect (
286+ await executeLocalFileRequest (
287+ { operation : 'read' } ,
288+ {
289+ toolName : 'read_local_file' ,
290+ args : { path, offset : 4 } ,
291+ }
292+ )
293+ ) . toMatchObject ( { ok : false , error : expect . stringContaining ( 'UTF-8' ) } )
294+ expect (
295+ await executeLocalFileRequest (
296+ { operation : 'read' } ,
297+ {
298+ toolName : 'read_local_file' ,
299+ args : { path, offset : 3 , limit : 1 } ,
300+ }
301+ )
302+ ) . toMatchObject ( { ok : false , error : expect . stringContaining ( 'limit' ) } )
303+ } )
0 commit comments