Files
openai--openai-agents-python/tests/sandbox/test_mounts.py
T
2026-07-13 12:39:17 +08:00

1307 lines
43 KiB
Python

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 == []