1010import agents .sandbox .entries .artifacts as artifacts_module
1111from agents .sandbox import SandboxConcurrencyLimits
1212from agents .sandbox .entries import Dir , File , GitRepo , LocalDir , LocalFile
13- from agents .sandbox .errors import ExecNonZeroError , LocalDirReadError
13+ from agents .sandbox .errors import ExecNonZeroError , LocalDirReadError , LocalFileReadError
1414from agents .sandbox .manifest import Manifest
1515from agents .sandbox .materialization import MaterializedFile
1616from agents .sandbox .session .base_sandbox_session import BaseSandboxSession
@@ -98,6 +98,15 @@ async def _exec_internal(
9898 return ExecResult (stdout = b"" , stderr = b"" , exit_code = 0 )
9999
100100
101+ def _symlink_or_skip (path : Path , target : Path , * , target_is_directory : bool = False ) -> None :
102+ try :
103+ path .symlink_to (target , target_is_directory = target_is_directory )
104+ except OSError as e :
105+ if os .name == "nt" and getattr (e , "winerror" , None ) == 1314 :
106+ pytest .skip ("symlink creation requires elevated privileges on Windows" )
107+ raise
108+
109+
101110@pytest .mark .asyncio
102111async def test_base_sandbox_session_uses_current_working_directory_for_local_file_sources (
103112 monkeypatch : pytest .MonkeyPatch ,
@@ -118,6 +127,47 @@ async def test_base_sandbox_session_uses_current_working_directory_for_local_fil
118127 assert session .writes [Path ("/workspace/copied.txt" )] == b"hello"
119128
120129
130+ @pytest .mark .asyncio
131+ async def test_local_file_rejects_symlinked_source_ancestors (tmp_path : Path ) -> None :
132+ target_dir = tmp_path / "secret-dir"
133+ target_dir .mkdir ()
134+ nested_dir = target_dir / "sub"
135+ nested_dir .mkdir ()
136+ (nested_dir / "secret.txt" ).write_text ("secret" , encoding = "utf-8" )
137+ _symlink_or_skip (tmp_path / "link" , target_dir , target_is_directory = True )
138+ session = _RecordingSession ()
139+
140+ with pytest .raises (LocalFileReadError ) as excinfo :
141+ await LocalFile (src = Path ("link/sub/secret.txt" )).apply (
142+ session ,
143+ Path ("/workspace/copied.txt" ),
144+ tmp_path ,
145+ )
146+
147+ assert excinfo .value .context ["reason" ] == "symlink_not_supported"
148+ assert excinfo .value .context ["child" ] == "link"
149+ assert session .writes == {}
150+
151+
152+ @pytest .mark .asyncio
153+ async def test_local_file_rejects_symlinked_source_leaf (tmp_path : Path ) -> None :
154+ secret = tmp_path / "secret.txt"
155+ secret .write_text ("secret" , encoding = "utf-8" )
156+ _symlink_or_skip (tmp_path / "link.txt" , secret )
157+ session = _RecordingSession ()
158+
159+ with pytest .raises (LocalFileReadError ) as excinfo :
160+ await LocalFile (src = Path ("link.txt" )).apply (
161+ session ,
162+ Path ("/workspace/copied.txt" ),
163+ tmp_path ,
164+ )
165+
166+ assert excinfo .value .context ["reason" ] == "symlink_not_supported"
167+ assert excinfo .value .context ["child" ] == "link.txt"
168+ assert session .writes == {}
169+
170+
121171@pytest .mark .asyncio
122172async def test_local_dir_copy_falls_back_when_safe_dir_fd_open_unavailable (
123173 monkeypatch : pytest .MonkeyPatch ,
0 commit comments