Files
2026-07-13 12:39:17 +08:00

1053 lines
35 KiB
Python

from __future__ import annotations
import hashlib
import io
import os
from collections.abc import Awaitable, Callable, Sequence
from pathlib import Path, PureWindowsPath
import pytest
import agents.sandbox.entries.artifacts as artifacts_module
from agents.sandbox import SandboxConcurrencyLimits, SandboxPathGrant
from agents.sandbox.entries import (
Dir,
File,
GitRepo,
LocalDir,
LocalFile,
resolve_workspace_path,
)
from agents.sandbox.errors import (
ExecNonZeroError,
GitCloneError,
GitCopyError,
GitSubpathError,
InvalidManifestPathError,
LocalDirReadError,
LocalFileReadError,
)
from agents.sandbox.manifest import Manifest
from agents.sandbox.materialization import MaterializedFile
from agents.sandbox.session.base_sandbox_session import BaseSandboxSession
from agents.sandbox.snapshot import NoopSnapshot
from agents.sandbox.types import ExecResult, User
from tests.utils.factories import TestSessionState
class _RecordingSession(BaseSandboxSession):
def __init__(self, manifest: Manifest | None = None) -> None:
self.state = TestSessionState(
manifest=manifest or Manifest(),
snapshot=NoopSnapshot(id="noop"),
)
self.exec_calls: list[tuple[str, ...]] = []
self.writes: dict[Path, bytes] = {}
async def _exec_internal(
self,
*command: str | Path,
timeout: float | None = None,
) -> ExecResult:
_ = timeout
cmd = tuple(str(part) for part in command)
self.exec_calls.append(cmd)
return ExecResult(stdout=b"", stderr=b"", exit_code=0)
async def read(self, path: Path, *, user: object = None) -> io.IOBase:
_ = user
return io.BytesIO(self.writes[path])
async def write(self, path: Path, data: io.IOBase, *, user: object = None) -> None:
_ = user
self.writes[path] = data.read()
async def running(self) -> bool:
return True
async def persist_workspace(self) -> io.IOBase:
return io.BytesIO()
async def hydrate_workspace(self, data: io.IOBase) -> None:
_ = data
async def shutdown(self) -> None:
return
class _GitRefSession(_RecordingSession):
async def _exec_internal(
self,
*command: str | Path,
timeout: float | None = None,
) -> ExecResult:
_ = timeout
cmd = tuple(str(part) for part in command)
self.exec_calls.append(cmd)
if cmd == ("command -v git >/dev/null 2>&1",):
return ExecResult(stdout=b"/usr/bin/git\n", stderr=b"", exit_code=0)
if cmd[:2] == ("git", "clone"):
return ExecResult(stdout=b"", stderr=b"unexpected clone path", exit_code=1)
return ExecResult(stdout=b"", stderr=b"", exit_code=0)
class _GitFailureSession(_RecordingSession):
def __init__(self, *, fail_on: str) -> None:
super().__init__()
self.fail_on = fail_on
async def _exec_internal(
self,
*command: str | Path,
timeout: float | None = None,
) -> ExecResult:
_ = timeout
cmd = tuple(str(part) for part in command)
self.exec_calls.append(cmd)
if cmd == ("command -v git >/dev/null 2>&1",):
return ExecResult(stdout=b"/usr/bin/git\n", stderr=b"", exit_code=0)
if self.fail_on == "clone" and cmd[:2] == ("git", "clone"):
return ExecResult(stdout=b"", stderr=b"clone failed", exit_code=1)
if self.fail_on == "copy" and cmd[:1] == ("cp",):
return ExecResult(stdout=b"", stderr=b"copy failed", exit_code=1)
return ExecResult(stdout=b"", stderr=b"", exit_code=0)
class _MetadataFailureSession(_RecordingSession):
def __init__(
self,
manifest: Manifest | None = None,
*,
fail_commands: set[str],
) -> None:
super().__init__(manifest)
self.fail_commands = fail_commands
async def _exec_internal(
self,
*command: str | Path,
timeout: float | None = None,
) -> ExecResult:
_ = timeout
cmd = tuple(str(part) for part in command)
self.exec_calls.append(cmd)
if cmd and cmd[0] in self.fail_commands:
return ExecResult(stdout=b"", stderr=b"metadata failed", exit_code=1)
return ExecResult(stdout=b"", stderr=b"", exit_code=0)
def test_resolve_workspace_path_rejects_windows_drive_absolute_path() -> None:
with pytest.raises(InvalidManifestPathError) as exc_info:
resolve_workspace_path(
Path("/workspace"),
PureWindowsPath("C:/tmp/secret.txt"),
allow_absolute_within_root=True,
)
assert str(exc_info.value) == "manifest path must be relative: C:/tmp/secret.txt"
assert exc_info.value.context == {"rel": "C:/tmp/secret.txt", "reason": "absolute"}
def test_resolve_workspace_path_rejects_absolute_escape_after_normalization() -> None:
with pytest.raises(InvalidManifestPathError) as exc_info:
resolve_workspace_path(
Path("/workspace"),
"/workspace/../etc/passwd",
allow_absolute_within_root=True,
)
assert str(exc_info.value) == "manifest path must be relative: /etc/passwd"
assert exc_info.value.context == {"rel": "/etc/passwd", "reason": "absolute"}
def test_resolve_workspace_path_rejects_absolute_symlink_escape_for_host_root(
tmp_path: Path,
) -> None:
root = tmp_path / "workspace"
outside = tmp_path / "outside"
root.mkdir()
outside.mkdir()
link = root / "link"
try:
os.symlink(outside, link, target_is_directory=True)
except (NotImplementedError, OSError) as exc:
pytest.skip(f"symlink unavailable: {exc}")
escaped = link / "secret.txt"
with pytest.raises(InvalidManifestPathError) as exc_info:
resolve_workspace_path(
root,
escaped,
allow_absolute_within_root=True,
)
assert str(exc_info.value) == f"manifest path must be relative: {escaped.as_posix()}"
assert exc_info.value.context == {"rel": escaped.as_posix(), "reason": "absolute"}
def _symlink_or_skip(path: Path, target: Path, *, target_is_directory: bool = False) -> None:
try:
path.symlink_to(target, target_is_directory=target_is_directory)
except OSError as e:
if os.name == "nt" and getattr(e, "winerror", None) == 1314:
pytest.skip("symlink creation requires elevated privileges on Windows")
raise
@pytest.mark.asyncio
async def test_base_sandbox_session_uses_current_working_directory_for_local_file_sources(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
source = tmp_path / "source.txt"
source.write_text("hello", encoding="utf-8")
monkeypatch.chdir(tmp_path)
session = _RecordingSession(
Manifest(
entries={"copied.txt": LocalFile(src=Path("source.txt"))},
),
)
result = await session.apply_manifest()
assert result.files[0].path == Path("/workspace/copied.txt")
assert result.files[0].sha256 == hashlib.sha256(b"hello").hexdigest()
assert session.writes[Path("/workspace/copied.txt")] == b"hello"
@pytest.mark.asyncio
async def test_local_file_rejects_absolute_source_outside_base_dir(tmp_path: Path) -> None:
base = tmp_path / "base"
outside = tmp_path / "outside"
base.mkdir()
outside.mkdir()
(outside / "secret.txt").write_text("secret", encoding="utf-8")
session = _RecordingSession()
with pytest.raises(LocalFileReadError) as excinfo:
await LocalFile(src=outside / "secret.txt").apply(
session,
Path("/workspace/copied.txt"),
base,
)
assert excinfo.value.context["reason"] == "outside_base_dir"
assert excinfo.value.context["base_dir"] == str(base)
assert session.writes == {}
@pytest.mark.asyncio
async def test_local_file_rejects_relative_source_outside_base_dir(tmp_path: Path) -> None:
base = tmp_path / "base"
outside = tmp_path / "outside"
base.mkdir()
outside.mkdir()
(outside / "secret.txt").write_text("secret", encoding="utf-8")
session = _RecordingSession()
with pytest.raises(LocalFileReadError) as excinfo:
await LocalFile(src=Path("../outside/secret.txt")).apply(
session,
Path("/workspace/copied.txt"),
base,
)
assert excinfo.value.context["reason"] == "outside_base_dir"
assert excinfo.value.context["base_dir"] == str(base)
assert session.writes == {}
@pytest.mark.asyncio
async def test_local_file_allows_extra_path_granted_source_outside_base_dir(
tmp_path: Path,
) -> None:
base = tmp_path / "base"
outside = tmp_path / "outside"
base.mkdir()
outside.mkdir()
(outside / "secret.txt").write_text("secret", encoding="utf-8")
session = _RecordingSession(
Manifest(extra_path_grants=(SandboxPathGrant(path=str(outside)),)),
)
result = await LocalFile(src=outside / "secret.txt").apply(
session,
Path("/workspace/copied.txt"),
base,
)
assert result[0].path == Path("/workspace/copied.txt")
assert session.writes[Path("/workspace/copied.txt")] == b"secret"
@pytest.mark.asyncio
async def test_local_file_rejects_source_outside_extra_path_grants(tmp_path: Path) -> None:
base = tmp_path / "base"
outside = tmp_path / "outside"
other = tmp_path / "other"
base.mkdir()
outside.mkdir()
other.mkdir()
(outside / "secret.txt").write_text("secret", encoding="utf-8")
session = _RecordingSession(
Manifest(extra_path_grants=(SandboxPathGrant(path=str(other)),)),
)
with pytest.raises(LocalFileReadError) as excinfo:
await LocalFile(src=outside / "secret.txt").apply(
session,
Path("/workspace/copied.txt"),
base,
)
assert excinfo.value.context["reason"] == "outside_base_dir"
assert excinfo.value.context["extra_path_grants"] == [str(other)]
assert session.writes == {}
@pytest.mark.asyncio
async def test_serialized_manifest_extra_path_grant_allows_local_file_source(
tmp_path: Path,
) -> None:
base = tmp_path / "base"
outside = tmp_path / "outside"
base.mkdir()
outside.mkdir()
(outside / "secret.txt").write_text("secret", encoding="utf-8")
manifest = Manifest.model_validate(
{
"extra_path_grants": [{"path": str(outside)}],
"entries": {
"copied.txt": {
"type": "local_file",
"src": str(outside / "secret.txt"),
}
},
}
)
session = _RecordingSession(manifest)
result = await session._apply_entry_batch(
[(Path("/workspace/copied.txt"), manifest.entries["copied.txt"])],
base_dir=base,
)
assert result[0].path == Path("/workspace/copied.txt")
assert session.writes[Path("/workspace/copied.txt")] == b"secret"
@pytest.mark.asyncio
async def test_local_file_allows_absolute_source_inside_base_dir(tmp_path: Path) -> None:
base = tmp_path / "base"
source_dir = base / "source"
source_dir.mkdir(parents=True)
(source_dir / "safe.txt").write_text("safe", encoding="utf-8")
session = _RecordingSession()
result = await LocalFile(src=source_dir / "safe.txt").apply(
session,
Path("/workspace/copied.txt"),
base,
)
assert result[0].path == Path("/workspace/copied.txt")
assert session.writes[Path("/workspace/copied.txt")] == b"safe"
@pytest.mark.asyncio
async def test_local_file_rejects_symlinked_source_ancestors(tmp_path: Path) -> None:
target_dir = tmp_path / "secret-dir"
target_dir.mkdir()
nested_dir = target_dir / "sub"
nested_dir.mkdir()
(nested_dir / "secret.txt").write_text("secret", encoding="utf-8")
_symlink_or_skip(tmp_path / "link", target_dir, target_is_directory=True)
session = _RecordingSession()
with pytest.raises(LocalFileReadError) as excinfo:
await LocalFile(src=Path("link/sub/secret.txt")).apply(
session,
Path("/workspace/copied.txt"),
tmp_path,
)
assert excinfo.value.context["reason"] == "symlink_not_supported"
assert excinfo.value.context["child"] == "link"
assert session.writes == {}
@pytest.mark.asyncio
async def test_local_file_rejects_symlinked_source_leaf(tmp_path: Path) -> None:
secret = tmp_path / "secret.txt"
secret.write_text("secret", encoding="utf-8")
_symlink_or_skip(tmp_path / "link.txt", secret)
session = _RecordingSession()
with pytest.raises(LocalFileReadError) as excinfo:
await LocalFile(src=Path("link.txt")).apply(
session,
Path("/workspace/copied.txt"),
tmp_path,
)
assert excinfo.value.context["reason"] == "symlink_not_supported"
assert excinfo.value.context["child"] == "link.txt"
assert session.writes == {}
@pytest.mark.asyncio
async def test_local_file_rejects_symlinked_source_before_checksum(tmp_path: Path) -> None:
target_dir = tmp_path / "secret-dir"
target_dir.mkdir()
_symlink_or_skip(tmp_path / "link.txt", target_dir, target_is_directory=True)
session = _RecordingSession()
with pytest.raises(LocalFileReadError) as excinfo:
await LocalFile(src=Path("link.txt")).apply(
session,
Path("/workspace/copied.txt"),
tmp_path,
)
assert excinfo.value.context["reason"] == "symlink_not_supported"
assert excinfo.value.context["child"] == "link.txt"
assert session.writes == {}
@pytest.mark.asyncio
async def test_local_dir_copy_falls_back_when_safe_dir_fd_open_unavailable(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
src_root = tmp_path / "src"
src_root.mkdir()
src_file = src_root / "safe.txt"
src_file.write_text("safe", encoding="utf-8")
session = _RecordingSession()
local_dir = LocalDir(src=Path("src"))
monkeypatch.setattr("agents.sandbox.entries.artifacts._OPEN_SUPPORTS_DIR_FD", False)
monkeypatch.setattr("agents.sandbox.entries.artifacts._HAS_O_DIRECTORY", False)
result = await local_dir._copy_local_dir_file(
base_dir=tmp_path,
session=session,
src_root=src_root,
src=src_file,
dest_root=Path("/workspace/copied"),
)
assert result.path == Path("/workspace/copied/safe.txt")
assert session.writes[Path("/workspace/copied/safe.txt")] == b"safe"
@pytest.mark.asyncio
async def test_local_dir_copy_revalidates_swapped_paths_during_open(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
if not artifacts_module._OPEN_SUPPORTS_DIR_FD or not artifacts_module._HAS_O_DIRECTORY:
pytest.skip("safe dir_fd open pinning is unavailable on this platform")
src_root = tmp_path / "src"
src_root.mkdir()
src_file = src_root / "safe.txt"
src_file.write_text("safe", encoding="utf-8")
secret = tmp_path / "secret.txt"
secret.write_text("secret", encoding="utf-8")
session = _RecordingSession()
local_dir = LocalDir(src=Path("src"))
original_open = os.open
swapped = False
def swap_then_open(
path: str | Path,
flags: int,
mode: int = 0o777,
*,
dir_fd: int | None = None,
) -> int:
nonlocal swapped
if (path == "safe.txt" or Path(path) == src_file) and not swapped:
src_file.unlink()
_symlink_or_skip(src_file, secret)
swapped = True
if dir_fd is None:
return original_open(path, flags, mode)
return original_open(path, flags, mode, dir_fd=dir_fd)
monkeypatch.setattr("agents.sandbox.entries.artifacts.os.open", swap_then_open)
with pytest.raises(LocalDirReadError) as excinfo:
await local_dir._copy_local_dir_file(
base_dir=tmp_path,
session=session,
src_root=src_root,
src=src_file,
dest_root=Path("/workspace/copied"),
)
assert excinfo.value.context["reason"] in {
"symlink_not_supported",
"path_changed_during_copy",
}
assert excinfo.value.context["child"] == "safe.txt"
assert session.writes == {}
@pytest.mark.asyncio
async def test_local_dir_copy_pins_parent_directories_during_open(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
if not artifacts_module._OPEN_SUPPORTS_DIR_FD or not artifacts_module._HAS_O_DIRECTORY:
pytest.skip("safe dir_fd open pinning is unavailable on this platform")
src_root = tmp_path / "src"
src_root.mkdir()
nested_dir = src_root / "nested"
nested_dir.mkdir()
src_file = nested_dir / "safe.txt"
src_file.write_text("safe", encoding="utf-8")
secret_dir = tmp_path / "secret-dir"
secret_dir.mkdir()
(secret_dir / "safe.txt").write_text("secret", encoding="utf-8")
session = _RecordingSession()
local_dir = LocalDir(src=Path("src"))
original_open = os.open
swapped = False
def swap_parent_then_open(
path: str | Path,
flags: int,
mode: int = 0o777,
*,
dir_fd: int | None = None,
) -> int:
nonlocal swapped
if path == "safe.txt" and not swapped:
(src_root / "nested").rename(src_root / "nested-original")
_symlink_or_skip(src_root / "nested", secret_dir, target_is_directory=True)
swapped = True
if dir_fd is None:
return original_open(path, flags, mode)
return original_open(path, flags, mode, dir_fd=dir_fd)
monkeypatch.setattr("agents.sandbox.entries.artifacts.os.open", swap_parent_then_open)
result = await local_dir._copy_local_dir_file(
base_dir=tmp_path,
session=session,
src_root=src_root,
src=src_file,
dest_root=Path("/workspace/copied"),
)
assert result.path == Path("/workspace/copied/nested/safe.txt")
assert session.writes[Path("/workspace/copied/nested/safe.txt")] == b"safe"
@pytest.mark.asyncio
async def test_local_dir_copy_fallback_rejects_swapped_parent_directory(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
src_root = tmp_path / "src"
src_root.mkdir()
nested_dir = src_root / "nested"
nested_dir.mkdir()
src_file = nested_dir / "safe.txt"
src_file.write_text("safe", encoding="utf-8")
secret_dir = tmp_path / "secret-dir"
secret_dir.mkdir()
(secret_dir / "safe.txt").write_text("secret", encoding="utf-8")
session = _RecordingSession()
local_dir = LocalDir(src=Path("src"))
original_open = os.open
swapped = False
monkeypatch.setattr("agents.sandbox.entries.artifacts._OPEN_SUPPORTS_DIR_FD", False)
monkeypatch.setattr("agents.sandbox.entries.artifacts._HAS_O_DIRECTORY", False)
def swap_parent_then_open(
path: str | Path,
flags: int,
mode: int = 0o777,
*,
dir_fd: int | None = None,
) -> int:
nonlocal swapped
if Path(path) == src_file and not swapped:
nested_dir.rename(src_root / "nested-original")
_symlink_or_skip(src_root / "nested", secret_dir, target_is_directory=True)
swapped = True
if dir_fd is None:
return original_open(path, flags, mode)
return original_open(path, flags, mode, dir_fd=dir_fd)
monkeypatch.setattr("agents.sandbox.entries.artifacts.os.open", swap_parent_then_open)
with pytest.raises(LocalDirReadError) as excinfo:
await local_dir._copy_local_dir_file(
base_dir=tmp_path,
session=session,
src_root=src_root,
src=src_file,
dest_root=Path("/workspace/copied"),
)
assert excinfo.value.context["reason"] == "symlink_not_supported"
assert excinfo.value.context["child"] == "src/nested"
assert session.writes == {}
@pytest.mark.asyncio
async def test_local_dir_apply_rejects_source_root_swapped_to_symlink_after_validation(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
if not artifacts_module._OPEN_SUPPORTS_DIR_FD or not artifacts_module._HAS_O_DIRECTORY:
pytest.skip("safe dir_fd open pinning is unavailable on this platform")
src_root = tmp_path / "src"
src_root.mkdir()
(src_root / "safe.txt").write_text("safe", encoding="utf-8")
secret_dir = tmp_path / "secret-dir"
secret_dir.mkdir()
(secret_dir / "secret.txt").write_text("secret", encoding="utf-8")
session = _RecordingSession()
local_dir = LocalDir(src=Path("src"))
original_open = os.open
swapped = False
def swap_root_then_open(
path: str | Path,
flags: int,
mode: int = 0o777,
*,
dir_fd: int | None = None,
) -> int:
nonlocal swapped
if (path == "src" or Path(path) in {src_root, src_root / "safe.txt"}) and not swapped:
src_root.rename(tmp_path / "src-original")
(tmp_path / "src").symlink_to(secret_dir, target_is_directory=True)
swapped = True
if dir_fd is None:
return original_open(path, flags, mode)
return original_open(path, flags, mode, dir_fd=dir_fd)
monkeypatch.setattr("agents.sandbox.entries.artifacts.os.open", swap_root_then_open)
with pytest.raises(LocalDirReadError) as excinfo:
await local_dir.apply(session, Path("/workspace/copied"), tmp_path)
assert excinfo.value.context["reason"] == "symlink_not_supported"
assert excinfo.value.context["child"] == "src"
assert session.writes == {}
@pytest.mark.asyncio
async def test_local_dir_apply_fallback_rejects_source_root_swapped_to_symlink_after_validation(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
src_root = tmp_path / "src"
src_root.mkdir()
(src_root / "safe.txt").write_text("safe", encoding="utf-8")
secret_dir = tmp_path / "secret-dir"
secret_dir.mkdir()
session = _RecordingSession()
local_dir = LocalDir(src=Path("src"))
original_open = os.open
swapped = False
monkeypatch.setattr("agents.sandbox.entries.artifacts._OPEN_SUPPORTS_DIR_FD", False)
monkeypatch.setattr("agents.sandbox.entries.artifacts._HAS_O_DIRECTORY", False)
def swap_root_then_open(
path: str | Path,
flags: int,
mode: int = 0o777,
*,
dir_fd: int | None = None,
) -> int:
nonlocal swapped
if Path(path) == src_root / "safe.txt" and not swapped:
src_root.rename(tmp_path / "src-original")
_symlink_or_skip(tmp_path / "src", secret_dir, target_is_directory=True)
swapped = True
if dir_fd is None:
return original_open(path, flags, mode)
return original_open(path, flags, mode, dir_fd=dir_fd)
monkeypatch.setattr("agents.sandbox.entries.artifacts.os.open", swap_root_then_open)
with pytest.raises(LocalDirReadError) as excinfo:
await local_dir.apply(session, Path("/workspace/copied"), tmp_path)
assert excinfo.value.context["reason"] == "symlink_not_supported"
assert excinfo.value.context["child"] == "src"
assert session.writes == {}
@pytest.mark.asyncio
async def test_local_dir_apply_uses_configured_file_copy_fanout(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
src_root = tmp_path / "src"
src_root.mkdir()
(src_root / "a.txt").write_text("a", encoding="utf-8")
(src_root / "b.txt").write_text("b", encoding="utf-8")
session = _RecordingSession()
session._set_concurrency_limits(
SandboxConcurrencyLimits(
manifest_entries=4,
local_dir_files=2,
)
)
observed_limits: list[int | None] = []
async def gather_with_limit_recording(
task_factories: Sequence[Callable[[], Awaitable[MaterializedFile]]],
*,
max_concurrency: int | None = None,
) -> list[MaterializedFile]:
observed_limits.append(max_concurrency)
return [await factory() for factory in task_factories]
monkeypatch.setattr(
artifacts_module,
"gather_in_order",
gather_with_limit_recording,
)
result = await LocalDir(src=Path("src")).apply(
session,
Path("/workspace/copied"),
tmp_path,
)
assert observed_limits == [2]
assert sorted(file.path.as_posix() for file in result) == [
"/workspace/copied/a.txt",
"/workspace/copied/b.txt",
]
assert session.writes == {
Path("/workspace/copied/a.txt"): b"a",
Path("/workspace/copied/b.txt"): b"b",
}
@pytest.mark.asyncio
async def test_local_dir_rejects_absolute_source_outside_base_dir(tmp_path: Path) -> None:
base = tmp_path / "base"
outside = tmp_path / "outside"
base.mkdir()
outside.mkdir()
(outside / "secret.txt").write_text("secret", encoding="utf-8")
session = _RecordingSession()
with pytest.raises(LocalDirReadError) as excinfo:
await LocalDir(src=outside).apply(session, Path("/workspace/copied"), base)
assert excinfo.value.context["reason"] == "outside_base_dir"
assert excinfo.value.context["base_dir"] == str(base)
assert session.writes == {}
@pytest.mark.asyncio
async def test_local_dir_rejects_relative_source_outside_base_dir(tmp_path: Path) -> None:
base = tmp_path / "base"
outside = tmp_path / "outside"
base.mkdir()
outside.mkdir()
(outside / "secret.txt").write_text("secret", encoding="utf-8")
session = _RecordingSession()
with pytest.raises(LocalDirReadError) as excinfo:
await LocalDir(src=Path("../outside")).apply(session, Path("/workspace/copied"), base)
assert excinfo.value.context["reason"] == "outside_base_dir"
assert excinfo.value.context["base_dir"] == str(base)
assert session.writes == {}
@pytest.mark.asyncio
async def test_local_dir_allows_extra_path_granted_source_outside_base_dir(
tmp_path: Path,
) -> None:
base = tmp_path / "base"
outside = tmp_path / "outside"
base.mkdir()
outside.mkdir()
(outside / "secret.txt").write_text("secret", encoding="utf-8")
session = _RecordingSession(
Manifest(extra_path_grants=(SandboxPathGrant(path=str(outside), read_only=True),)),
)
result = await LocalDir(src=outside).apply(
session,
Path("/workspace/copied"),
base,
)
assert result[0].path == Path("/workspace/copied/secret.txt")
assert session.writes[Path("/workspace/copied/secret.txt")] == b"secret"
@pytest.mark.asyncio
async def test_local_dir_allows_absolute_source_inside_base_dir(tmp_path: Path) -> None:
base = tmp_path / "base"
source = base / "source"
source.mkdir(parents=True)
(source / "safe.txt").write_text("safe", encoding="utf-8")
session = _RecordingSession()
result = await LocalDir(src=source).apply(session, Path("/workspace/copied"), base)
assert result[0].path == Path("/workspace/copied/safe.txt")
assert session.writes[Path("/workspace/copied/safe.txt")] == b"safe"
@pytest.mark.asyncio
async def test_local_dir_rejects_symlinked_source_ancestors(tmp_path: Path) -> None:
target_dir = tmp_path / "secret-dir"
target_dir.mkdir()
nested_dir = target_dir / "sub"
nested_dir.mkdir()
(nested_dir / "secret.txt").write_text("secret", encoding="utf-8")
_symlink_or_skip(tmp_path / "link", target_dir, target_is_directory=True)
session = _RecordingSession()
with pytest.raises(LocalDirReadError) as excinfo:
await LocalDir(src=Path("link/sub")).apply(session, Path("/workspace/copied"), tmp_path)
assert excinfo.value.context["reason"] == "symlink_not_supported"
assert excinfo.value.context["child"] == "link"
assert session.writes == {}
@pytest.mark.asyncio
async def test_local_dir_rejects_symlinked_source_root(tmp_path: Path) -> None:
target_dir = tmp_path / "secret-dir"
target_dir.mkdir()
(target_dir / "secret.txt").write_text("secret", encoding="utf-8")
_symlink_or_skip(tmp_path / "src", target_dir, target_is_directory=True)
session = _RecordingSession()
with pytest.raises(LocalDirReadError) as excinfo:
await LocalDir(src=Path("src")).apply(session, Path("/workspace/copied"), tmp_path)
assert excinfo.value.context["reason"] == "symlink_not_supported"
assert excinfo.value.context["child"] == "src"
assert session.writes == {}
@pytest.mark.asyncio
async def test_local_dir_rejects_symlinked_files(tmp_path: Path) -> None:
src_root = tmp_path / "src"
src_root.mkdir()
(src_root / "safe.txt").write_text("safe", encoding="utf-8")
secret = tmp_path / "secret.txt"
secret.write_text("secret", encoding="utf-8")
_symlink_or_skip(src_root / "link.txt", secret)
session = _RecordingSession()
with pytest.raises(LocalDirReadError) as excinfo:
await LocalDir(src=Path("src")).apply(session, Path("/workspace/copied"), tmp_path)
assert excinfo.value.context["reason"] == "symlink_not_supported"
assert excinfo.value.context["child"] == "link.txt"
assert session.writes == {}
@pytest.mark.asyncio
async def test_local_dir_rejects_symlinked_directories(tmp_path: Path) -> None:
src_root = tmp_path / "src"
src_root.mkdir()
(src_root / "safe.txt").write_text("safe", encoding="utf-8")
target_dir = tmp_path / "secret-dir"
target_dir.mkdir()
(target_dir / "secret.txt").write_text("secret", encoding="utf-8")
_symlink_or_skip(src_root / "linked-dir", target_dir, target_is_directory=True)
session = _RecordingSession()
with pytest.raises(LocalDirReadError) as excinfo:
await LocalDir(src=Path("src")).apply(session, Path("/workspace/copied"), tmp_path)
assert excinfo.value.context["reason"] == "symlink_not_supported"
assert excinfo.value.context["child"] == "linked-dir"
assert session.writes == {}
@pytest.mark.asyncio
async def test_git_repo_uses_fetch_checkout_path_for_commit_refs() -> None:
session = _GitRefSession()
repo = GitRepo(repo="openai/example", ref="deadbeef")
await repo.apply(session, Path("/workspace/repo"), Path("/ignored"))
assert not any(call[:2] == ("git", "clone") for call in session.exec_calls)
assert any(call[:2] == ("git", "init") for call in session.exec_calls)
assert any(
len(call) >= 7
and call[:2] == ("git", "-C")
and call[3:6] == ("remote", "add", "origin")
and call[6] == "https://github.com/openai/example.git"
for call in session.exec_calls
)
assert any(
len(call) >= 9
and call[:2] == ("git", "-C")
and call[3:7] == ("fetch", "--depth", "1", "--no-tags")
and call[-2:] == ("origin", "deadbeef")
for call in session.exec_calls
)
assert any(
len(call) >= 6
and call[:2] == ("git", "-C")
and call[3:5] == ("checkout", "--detach")
and call[-1] == "FETCH_HEAD"
for call in session.exec_calls
)
@pytest.mark.asyncio
@pytest.mark.parametrize(
("subpath", "reason"),
[
(" ", "empty"),
("/docs", "absolute"),
("../outside", "parent_traversal"),
("docs/../../outside", "parent_traversal"),
("C:/repo", "windows_path"),
("docs\\outside", "windows_path"),
],
)
async def test_git_repo_rejects_invalid_subpath_before_copy(
subpath: str,
reason: str,
) -> None:
session = _GitFailureSession(fail_on="clone")
repo = GitRepo(repo="openai/example", ref="main", subpath=subpath)
with pytest.raises(GitSubpathError) as excinfo:
await repo.apply(session, Path("/workspace/repo"), Path("/ignored"))
assert excinfo.value.context["reason"] == reason
assert excinfo.value.context["subpath"] == subpath
assert session.exec_calls == []
@pytest.mark.asyncio
@pytest.mark.parametrize("subpath", ["", ".", "./", "./.", " ./ "])
async def test_git_repo_root_subpath_alias_copies_repo_root(subpath: str) -> None:
session = _RecordingSession()
repo = GitRepo(repo="openai/example", ref="main", subpath=subpath)
await repo.apply(session, Path("/workspace/repo"), Path("/ignored"))
copy_call = next(call for call in session.exec_calls if call[:1] == ("cp",))
assert copy_call[3].startswith("/tmp/sandbox-git-")
assert copy_call[3].endswith("/.")
assert not copy_call[3].endswith("//.")
assert copy_call[4].replace("\\", "/") == "/workspace/repo/"
@pytest.mark.asyncio
async def test_git_repo_allows_relative_subpath_copy() -> None:
session = _RecordingSession()
repo = GitRepo(repo="openai/example", ref="main", subpath="docs/reference")
await repo.apply(session, Path("/workspace/repo"), Path("/ignored"))
copy_call = next(call for call in session.exec_calls if call[:1] == ("cp",))
assert copy_call[3].endswith("/docs/reference/.")
assert copy_call[4].replace("\\", "/") == "/workspace/repo/"
def _git_temp_cleanup_calls(session: _RecordingSession) -> list[tuple[str, ...]]:
return [call for call in session.exec_calls if call[:3] == ("rm", "-rf", "--")]
def _git_temp_cleanup_call_indices(session: _RecordingSession) -> list[int]:
return [i for i, call in enumerate(session.exec_calls) if call[:3] == ("rm", "-rf", "--")]
@pytest.mark.asyncio
async def test_git_repo_cleans_temp_clone_after_copy_failure() -> None:
session = _GitFailureSession(fail_on="copy")
repo = GitRepo(repo="openai/example", ref="main")
with pytest.raises(GitCopyError):
await repo.apply(session, Path("/workspace/repo"), Path("/ignored"))
cleanup_calls = _git_temp_cleanup_calls(session)
cleanup_indices = _git_temp_cleanup_call_indices(session)
assert len(cleanup_calls) == 2
assert cleanup_calls[0] == cleanup_calls[1]
assert cleanup_indices[1] > next(
i for i, call in enumerate(session.exec_calls) if call[:1] == ("cp",)
)
@pytest.mark.asyncio
async def test_git_repo_cleans_temp_clone_after_clone_failure() -> None:
session = _GitFailureSession(fail_on="clone")
repo = GitRepo(repo="openai/example", ref="main")
with pytest.raises(GitCloneError):
await repo.apply(session, Path("/workspace/repo"), Path("/ignored"))
cleanup_calls = _git_temp_cleanup_calls(session)
cleanup_indices = _git_temp_cleanup_call_indices(session)
assert len(cleanup_calls) == 2
assert cleanup_calls[0] == cleanup_calls[1]
assert cleanup_indices[1] > next(
i for i, call in enumerate(session.exec_calls) if call[:2] == ("git", "clone")
)
@pytest.mark.asyncio
async def test_dir_metadata_strips_file_type_bits_before_chmod() -> None:
session = _RecordingSession()
dest = Path("/workspace/dir")
await Dir()._apply_metadata(session, dest)
assert ("chmod", "0755", "/workspace/dir") in session.exec_calls
@pytest.mark.asyncio
async def test_apply_manifest_raises_on_chmod_failure() -> None:
session = _MetadataFailureSession(
Manifest(entries={"copied.txt": File(content=b"hello")}),
fail_commands={"chmod"},
)
with pytest.raises(ExecNonZeroError):
await session.apply_manifest()
@pytest.mark.asyncio
async def test_apply_manifest_raises_on_chgrp_failure() -> None:
session = _MetadataFailureSession(
Manifest(
entries={
"copied.txt": File(
content=b"hello",
group=User(name="sandbox-user"),
)
}
),
fail_commands={"chgrp"},
)
with pytest.raises(ExecNonZeroError):
await session.apply_manifest()
assert ("chgrp", "sandbox-user", "/workspace/copied.txt") in session.exec_calls
assert not any(call[0] == "chmod" for call in session.exec_calls)