from __future__ import annotations import io import types import uuid from pathlib import Path from typing import Any, cast import pytest from agents.sandbox import Manifest from agents.sandbox.entries import RcloneMountPattern, S3Mount from agents.sandbox.errors import MountConfigError from agents.sandbox.session.base_sandbox_session import BaseSandboxSession from agents.sandbox.types import ExecResult class _FakeRunloopMountSession(BaseSandboxSession): def __init__(self, results: list[ExecResult] | None = None) -> None: self.state = cast( Any, types.SimpleNamespace( session_id=uuid.uuid4(), manifest=Manifest(root="/workspace"), ), ) self._results = list(results or []) self.exec_calls: list[str] = [] async def _exec_internal( self, *command: str | Path, timeout: float | None = None, ) -> ExecResult: _ = timeout cmd_str = " ".join(str(c) for c in command) self.exec_calls.append(cmd_str) if self._results: return self._results.pop(0) return ExecResult(stdout=b"", stderr=b"", exit_code=0) async def read(self, path: Path, *, user: object = None) -> io.IOBase: _ = (path, user) return io.BytesIO(b"") async def write(self, path: Path, data: io.IOBase, *, user: object = None) -> None: _ = (path, data, user) async def persist_workspace(self) -> io.IOBase: raise AssertionError("not expected") async def hydrate_workspace(self, data: io.IOBase) -> None: _ = data raise AssertionError("not expected") async def running(self) -> bool: return True _FakeRunloopMountSession.__name__ = "RunloopSandboxSession" def _exec_ok(stdout: bytes = b"") -> ExecResult: return ExecResult(stdout=stdout, stderr=b"", exit_code=0) def _exec_fail() -> ExecResult: return ExecResult(stdout=b"", stderr=b"", exit_code=1) def test_runloop_package_re_exports_cloud_bucket_strategy() -> None: package_module = __import__( "agents.extensions.sandbox.runloop", fromlist=["RunloopCloudBucketMountStrategy"], ) assert hasattr(package_module, "RunloopCloudBucketMountStrategy") def test_runloop_extension_re_exports_cloud_bucket_strategy() -> None: package_module = __import__( "agents.extensions.sandbox", fromlist=["RunloopCloudBucketMountStrategy"], ) assert hasattr(package_module, "RunloopCloudBucketMountStrategy") def test_runloop_mount_strategy_type_and_default_pattern() -> None: from agents.extensions.sandbox.runloop.mounts import RunloopCloudBucketMountStrategy strategy = RunloopCloudBucketMountStrategy() assert strategy.type == "runloop_cloud_bucket" assert isinstance(strategy.pattern, RcloneMountPattern) assert strategy.pattern.mode == "fuse" def test_runloop_mount_strategy_round_trips_through_manifest() -> None: from agents.extensions.sandbox.runloop.mounts import RunloopCloudBucketMountStrategy manifest = Manifest.model_validate( { "root": "/workspace", "entries": { "bucket": { "type": "s3_mount", "bucket": "my-bucket", "mount_strategy": {"type": "runloop_cloud_bucket"}, } }, } ) mount = manifest.entries["bucket"] assert isinstance(mount, S3Mount) assert isinstance(mount.mount_strategy, RunloopCloudBucketMountStrategy) def test_runloop_session_guard_rejects_wrong_type() -> None: from agents.extensions.sandbox.runloop.mounts import _assert_runloop_session class _WrongSession: pass with pytest.raises(MountConfigError, match="RunloopSandboxSession"): _assert_runloop_session(_WrongSession()) # type: ignore[arg-type] def test_runloop_session_guard_accepts_correct_type() -> None: from agents.extensions.sandbox.runloop.mounts import _assert_runloop_session _assert_runloop_session(_FakeRunloopMountSession()) @pytest.mark.asyncio async def test_runloop_ensure_rclone_installs_with_root_apt() -> None: from agents.extensions.sandbox._rclone import ensure_rclone session = _FakeRunloopMountSession( [ _exec_fail(), _exec_ok(), _exec_ok(), _exec_ok(), _exec_ok(), ] ) await ensure_rclone(session) assert session.exec_calls[:2] == [ "sh -lc command -v rclone >/dev/null 2>&1 || test -x /usr/local/bin/rclone", "sh -lc command -v apt-get >/dev/null 2>&1", ] assert session.exec_calls[2] == ( "sudo -u root -- sh -lc DEBIAN_FRONTEND=noninteractive " "DEBCONF_NOWARNINGS=yes apt-get -o Dpkg::Use-Pty=0 update -qq" ) assert session.exec_calls[3] == ( "sudo -u root -- sh -lc DEBIAN_FRONTEND=noninteractive " "DEBCONF_NOWARNINGS=yes apt-get -o Dpkg::Use-Pty=0 install -y -qq " "curl unzip ca-certificates" ) assert ( session.exec_calls[4] == "sudo -u root -- sh -lc curl -fsSL https://rclone.org/install.sh | bash" ) assert session.exec_calls[5] == ( "sh -lc command -v rclone >/dev/null 2>&1 || test -x /usr/local/bin/rclone" ) @pytest.mark.asyncio async def test_runloop_ensure_fuse_installs_missing_fusermount() -> None: from agents.extensions.sandbox.runloop.mounts import _ensure_fuse_support session = _FakeRunloopMountSession( [ _exec_ok(), _exec_ok(), _exec_fail(), _exec_ok(), _exec_ok(), _exec_ok(), _exec_ok(), _exec_ok(), ] ) await _ensure_fuse_support(session) assert session.exec_calls == [ "sh -lc test -c /dev/fuse", "sh -lc grep -qw fuse /proc/filesystems", "sh -lc command -v fusermount3 >/dev/null 2>&1 || command -v fusermount >/dev/null 2>&1", "sh -lc command -v apt-get >/dev/null 2>&1", ( "sudo -u root -- sh -lc DEBIAN_FRONTEND=noninteractive " "DEBCONF_NOWARNINGS=yes apt-get -o Dpkg::Use-Pty=0 update -qq" ), ( "sudo -u root -- sh -lc DEBIAN_FRONTEND=noninteractive " "DEBCONF_NOWARNINGS=yes apt-get -o Dpkg::Use-Pty=0 install -y -qq fuse3" ), "sh -lc command -v fusermount3 >/dev/null 2>&1 || command -v fusermount >/dev/null 2>&1", ( "sudo -u root -- sh -lc chmod a+rw /dev/fuse && " "touch /etc/fuse.conf && " "(grep -qxF user_allow_other /etc/fuse.conf || " "printf '\\nuser_allow_other\\n' >> /etc/fuse.conf)" ), ] @pytest.mark.asyncio async def test_runloop_rclone_pattern_adds_fuse_access_args() -> None: from agents.extensions.sandbox._rclone import rclone_pattern_for_session session = _FakeRunloopMountSession([_exec_ok(stdout=b"1000\n1000\n")]) pattern = await rclone_pattern_for_session(session, RcloneMountPattern(mode="fuse")) assert pattern.extra_args == ["--allow-other", "--uid", "1000", "--gid", "1000"]