from __future__ import annotations import io import uuid from pathlib import Path import pytest from agents.sandbox import Manifest from agents.sandbox.entries import ( AzureBlobMount, BoxMount, DockerVolumeMountStrategy, FuseMountPattern, GCSMount, InContainerMountStrategy, Mount, MountpointMountPattern, MountStrategy, R2Mount, RcloneMountPattern, S3FilesMount, S3FilesMountPattern, S3Mount, ) from agents.sandbox.entries.mounts.patterns import ( FuseMountConfig, MountpointMountConfig, RcloneMountConfig, S3FilesMountConfig, ) from agents.sandbox.errors import MountCommandError, MountConfigError from agents.sandbox.session.base_sandbox_session import BaseSandboxSession from agents.sandbox.session.events import SandboxSessionEvent from agents.sandbox.session.manager import Instrumentation from agents.sandbox.session.sandbox_session import SandboxSession from agents.sandbox.session.sinks import CallbackSink from agents.sandbox.snapshot import NoopSnapshot from agents.sandbox.types import ExecResult from tests.utils.factories import TestSessionState class _MountConfigSession(BaseSandboxSession): def __init__(self, *, session_id: uuid.UUID | None = None, config_text: str = "") -> None: self.state = TestSessionState( session_id=session_id or uuid.uuid4(), manifest=Manifest(root="/workspace"), snapshot=NoopSnapshot(id=str(uuid.uuid4())), ) self._config_text = config_text async def read(self, path: Path, *, user: object = None) -> io.BytesIO: _ = (path, user) return io.BytesIO(self._config_text.encode("utf-8")) async def shutdown(self) -> None: return None async def write(self, path: Path, data: io.IOBase, *, user: object = None) -> None: _ = (path, data, user) raise AssertionError("write() should not be called in these tests") async def running(self) -> bool: return True async def _exec_internal( self, *command: str | Path, timeout: float | None = None, ) -> ExecResult: _ = (command, timeout) raise AssertionError("exec() should not be called in these tests") async def persist_workspace(self) -> io.IOBase: raise AssertionError("persist_workspace() should not be called in these tests") async def hydrate_workspace(self, data: io.IOBase) -> None: _ = data raise AssertionError("hydrate_workspace() should not be called in these tests") class _MountpointApplySession(BaseSandboxSession): def __init__(self, *, mount_exit_code: int = 0, mount_stderr: bytes = b"") -> None: self.state = TestSessionState( session_id=uuid.uuid4(), manifest=Manifest(root="/workspace"), snapshot=NoopSnapshot(id=str(uuid.uuid4())), ) self._mount_exit_code = mount_exit_code self._mount_stderr = mount_stderr self.exec_calls: list[list[str]] = [] self.write_calls: list[tuple[Path, bytes]] = [] async def read(self, path: Path, *, user: object = None) -> io.BytesIO: _ = (path, user) raise AssertionError("read() should not be called in these tests") async def shutdown(self) -> None: return None async def write(self, path: Path, data: io.IOBase, *, user: object = None) -> None: _ = user self.write_calls.append((path, data.read())) async def running(self) -> bool: return True def persist_workspace_skip_paths(self) -> set[Path]: return self._persist_workspace_skip_relpaths() async def _exec_internal( self, *command: str | Path, timeout: float | None = None, ) -> ExecResult: _ = timeout command_strs = [str(part) for part in command] self.exec_calls.append(command_strs) if ( len(command_strs) >= 3 and command_strs[:2] == ["sh", "-lc"] and "mount-s3 " in command_strs[2] and "command -v " not in command_strs[2] ): return ExecResult( exit_code=self._mount_exit_code, stdout=b"", stderr=self._mount_stderr ) return ExecResult(exit_code=0, stdout=b"", stderr=b"") async def persist_workspace(self) -> io.IOBase: raise AssertionError("persist_workspace() should not be called in these tests") async def hydrate_workspace(self, data: io.IOBase) -> None: _ = data raise AssertionError("hydrate_workspace() should not be called in these tests") class _GeneratedConfigApplySession(BaseSandboxSession): def __init__(self, *, session_id: uuid.UUID) -> None: self.state = TestSessionState( session_id=session_id, manifest=Manifest(root="/workspace"), snapshot=NoopSnapshot(id=str(uuid.uuid4())), ) self.exec_calls: list[list[str]] = [] self.write_calls: list[tuple[Path, bytes]] = [] async def read(self, path: Path, *, user: object = None) -> io.BytesIO: _ = (path, user) raise AssertionError("read() should not be called in these tests") async def shutdown(self) -> None: return None async def write(self, path: Path, data: io.IOBase, *, user: object = None) -> None: _ = user self.write_calls.append((path, data.read())) async def running(self) -> bool: return True async def _exec_internal( self, *command: str | Path, timeout: float | None = None, ) -> ExecResult: _ = timeout self.exec_calls.append([str(part) for part in command]) return ExecResult(exit_code=0, stdout=b"", stderr=b"") async def persist_workspace(self) -> io.IOBase: raise AssertionError("persist_workspace() should not be called in these tests") async def hydrate_workspace(self, data: io.IOBase) -> None: _ = data raise AssertionError("hydrate_workspace() should not be called in these tests") class _NoStrategyMount(Mount): type: str = f"no_strategy_mount_{uuid.uuid4().hex}" mount_strategy: MountStrategy = DockerVolumeMountStrategy(driver="rclone") def test_manifest_model_dump_preserves_mount_strategy_subtype_fields() -> None: manifest = Manifest( entries={ "in-container": S3Mount( bucket="bucket", mount_strategy=InContainerMountStrategy(pattern=MountpointMountPattern()), ), "docker-volume": S3Mount( bucket="bucket", mount_strategy=DockerVolumeMountStrategy( driver="rclone", driver_options={"vfs-cache-mode": "off"}, ), ), } ) payload = manifest.model_dump(mode="json") assert payload["entries"]["in-container"]["mount_strategy"] == { "type": "in_container", "pattern": { "type": "mountpoint", "options": { "prefix": None, "region": None, "endpoint_url": None, }, }, } assert payload["entries"]["docker-volume"]["mount_strategy"] == { "type": "docker_volume", "driver": "rclone", "driver_options": {"vfs-cache-mode": "off"}, } restored = Manifest.model_validate(payload) in_container = restored.entries["in-container"] docker_volume = restored.entries["docker-volume"] assert isinstance(in_container, S3Mount) assert isinstance(in_container.mount_strategy, InContainerMountStrategy) assert isinstance(in_container.mount_strategy.pattern, MountpointMountPattern) assert isinstance(docker_volume, S3Mount) assert isinstance(docker_volume.mount_strategy, DockerVolumeMountStrategy) assert docker_volume.mount_strategy.driver == "rclone" assert docker_volume.mount_strategy.driver_options == {"vfs-cache-mode": "off"} def test_manifest_model_dump_round_trips_s3_files_mount() -> None: manifest = Manifest( entries={ "remote": S3FilesMount( file_system_id="fs-1234567890abcdef0", subpath="/datasets", mount_target_ip="10.99.1.209", region="us-east-1", read_only=False, mount_strategy=InContainerMountStrategy(pattern=S3FilesMountPattern()), ) } ) payload = manifest.model_dump(mode="json") assert payload["entries"]["remote"]["type"] == "s3_files_mount" assert payload["entries"]["remote"]["mount_strategy"] == { "type": "in_container", "pattern": { "type": "s3files", "options": { "mount_target_ip": None, "access_point": None, "region": None, "extra_options": {}, }, }, } restored = Manifest.model_validate(payload) mount = restored.entries["remote"] assert isinstance(mount, S3FilesMount) assert mount.file_system_id == "fs-1234567890abcdef0" assert mount.subpath == "/datasets" assert mount.mount_target_ip == "10.99.1.209" assert mount.region == "us-east-1" assert mount.read_only is False assert isinstance(mount.mount_strategy, InContainerMountStrategy) assert isinstance(mount.mount_strategy.pattern, S3FilesMountPattern) @pytest.mark.asyncio async def test_azure_blob_mount_builds_rclone_runtime_config_without_hidden_pattern_state() -> None: session_id = uuid.uuid4() pattern = RcloneMountPattern(config_file_path=Path("rclone.conf")) remote_name = pattern.resolve_remote_name( session_id=session_id.hex, remote_kind="azureblob", mount_type="azure_blob_mount", ) session = _MountConfigSession( session_id=session_id, config_text=f"[{remote_name}]\ntype = azureblob\n", ) mount = AzureBlobMount( account="acct", container="container", mount_strategy=InContainerMountStrategy(pattern=pattern), ) apply_config = await mount.build_in_container_mount_config( session, pattern, include_config_text=True ) unmount_config = await mount.build_in_container_mount_config( session, pattern, include_config_text=False ) assert isinstance(apply_config, RcloneMountConfig) assert apply_config.remote_name == remote_name assert apply_config.remote_path == "container" assert apply_config.config_text is not None assert "account = acct" in apply_config.config_text assert isinstance(unmount_config, RcloneMountConfig) assert unmount_config.remote_name == remote_name assert unmount_config.config_text is None @pytest.mark.asyncio async def test_box_mount_builds_rclone_runtime_config_with_box_auth_options() -> None: session_id = uuid.uuid4() pattern = RcloneMountPattern(config_file_path=Path("rclone.conf")) remote_name = pattern.resolve_remote_name( session_id=session_id.hex, remote_kind="box", mount_type="box_mount", ) session = _MountConfigSession( session_id=session_id, config_text=f"[{remote_name}]\ntype = box\n", ) mount = BoxMount( path="/Shared/Finance", client_id="client-id", client_secret="client-secret", token='{"access_token":"token"}', root_folder_id="12345", impersonate="user-42", mount_strategy=InContainerMountStrategy(pattern=pattern), read_only=False, ) apply_config = await mount.build_in_container_mount_config( session, pattern, include_config_text=True ) unmount_config = await mount.build_in_container_mount_config( session, pattern, include_config_text=False ) assert isinstance(apply_config, RcloneMountConfig) assert apply_config.remote_name == remote_name assert apply_config.remote_path == "Shared/Finance" assert apply_config.read_only is False assert apply_config.config_text is not None assert "type = box" in apply_config.config_text assert "client_id = client-id" in apply_config.config_text assert "client_secret = client-secret" in apply_config.config_text assert 'token = {"access_token":"token"}' in apply_config.config_text assert "root_folder_id = 12345" in apply_config.config_text assert "impersonate = user-42" in apply_config.config_text assert isinstance(unmount_config, RcloneMountConfig) assert unmount_config.remote_name == remote_name assert unmount_config.remote_path == "Shared/Finance" assert unmount_config.config_text is None @pytest.mark.asyncio async def test_gcs_mount_uses_runtime_endpoint_override_without_mutating_pattern_options() -> None: pattern = MountpointMountPattern() mount = GCSMount( bucket="bucket", mount_strategy=InContainerMountStrategy(pattern=pattern), read_only=False, ) config = await mount.build_in_container_mount_config( _MountConfigSession(), pattern, include_config_text=False, ) assert isinstance(config, MountpointMountConfig) assert config.endpoint_url == "https://storage.googleapis.com" assert pattern.options.endpoint_url is None assert mount.read_only is False assert config.read_only is False session = _MountpointApplySession() await pattern.apply( session, Path("/workspace/remote"), MountpointMountConfig( bucket="bucket", access_key_id="access", secret_access_key="secret", session_token=None, prefix=None, region="us-east1", endpoint_url=config.endpoint_url, mount_type="gcs_mount", ), ) assert session.exec_calls[:2] == [ ["sh", "-lc", "command -v mount-s3 >/dev/null 2>&1"], ["mkdir", "-p", "/workspace/remote"], ] assert len(session.exec_calls) == 5 assert len(session.write_calls) == 1 env_path, env_payload = session.write_calls[0] assert env_path.as_posix().startswith(".sandbox-mountpoint-env/") assert env_path.name.endswith(".env") assert env_payload == b"export AWS_ACCESS_KEY_ID=access\nexport AWS_SECRET_ACCESS_KEY=secret\n" mount_command = session.exec_calls[-1] assert mount_command[:2] == ["sh", "-lc"] assert "mount-s3" in mount_command[2] assert "AWS_ACCESS_KEY_ID=access" not in mount_command[2] assert "AWS_SECRET_ACCESS_KEY=secret" not in mount_command[2] assert ".sandbox-mountpoint-env" in mount_command[2] assert "--region us-east1" in mount_command[2] assert "--endpoint-url https://storage.googleapis.com" in mount_command[2] assert "--upload-checksums off" in mount_command[2] assert "bucket /workspace/remote" in mount_command[2] @pytest.mark.asyncio async def test_s3_mountpoint_writable_mode_enables_overwrite_and_delete() -> None: session = _MountpointApplySession() pattern = MountpointMountPattern() await pattern.apply( session, Path("/workspace/remote"), MountpointMountConfig( bucket="bucket", access_key_id="access", secret_access_key="secret", session_token="token", prefix=None, region="us-east-1", endpoint_url=None, mount_type="s3_mount", read_only=False, ), ) assert session.exec_calls[:2] == [ ["sh", "-lc", "command -v mount-s3 >/dev/null 2>&1"], ["mkdir", "-p", "/workspace/remote"], ] assert len(session.exec_calls) == 5 assert len(session.write_calls) == 1 env_path, env_payload = session.write_calls[0] assert env_path.as_posix().startswith(".sandbox-mountpoint-env/") assert env_path.name.endswith(".env") assert env_payload == ( b"export AWS_ACCESS_KEY_ID=access\n" b"export AWS_SECRET_ACCESS_KEY=secret\n" b"export AWS_SESSION_TOKEN=token\n" ) mount_command = session.exec_calls[-1] assert mount_command[:2] == ["sh", "-lc"] assert "mount-s3" in mount_command[2] assert "--read-only" not in mount_command[2] assert "--allow-overwrite" in mount_command[2] assert "--allow-delete" in mount_command[2] assert "--region us-east-1" in mount_command[2] assert "AWS_ACCESS_KEY_ID=access" not in mount_command[2] assert "AWS_SECRET_ACCESS_KEY=secret" not in mount_command[2] assert "AWS_SESSION_TOKEN=token" not in mount_command[2] assert ".sandbox-mountpoint-env" in mount_command[2] assert "bucket /workspace/remote" in mount_command[2] @pytest.mark.asyncio async def test_gcs_mountpoint_writable_mode_enables_overwrite_and_delete() -> None: session = _MountpointApplySession() pattern = MountpointMountPattern() await pattern.apply( session, Path("/workspace/remote"), MountpointMountConfig( bucket="bucket", access_key_id="access", secret_access_key="secret", session_token=None, prefix=None, region="us-east1", endpoint_url="https://storage.googleapis.com", mount_type="gcs_mount", read_only=False, ), ) assert session.exec_calls[:2] == [ ["sh", "-lc", "command -v mount-s3 >/dev/null 2>&1"], ["mkdir", "-p", "/workspace/remote"], ] assert len(session.exec_calls) == 5 assert len(session.write_calls) == 1 env_path, env_payload = session.write_calls[0] assert env_path.as_posix().startswith(".sandbox-mountpoint-env/") assert env_path.name.endswith(".env") assert env_payload == b"export AWS_ACCESS_KEY_ID=access\nexport AWS_SECRET_ACCESS_KEY=secret\n" mount_command = session.exec_calls[-1] assert mount_command[:2] == ["sh", "-lc"] assert "mount-s3" in mount_command[2] assert "--read-only" not in mount_command[2] assert "--allow-overwrite" in mount_command[2] assert "--allow-delete" in mount_command[2] assert "--region us-east1" in mount_command[2] assert "--endpoint-url https://storage.googleapis.com" in mount_command[2] assert "--upload-checksums off" in mount_command[2] assert "AWS_ACCESS_KEY_ID=access" not in mount_command[2] assert "AWS_SECRET_ACCESS_KEY=secret" not in mount_command[2] assert ".sandbox-mountpoint-env" in mount_command[2] assert "bucket /workspace/remote" in mount_command[2] @pytest.mark.asyncio async def test_s3_mountpoint_failure_redacts_credentials_from_errors_and_events() -> None: events: list[SandboxSessionEvent] = [] inner = _MountpointApplySession( mount_exit_code=1, mount_stderr=b"bad credentials: access secret token", ) session = SandboxSession( inner, instrumentation=Instrumentation( sinks=[CallbackSink(lambda event, _session: events.append(event))] ), ) pattern = MountpointMountPattern() with pytest.raises(MountCommandError) as exc_info: await pattern.apply( session, Path("/workspace/remote"), MountpointMountConfig( bucket="bucket", access_key_id="access", secret_access_key="secret", session_token="token", prefix=None, region="us-east-1", endpoint_url=None, mount_type="s3_mount", read_only=False, ), ) context = exc_info.value.context command = str(context["command"]) stderr = str(context["stderr"]) assert "REDACTED" in stderr assert ".sandbox-mountpoint-env" in command assert any( path.as_posix().startswith(".sandbox-mountpoint-env/") for path in inner.persist_workspace_skip_paths() ) serialized_events = "\n".join(event.model_dump_json() for event in events) for sensitive_value in ("access", "secret", "token"): assert sensitive_value not in command assert sensitive_value not in stderr assert sensitive_value not in serialized_events @pytest.mark.asyncio async def test_s3_files_mount_builds_runtime_config_with_pattern_defaults() -> None: pattern = S3FilesMountPattern( options=S3FilesMountPattern.S3FilesOptions( mount_target_ip="10.99.1.209", access_point="fsap-pattern", region="us-east-1", extra_options={"tlsport": "3049"}, ) ) mount = S3FilesMount( file_system_id="fs-1234567890abcdef0", subpath="/datasets", access_point="fsap-direct", extra_options={"tlsport": "4049", "iam": None}, mount_strategy=InContainerMountStrategy(pattern=pattern), ) config = await mount.build_in_container_mount_config( _MountConfigSession(), pattern, include_config_text=False, ) assert isinstance(config, S3FilesMountConfig) assert config.file_system_id == "fs-1234567890abcdef0" assert config.subpath == "/datasets" assert config.mount_target_ip == "10.99.1.209" assert config.access_point == "fsap-direct" assert config.region == "us-east-1" assert config.extra_options == {"tlsport": "4049", "iam": None} @pytest.mark.asyncio async def test_s3_files_pattern_mounts_with_helper_options() -> None: session = _MountpointApplySession() pattern = S3FilesMountPattern() await pattern.apply( session, Path("/workspace/remote"), S3FilesMountConfig( file_system_id="fs-1234567890abcdef0", subpath="/datasets", mount_target_ip="10.99.1.209", access_point="fsap-123", region="us-east-1", extra_options={"tlsport": "4049"}, mount_type="s3_files_mount", read_only=True, ), ) assert session.exec_calls[:2] == [ ["sh", "-lc", "command -v mount.s3files >/dev/null 2>&1"], ["mkdir", "-p", "/workspace/remote"], ] assert session.exec_calls[2] == [ "mount", "-t", "s3files", "-o", ("tlsport=4049,ro,mounttargetip=10.99.1.209,accesspoint=fsap-123,region=us-east-1"), "fs-1234567890abcdef0:/datasets", "/workspace/remote", ] @pytest.mark.asyncio async def test_gcs_mount_builds_native_rclone_config_with_service_account_auth() -> None: session_id = uuid.uuid4() pattern = RcloneMountPattern() remote_name = pattern.resolve_remote_name( session_id=session_id.hex, remote_kind="gcs", mount_type="gcs_mount", ) mount = GCSMount( bucket="bucket", prefix="nested/prefix/", mount_strategy=InContainerMountStrategy(pattern=pattern), service_account_file="/data/config/gcs.json", service_account_credentials='{"type":"service_account"}', access_token="token", ) config = await mount.build_in_container_mount_config( _MountConfigSession(session_id=session_id), pattern, include_config_text=True, ) assert isinstance(config, RcloneMountConfig) assert config.remote_name == remote_name assert config.remote_path == "bucket/nested/prefix/" assert config.config_text == ( f"[{remote_name}]\n" "type = google cloud storage\n" "service_account_file = /data/config/gcs.json\n" 'service_account_credentials = {"type":"service_account"}\n' "access_token = token\n" "env_auth = false\n" ) @pytest.mark.asyncio async def test_gcs_mount_builds_s3_compatible_rclone_config_with_hmac_auth() -> None: session_id = uuid.uuid4() pattern = RcloneMountPattern() remote_name = pattern.resolve_remote_name( session_id=session_id.hex, remote_kind="gcs_s3", mount_type="gcs_mount", ) mount = GCSMount( bucket="bucket", access_id="access-id", secret_access_key="secret-key", prefix="nested/prefix/", region="auto", mount_strategy=InContainerMountStrategy(pattern=pattern), ) config = await mount.build_in_container_mount_config( _MountConfigSession(session_id=session_id), pattern, include_config_text=True, ) assert isinstance(config, RcloneMountConfig) assert config.remote_name == remote_name assert config.remote_path == "bucket/nested/prefix/" assert config.config_text == ( f"[{remote_name}]\n" "type = s3\n" "provider = GCS\n" "env_auth = false\n" "access_key_id = access-id\n" "secret_access_key = secret-key\n" "endpoint = https://storage.googleapis.com\n" "region = auto\n" ) @pytest.mark.asyncio async def test_gcs_hmac_rclone_remote_name_does_not_collide_with_s3_mount() -> None: session_id = uuid.UUID("12345678-1234-5678-1234-567812345678") pattern = RcloneMountPattern() session = _MountConfigSession(session_id=session_id) s3_mount = S3Mount( bucket="s3-bucket", mount_strategy=InContainerMountStrategy(pattern=pattern), ) gcs_mount = GCSMount( bucket="gcs-bucket", access_id="access-id", secret_access_key="secret-key", mount_strategy=InContainerMountStrategy(pattern=pattern), ) s3_config = await s3_mount.build_in_container_mount_config( session, pattern, include_config_text=True, ) gcs_config = await gcs_mount.build_in_container_mount_config( session, pattern, include_config_text=True, ) assert isinstance(s3_config, RcloneMountConfig) assert isinstance(gcs_config, RcloneMountConfig) assert s3_config.remote_name == "sandbox_s3_12345678123456781234567812345678" assert gcs_config.remote_name == "sandbox_gcs_s3_12345678123456781234567812345678" assert s3_config.remote_name != gcs_config.remote_name @pytest.mark.asyncio async def test_s3_mount_direct_mountpoint_fields_override_pattern_options() -> None: pattern = MountpointMountPattern( options=MountpointMountPattern.MountpointOptions( prefix="pattern-prefix/", region="pattern-region", endpoint_url="https://pattern.example.test", ) ) mount = S3Mount( bucket="bucket", prefix="direct-prefix/", region="direct-region", endpoint_url="https://direct.example.test", mount_strategy=InContainerMountStrategy(pattern=pattern), ) config = await mount.build_in_container_mount_config( _MountConfigSession(), pattern, include_config_text=False, ) assert isinstance(config, MountpointMountConfig) assert config.prefix == "direct-prefix/" assert config.region == "direct-region" assert config.endpoint_url == "https://direct.example.test" @pytest.mark.asyncio async def test_s3_mount_builds_prefixed_rclone_remote_path() -> None: session_id = uuid.uuid4() pattern = RcloneMountPattern() remote_name = pattern.resolve_remote_name( session_id=session_id.hex, remote_kind="s3", mount_type="s3_mount", ) mount = S3Mount( bucket="bucket", prefix="nested/prefix/", mount_strategy=InContainerMountStrategy(pattern=pattern), ) config = await mount.build_in_container_mount_config( _MountConfigSession(session_id=session_id), pattern, include_config_text=True, ) assert isinstance(config, RcloneMountConfig) assert config.remote_name == remote_name assert config.remote_path == "bucket/nested/prefix/" @pytest.mark.asyncio async def test_s3_mount_rclone_config_includes_endpoint_and_region() -> None: """S3Mount must emit endpoint and region in the rclone config.""" session_id = uuid.uuid4() pattern = RcloneMountPattern() remote_name = pattern.resolve_remote_name( session_id=session_id.hex, remote_kind="s3", mount_type="s3_mount", ) mount = S3Mount( bucket="my-bucket", access_key_id="ak", secret_access_key="sk", endpoint_url="http://localhost:9000", region="us-west-2", mount_strategy=InContainerMountStrategy(pattern=pattern), ) config = await mount.build_in_container_mount_config( _MountConfigSession(session_id=session_id), pattern, include_config_text=True, ) assert isinstance(config, RcloneMountConfig) assert config.config_text == ( f"[{remote_name}]\n" "type = s3\n" "provider = AWS\n" "endpoint = http://localhost:9000\n" "region = us-west-2\n" "env_auth = false\n" "access_key_id = ak\n" "secret_access_key = sk\n" ) @pytest.mark.asyncio async def test_s3_mount_rclone_config_omits_endpoint_when_unset() -> None: """When endpoint_url and region are not set, rclone defaults to AWS.""" session_id = uuid.uuid4() pattern = RcloneMountPattern() remote_name = pattern.resolve_remote_name( session_id=session_id.hex, remote_kind="s3", mount_type="s3_mount", ) mount = S3Mount( bucket="my-bucket", access_key_id="ak", secret_access_key="sk", mount_strategy=InContainerMountStrategy(pattern=pattern), ) config = await mount.build_in_container_mount_config( _MountConfigSession(session_id=session_id), pattern, include_config_text=True, ) assert isinstance(config, RcloneMountConfig) assert config.config_text == ( f"[{remote_name}]\n" "type = s3\n" "provider = AWS\n" "env_auth = false\n" "access_key_id = ak\n" "secret_access_key = sk\n" ) @pytest.mark.asyncio async def test_s3_mount_rclone_config_uses_custom_provider() -> None: """S3Mount with s3_provider='Other' emits the custom provider in the rclone config, which is required for non-AWS S3-compatible services (MinIO, Ceph, etc.) that need path-style addressing instead of AWS virtual-hosted-style.""" session_id = uuid.uuid4() pattern = RcloneMountPattern() remote_name = pattern.resolve_remote_name( session_id=session_id.hex, remote_kind="s3", mount_type="s3_mount", ) mount = S3Mount( bucket="my-bucket", access_key_id="ak", secret_access_key="sk", endpoint_url="http://localhost:9000", s3_provider="Other", mount_strategy=InContainerMountStrategy(pattern=pattern), ) config = await mount.build_in_container_mount_config( _MountConfigSession(session_id=session_id), pattern, include_config_text=True, ) assert isinstance(config, RcloneMountConfig) assert config.config_text == ( f"[{remote_name}]\n" "type = s3\n" "provider = Other\n" "endpoint = http://localhost:9000\n" "env_auth = false\n" "access_key_id = ak\n" "secret_access_key = sk\n" ) @pytest.mark.asyncio async def test_r2_mount_builds_rclone_config_with_explicit_credentials() -> None: session_id = uuid.uuid4() pattern = RcloneMountPattern() remote_name = pattern.resolve_remote_name( session_id=session_id.hex, remote_kind="r2", mount_type="r2_mount", ) mount = R2Mount( bucket="bucket", account_id="abc123accountid", access_key_id="r2-access", secret_access_key="r2-secret", mount_strategy=InContainerMountStrategy(pattern=pattern), ) config = await mount.build_in_container_mount_config( _MountConfigSession(session_id=session_id), pattern, include_config_text=True, ) assert isinstance(config, RcloneMountConfig) assert config.remote_name == remote_name assert config.remote_path == "bucket" assert config.config_text == ( f"[{remote_name}]\n" "type = s3\n" "provider = Cloudflare\n" "endpoint = https://abc123accountid.r2.cloudflarestorage.com\n" "acl = private\n" "env_auth = false\n" "access_key_id = r2-access\n" "secret_access_key = r2-secret\n" ) @pytest.mark.asyncio async def test_r2_mount_builds_env_auth_config_with_custom_domain() -> None: session_id = uuid.uuid4() pattern = RcloneMountPattern() remote_name = pattern.resolve_remote_name( session_id=session_id.hex, remote_kind="r2", mount_type="r2_mount", ) mount = R2Mount( bucket="bucket", account_id="abc123accountid", custom_domain="https://eu.r2.cloudflarestorage.com", mount_strategy=InContainerMountStrategy(pattern=pattern), ) config = await mount.build_in_container_mount_config( _MountConfigSession(session_id=session_id), pattern, include_config_text=True, ) assert isinstance(config, RcloneMountConfig) assert config.remote_name == remote_name assert config.remote_path == "bucket" assert config.config_text == ( f"[{remote_name}]\n" "type = s3\n" "provider = Cloudflare\n" "endpoint = https://eu.r2.cloudflarestorage.com\n" "acl = private\n" "env_auth = true\n" ) @pytest.mark.asyncio async def test_r2_mount_merges_existing_rclone_config_section() -> None: session_id = uuid.uuid4() pattern = RcloneMountPattern(config_file_path=Path("rclone.conf")) remote_name = pattern.resolve_remote_name( session_id=session_id.hex, remote_kind="r2", mount_type="r2_mount", ) session = _MountConfigSession( session_id=session_id, config_text=(f"[{remote_name}]\ntype = s3\nregion = auto\n\n[other]\ntype = memory\n"), ) mount = R2Mount( bucket="bucket", account_id="abc123accountid", access_key_id="r2-access", secret_access_key="r2-secret", mount_strategy=InContainerMountStrategy(pattern=pattern), ) config = await mount.build_in_container_mount_config( session, pattern, include_config_text=True, ) assert isinstance(config, RcloneMountConfig) assert config.remote_name == remote_name assert config.config_text == ( f"[{remote_name}]\n" "type = s3\n" "region = auto\n" "type = s3\n" "provider = Cloudflare\n" "endpoint = https://abc123accountid.r2.cloudflarestorage.com\n" "acl = private\n" "env_auth = false\n" "access_key_id = r2-access\n" "secret_access_key = r2-secret\n" "\n" "[other]\n" "type = memory\n" ) def test_r2_mount_rejects_mountpoint_pattern() -> None: with pytest.raises(MountConfigError, match="invalid mount_pattern type"): R2Mount( bucket="bucket", account_id="abc123accountid", mount_strategy=InContainerMountStrategy(pattern=MountpointMountPattern()), ) @pytest.mark.asyncio async def test_r2_mount_rejects_partial_credentials_for_both_strategies() -> None: in_container_mount = R2Mount( bucket="bucket", account_id="abc123accountid", access_key_id="r2-access", mount_strategy=InContainerMountStrategy(pattern=RcloneMountPattern()), ) with pytest.raises( MountConfigError, match="r2 credentials must include both access_key_id and secret_access_key", ): await in_container_mount.build_in_container_mount_config( _MountConfigSession(), RcloneMountPattern(), include_config_text=True, ) docker_mount = R2Mount( bucket="bucket", account_id="abc123accountid", secret_access_key="r2-secret", mount_strategy=DockerVolumeMountStrategy(driver="rclone"), ) with pytest.raises( MountConfigError, match="r2 credentials must include both access_key_id and secret_access_key", ): docker_mount.build_docker_volume_driver_config(DockerVolumeMountStrategy(driver="rclone")) @pytest.mark.asyncio async def test_docker_volume_mount_apply_fails_on_non_docker_session() -> None: mount = S3Mount( bucket="bucket", mount_strategy=DockerVolumeMountStrategy(driver="rclone"), ) with pytest.raises(MountConfigError) as exc_info: await mount.apply(_MountConfigSession(), Path("/workspace/data"), Path("/ignored")) assert str(exc_info.value) == "docker-volume mounts are not supported by this sandbox backend" def test_mount_requires_at_least_one_supported_strategy() -> None: with pytest.raises( MountConfigError, match="mount type must support at least one mount strategy", ): _NoStrategyMount() @pytest.mark.asyncio async def test_rclone_nfs_server_honors_read_only_runtime_config() -> None: session = _MountpointApplySession() pattern = RcloneMountPattern(mode="nfs") await pattern._start_rclone_server( session, config=RcloneMountConfig( remote_name="remote", remote_path="bucket", remote_kind="s3", mount_type="s3_mount", read_only=True, ), config_path=Path("/workspace/.sandbox-rclone-config/session/remote.conf"), nfs_addr="127.0.0.1:2049", ) assert session.exec_calls == [ [ "sh", "-lc", "/usr/local/bin/rclone serve nfs --help >/dev/null 2>&1" " || rclone serve nfs --help >/dev/null 2>&1", ], [ "sh", "-lc", "rclone serve nfs remote:bucket --addr 127.0.0.1:2049" " --config /workspace/.sandbox-rclone-config/session/remote.conf --read-only &", ], ] @pytest.mark.asyncio async def test_rclone_generated_config_is_written_owner_only() -> None: session_id = uuid.UUID("12345678-1234-5678-1234-567812345678") session = _GeneratedConfigApplySession(session_id=session_id) pattern = RcloneMountPattern() await pattern.apply( session, Path("/workspace/mnt"), RcloneMountConfig( remote_name="remote", remote_path="bucket", remote_kind="s3", mount_type="s3_mount", config_text="[remote]\ntype = s3\n", ), ) assert session.write_calls == [ ( Path(".sandbox-rclone-config/12345678123456781234567812345678/remote.conf"), b"[remote]\ntype = s3\n", ) ] assert session.exec_calls == [ ["sh", "-lc", "command -v rclone >/dev/null 2>&1 || test -x /usr/local/bin/rclone"], ["mkdir", "-p", "/workspace/mnt"], ["mkdir", "-p", "/workspace/.sandbox-rclone-config/12345678123456781234567812345678"], [ "chmod", "0600", "/workspace/.sandbox-rclone-config/12345678123456781234567812345678/remote.conf", ], [ "rclone", "mount", "remote:bucket", "/workspace/mnt", "--read-only", "--config", "/workspace/.sandbox-rclone-config/12345678123456781234567812345678/remote.conf", "--daemon", ], ] @pytest.mark.asyncio async def test_blobfuse_generated_config_is_written_owner_only() -> None: session_id = uuid.UUID("12345678-1234-5678-1234-567812345678") session = _GeneratedConfigApplySession(session_id=session_id) pattern = FuseMountPattern() await pattern.apply( session, Path("/workspace/mnt"), FuseMountConfig( account="acct", container="container", endpoint=None, identity_client_id=None, account_key="secret", mount_type="azure_blob_mount", read_only=True, ), ) assert session.write_calls == [ ( Path(".sandbox-blobfuse-config/12345678123456781234567812345678/acct_container.yaml"), ( b"allow-other: true\n" b"\n" b"logging:\n" b" type: syslog\n" b" level: log_debug\n" b"\n" b"components:\n" b" - libfuse\n" b" - block_cache\n" b" - attr_cache\n" b" - azstorage\n" b"\n" b"block_cache:\n" b" block-size-mb: 16\n" b" mem-size-mb: 50000\n" b" path: /workspace/.sandbox-blobfuse-cache/" b"12345678123456781234567812345678/acct/container\n" b" disk-size-mb: 50000\n" b" disk-timeout-sec: 3600\n" b"\n" b"attr_cache:\n" b" timeout-sec: 7200\n" b"\n" b"azstorage:\n" b" type: block\n" b" account-name: acct\n" b" container: container\n" b" endpoint: https://acct.blob.core.windows.net\n" b" auth-type: key\n" b" account-key: secret\n" ), ) ] assert session.exec_calls == [ ["sh", "-lc", "command -v blobfuse2 >/dev/null 2>&1"], ["mkdir", "-p", "/workspace/mnt"], [ "mkdir", "-p", "/workspace/.sandbox-blobfuse-cache/12345678123456781234567812345678/acct/container", ], ["mkdir", "-p", "/workspace/.sandbox-blobfuse-config/12345678123456781234567812345678"], [ "chmod", "0600", "/workspace/.sandbox-blobfuse-config/12345678123456781234567812345678/acct_container.yaml", ], [ "blobfuse2", "mount", "--read-only", "--config-file", "/workspace/.sandbox-blobfuse-config/12345678123456781234567812345678/acct_container.yaml", "/workspace/mnt", ], ] @pytest.mark.asyncio async def test_blobfuse_cache_path_must_be_relative_to_workspace() -> None: with pytest.raises(MountConfigError) as exc_info: FuseMountPattern(cache_path=Path("/tmp/blobfuse-cache")) assert exc_info.value.message == "blobfuse cache_path must be relative to the workspace root" assert exc_info.value.context == {"cache_path": "/tmp/blobfuse-cache"} with pytest.raises(MountConfigError) as escape_exc_info: FuseMountPattern(cache_path=Path("../blobfuse-cache")) assert escape_exc_info.value.message == ( "blobfuse cache_path must be relative to the workspace root" ) assert escape_exc_info.value.context == {"cache_path": "../blobfuse-cache"} with pytest.raises(MountConfigError) as windows_exc_info: FuseMountPattern(cache_path=Path("C:\\blobfuse-cache")) assert windows_exc_info.value.message == ( "blobfuse cache_path must be relative to the workspace root" ) assert windows_exc_info.value.context == {"cache_path": "C:/blobfuse-cache"} @pytest.mark.asyncio async def test_blobfuse_cache_path_must_be_outside_mount_path() -> None: session_id = uuid.UUID("12345678-1234-5678-1234-567812345678") session = _GeneratedConfigApplySession(session_id=session_id) pattern = FuseMountPattern() with pytest.raises(MountConfigError) as exc_info: await pattern.apply( session, Path("/workspace"), FuseMountConfig( account="acct", container="container", endpoint=None, identity_client_id=None, account_key="secret", mount_type="azure_blob_mount", read_only=True, ), ) assert exc_info.value.message == "blobfuse cache_path must be outside the mount path" assert exc_info.value.context == { "mount_path": "/workspace", "cache_path": ( "/workspace/.sandbox-blobfuse-cache/12345678123456781234567812345678/acct/container" ), } assert session.exec_calls == [["sh", "-lc", "command -v blobfuse2 >/dev/null 2>&1"]] assert session.write_calls == []