1010import agents .sandbox .entries .artifacts as artifacts_module
1111from agents .sandbox import SandboxConcurrencyLimits
1212from agents .sandbox .entries import Dir , File , GitRepo , LocalDir , LocalFile , resolve_workspace_path
13- from agents .sandbox .errors import ExecNonZeroError , InvalidManifestPathError , LocalDirReadError
13+ from agents .sandbox .errors import (
14+ ExecNonZeroError ,
15+ InvalidManifestPathError ,
16+ LocalDirReadError ,
17+ LocalFileReadError ,
18+ )
1419from agents .sandbox .manifest import Manifest
1520from agents .sandbox .materialization import MaterializedFile
1621from agents .sandbox .session .base_sandbox_session import BaseSandboxSession
@@ -148,6 +153,15 @@ def test_resolve_workspace_path_rejects_absolute_symlink_escape_for_host_root(
148153 assert exc_info .value .context == {"rel" : escaped .as_posix (), "reason" : "absolute" }
149154
150155
156+ def _symlink_or_skip (path : Path , target : Path , * , target_is_directory : bool = False ) -> None :
157+ try :
158+ path .symlink_to (target , target_is_directory = target_is_directory )
159+ except OSError as e :
160+ if os .name == "nt" and getattr (e , "winerror" , None ) == 1314 :
161+ pytest .skip ("symlink creation requires elevated privileges on Windows" )
162+ raise
163+
164+
151165@pytest .mark .asyncio
152166async def test_base_sandbox_session_uses_current_working_directory_for_local_file_sources (
153167 monkeypatch : pytest .MonkeyPatch ,
@@ -168,6 +182,47 @@ async def test_base_sandbox_session_uses_current_working_directory_for_local_fil
168182 assert session .writes [Path ("/workspace/copied.txt" )] == b"hello"
169183
170184
185+ @pytest .mark .asyncio
186+ async def test_local_file_rejects_symlinked_source_ancestors (tmp_path : Path ) -> None :
187+ target_dir = tmp_path / "secret-dir"
188+ target_dir .mkdir ()
189+ nested_dir = target_dir / "sub"
190+ nested_dir .mkdir ()
191+ (nested_dir / "secret.txt" ).write_text ("secret" , encoding = "utf-8" )
192+ _symlink_or_skip (tmp_path / "link" , target_dir , target_is_directory = True )
193+ session = _RecordingSession ()
194+
195+ with pytest .raises (LocalFileReadError ) as excinfo :
196+ await LocalFile (src = Path ("link/sub/secret.txt" )).apply (
197+ session ,
198+ Path ("/workspace/copied.txt" ),
199+ tmp_path ,
200+ )
201+
202+ assert excinfo .value .context ["reason" ] == "symlink_not_supported"
203+ assert excinfo .value .context ["child" ] == "link"
204+ assert session .writes == {}
205+
206+
207+ @pytest .mark .asyncio
208+ async def test_local_file_rejects_symlinked_source_leaf (tmp_path : Path ) -> None :
209+ secret = tmp_path / "secret.txt"
210+ secret .write_text ("secret" , encoding = "utf-8" )
211+ _symlink_or_skip (tmp_path / "link.txt" , secret )
212+ session = _RecordingSession ()
213+
214+ with pytest .raises (LocalFileReadError ) as excinfo :
215+ await LocalFile (src = Path ("link.txt" )).apply (
216+ session ,
217+ Path ("/workspace/copied.txt" ),
218+ tmp_path ,
219+ )
220+
221+ assert excinfo .value .context ["reason" ] == "symlink_not_supported"
222+ assert excinfo .value .context ["child" ] == "link.txt"
223+ assert session .writes == {}
224+
225+
171226@pytest .mark .asyncio
172227async def test_local_dir_copy_falls_back_when_safe_dir_fd_open_unavailable (
173228 monkeypatch : pytest .MonkeyPatch ,
0 commit comments