@@ -377,6 +377,60 @@ def swap_parent_then_open(
377377 assert session .writes [Path ("/workspace/copied/nested/safe.txt" )] == b"safe"
378378
379379
380+ @pytest .mark .asyncio
381+ async def test_local_dir_copy_fallback_rejects_swapped_parent_directory (
382+ monkeypatch : pytest .MonkeyPatch ,
383+ tmp_path : Path ,
384+ ) -> None :
385+ src_root = tmp_path / "src"
386+ src_root .mkdir ()
387+ nested_dir = src_root / "nested"
388+ nested_dir .mkdir ()
389+ src_file = nested_dir / "safe.txt"
390+ src_file .write_text ("safe" , encoding = "utf-8" )
391+ secret_dir = tmp_path / "secret-dir"
392+ secret_dir .mkdir ()
393+ (secret_dir / "safe.txt" ).write_text ("secret" , encoding = "utf-8" )
394+ session = _RecordingSession ()
395+ local_dir = LocalDir (src = Path ("src" ))
396+ original_open = os .open
397+ swapped = False
398+
399+ monkeypatch .setattr ("agents.sandbox.entries.artifacts._OPEN_SUPPORTS_DIR_FD" , False )
400+ monkeypatch .setattr ("agents.sandbox.entries.artifacts._HAS_O_DIRECTORY" , False )
401+
402+ def swap_parent_then_open (
403+ path : str | Path ,
404+ flags : int ,
405+ mode : int = 0o777 ,
406+ * ,
407+ dir_fd : int | None = None ,
408+ ) -> int :
409+ nonlocal swapped
410+ if Path (path ) == src_file and not swapped :
411+ nested_dir .rename (src_root / "nested-original" )
412+ _symlink_or_skip (src_root / "nested" , secret_dir , target_is_directory = True )
413+ swapped = True
414+ if dir_fd is None :
415+ return original_open (path , flags , mode )
416+ return original_open (path , flags , mode , dir_fd = dir_fd )
417+
418+ monkeypatch .setattr ("agents.sandbox.entries.artifacts.os.open" , swap_parent_then_open )
419+
420+ with pytest .raises (LocalDirReadError ) as excinfo :
421+ await local_dir ._copy_local_dir_file (
422+ base_dir = tmp_path ,
423+ session = session ,
424+ src_root = src_root ,
425+ src = src_file ,
426+ dest_root = Path ("/workspace/copied" ),
427+ )
428+
429+ assert excinfo .value .context ["reason" ] == "symlink_not_supported"
430+ assert excinfo .value .context ["child" ] == "src/nested"
431+ assert session .writes == {}
432+
433+
380434@pytest .mark .asyncio
381435async def test_local_dir_apply_rejects_source_root_swapped_to_symlink_after_validation (
382436 monkeypatch : pytest .MonkeyPatch ,
0 commit comments