豆豆友情提示:这是一个非官方 GitHub 代理镜像,主要用于网络测试或访问加速。请勿在此进行登录、注册或处理任何敏感信息。进行这些操作请务必访问官方网站 github.com。 Raw 内容也通过此代理提供。
Skip to content

Commit a9f86e4

Browse files
committed
fix
1 parent 4228213 commit a9f86e4

File tree

4 files changed

+81
-19
lines changed

4 files changed

+81
-19
lines changed

src/agents/sandbox/entries/artifacts.py

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,7 @@ def _open_local_dir_file_for_copy(
365365
) -> int:
366366
if not _OPEN_SUPPORTS_DIR_FD or not _HAS_O_DIRECTORY:
367367
return self._open_local_dir_file_for_copy_fallback(
368+
base_dir=base_dir,
368369
src_root=src_root,
369370
rel_child=rel_child,
370371
)
@@ -539,7 +540,9 @@ def _local_dir_open_error(
539540
)
540541
return LocalDirReadError(src=src_root, cause=error)
541542

542-
def _open_local_dir_file_for_copy_fallback(self, *, src_root: Path, rel_child: Path) -> int:
543+
def _open_local_dir_file_for_copy_fallback(
544+
self, *, base_dir: Path, src_root: Path, rel_child: Path
545+
) -> int:
543546
src = src_root / rel_child
544547
try:
545548
src_stat = src.lstat()
@@ -564,20 +567,32 @@ def _open_local_dir_file_for_copy_fallback(self, *, src_root: Path, rel_child: P
564567
file_flags = os.O_RDONLY | getattr(os, "O_BINARY", 0) | getattr(os, "O_NOFOLLOW", 0)
565568
try:
566569
leaf_fd = os.open(src, file_flags)
567-
leaf_stat = os.fstat(leaf_fd)
568-
if not stat.S_ISREG(leaf_stat.st_mode) or not os.path.samestat(src_stat, leaf_stat):
570+
try:
571+
self._resolve_local_dir_src_root(base_dir)
572+
leaf_stat = os.fstat(leaf_fd)
573+
if not stat.S_ISREG(leaf_stat.st_mode) or not os.path.samestat(src_stat, leaf_stat):
574+
raise LocalDirReadError(
575+
src=src_root,
576+
context={
577+
"reason": "path_changed_during_copy",
578+
"child": rel_child.as_posix(),
579+
},
580+
)
581+
return leaf_fd
582+
except Exception:
569583
os.close(leaf_fd)
570-
raise LocalDirReadError(
571-
src=src_root,
572-
context={"reason": "path_changed_during_copy", "child": rel_child.as_posix()},
573-
)
574-
return leaf_fd
584+
raise
575585
except FileNotFoundError:
586+
self._resolve_local_dir_src_root(base_dir)
576587
raise LocalDirReadError(
577588
src=src_root,
578589
context={"reason": "path_changed_during_copy", "child": rel_child.as_posix()},
579590
) from None
580591
except OSError as e:
592+
try:
593+
self._resolve_local_dir_src_root(base_dir)
594+
except LocalDirReadError as root_error:
595+
raise root_error from e
581596
if e.errno == errno.ELOOP:
582597
raise LocalDirReadError(
583598
src=src_root,

src/agents/sandbox/sandboxes/docker.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ async def _copy_workspace_tree_pruned(
298298
await self._exec_checked(
299299
"mkdir",
300300
"-p",
301-
str(dst_child),
301+
sandbox_path_str(dst_child),
302302
error_cls=WorkspaceArchiveReadError,
303303
error_path=src_child,
304304
)
@@ -314,8 +314,8 @@ async def _copy_workspace_tree_pruned(
314314
"cp",
315315
"-R",
316316
"--",
317-
str(src_child),
318-
str(dst_child),
317+
sandbox_path_str(src_child),
318+
sandbox_path_str(dst_child),
319319
error_cls=WorkspaceArchiveReadError,
320320
error_path=src_child,
321321
)
@@ -337,7 +337,7 @@ async def _stage_workspace_copy(
337337
await self._exec_checked(
338338
"mkdir",
339339
"-p",
340-
str(staging_parent),
340+
sandbox_path_str(staging_parent),
341341
error_cls=WorkspaceArchiveReadError,
342342
error_path=root,
343343
)
@@ -347,15 +347,15 @@ async def _stage_workspace_copy(
347347
await self._exec_checked(
348348
"mkdir",
349349
"-p",
350-
str(staging_workspace),
350+
sandbox_path_str(staging_workspace),
351351
error_cls=WorkspaceArchiveReadError,
352352
error_path=root,
353353
)
354354
elif skip_rel_paths:
355355
await self._exec_checked(
356356
"mkdir",
357357
"-p",
358-
str(staging_workspace),
358+
sandbox_path_str(staging_workspace),
359359
error_cls=WorkspaceArchiveReadError,
360360
error_path=root,
361361
)
@@ -371,7 +371,7 @@ async def _stage_workspace_copy(
371371
"-R",
372372
"--",
373373
root.as_posix(),
374-
str(staging_workspace),
374+
sandbox_path_str(staging_workspace),
375375
error_cls=WorkspaceArchiveReadError,
376376
error_path=root,
377377
)

tests/extensions/test_sandbox_vercel.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,10 @@ async def run_command(
239239
if not path.startswith(cwd.rstrip("/") + "/"):
240240
continue
241241
rel_path = path[len(cwd.rstrip("/")) + 1 :]
242-
if rel_path in exclusions:
242+
if any(
243+
rel_path == exclusion or rel_path.startswith(f"{exclusion}/")
244+
for exclusion in exclusions
245+
):
243246
continue
244247
info = tarfile.TarInfo(name=rel_path if include_root else path)
245248
info.size = len(content)
@@ -340,7 +343,7 @@ async def teardown_for_snapshot(
340343
mount._events.append(("unmount", path.as_posix()))
341344
sandbox = cast(Any, session)._sandbox
342345
if sandbox is not None:
343-
sandbox.files.pop(f"{path}/mounted.txt", None)
346+
sandbox.files.pop(f"{path.as_posix()}/mounted.txt", None)
344347

345348
async def restore_after_snapshot(
346349
self,
@@ -352,7 +355,7 @@ async def restore_after_snapshot(
352355
mount._events.append(("mount", path.as_posix()))
353356
sandbox = cast(Any, session)._sandbox
354357
if sandbox is not None:
355-
sandbox.files[f"{path}/mounted.txt"] = b"mounted-content"
358+
sandbox.files[f"{path.as_posix()}/mounted.txt"] = b"mounted-content"
356359

357360
return _Adapter(self)
358361

tests/sandbox/test_entries.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,51 @@ def swap_root_then_open(
293293
dir_fd: int | None = None,
294294
) -> int:
295295
nonlocal swapped
296-
if (path == "src" or Path(path) == src_root) and not swapped:
296+
if (path == "src" or Path(path) in {src_root, src_root / "safe.txt"}) and not swapped:
297+
src_root.rename(tmp_path / "src-original")
298+
(tmp_path / "src").symlink_to(secret_dir, target_is_directory=True)
299+
swapped = True
300+
if dir_fd is None:
301+
return original_open(path, flags, mode)
302+
return original_open(path, flags, mode, dir_fd=dir_fd)
303+
304+
monkeypatch.setattr("agents.sandbox.entries.artifacts.os.open", swap_root_then_open)
305+
306+
with pytest.raises(LocalDirReadError) as excinfo:
307+
await local_dir.apply(session, Path("/workspace/copied"), tmp_path)
308+
309+
assert excinfo.value.context["reason"] == "symlink_not_supported"
310+
assert excinfo.value.context["child"] == "src"
311+
assert session.writes == {}
312+
313+
314+
@pytest.mark.asyncio
315+
async def test_local_dir_apply_fallback_rejects_source_root_swapped_to_symlink_after_validation(
316+
monkeypatch: pytest.MonkeyPatch,
317+
tmp_path: Path,
318+
) -> None:
319+
src_root = tmp_path / "src"
320+
src_root.mkdir()
321+
(src_root / "safe.txt").write_text("safe", encoding="utf-8")
322+
secret_dir = tmp_path / "secret-dir"
323+
secret_dir.mkdir()
324+
session = _RecordingSession()
325+
local_dir = LocalDir(src=Path("src"))
326+
original_open = os.open
327+
swapped = False
328+
329+
monkeypatch.setattr("agents.sandbox.entries.artifacts._OPEN_SUPPORTS_DIR_FD", False)
330+
monkeypatch.setattr("agents.sandbox.entries.artifacts._HAS_O_DIRECTORY", False)
331+
332+
def swap_root_then_open(
333+
path: str | Path,
334+
flags: int,
335+
mode: int = 0o777,
336+
*,
337+
dir_fd: int | None = None,
338+
) -> int:
339+
nonlocal swapped
340+
if Path(path) == src_root / "safe.txt" and not swapped:
297341
src_root.rename(tmp_path / "src-original")
298342
(tmp_path / "src").symlink_to(secret_dir, target_is_directory=True)
299343
swapped = True

0 commit comments

Comments
 (0)