Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/ref/sandbox/entries.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@
- R2Mount
- S3Mount
- S3FilesMount
- BoxMount
32 changes: 16 additions & 16 deletions docs/sandbox/clients.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,11 @@ Generic local/container strategies:

| Strategy or pattern | Use it when | Notes |
| --- | --- | --- |
| `InContainerMountStrategy(pattern=RcloneMountPattern(...))` | The sandbox image can run `rclone`. | Supports S3, GCS, R2, and Azure Blob. `RcloneMountPattern` can run in `fuse` mode or `nfs` mode. |
| `InContainerMountStrategy(pattern=RcloneMountPattern(...))` | The sandbox image can run `rclone`. | Supports S3, GCS, R2, Azure Blob, and Box. `RcloneMountPattern` can run in `fuse` mode or `nfs` mode. |
| `InContainerMountStrategy(pattern=MountpointMountPattern(...))` | The image has `mount-s3` and you want Mountpoint-style S3 or S3-compatible access. | Supports `S3Mount` and `GCSMount`. |
| `InContainerMountStrategy(pattern=FuseMountPattern(...))` | The image has `blobfuse2` and FUSE support. | Supports `AzureBlobMount`. |
| `InContainerMountStrategy(pattern=S3FilesMountPattern(...))` | The image has `mount.s3files` and can reach an existing S3 Files mount target. | Supports `S3FilesMount`. |
| `DockerVolumeMountStrategy(driver=...)` | Docker should attach a volume-driver-backed mount before the container starts. | Docker-only. S3, GCS, R2, and Azure Blob support `rclone`; S3 and GCS also support `mountpoint`. |
| `DockerVolumeMountStrategy(driver=...)` | Docker should attach a volume-driver-backed mount before the container starts. | Docker-only. S3, GCS, R2, Azure Blob, and Box support `rclone`; S3 and GCS also support `mountpoint`. |

</div>

Expand Down Expand Up @@ -106,13 +106,13 @@ Hosted sandbox clients expose provider-specific mount strategies. Choose the bac

| Backend | Mount notes |
| --- | --- |
| Docker | Supports `S3Mount`, `GCSMount`, `R2Mount`, `AzureBlobMount`, and `S3FilesMount` with local strategies such as `InContainerMountStrategy` and `DockerVolumeMountStrategy`. |
| Docker | Supports `S3Mount`, `GCSMount`, `R2Mount`, `AzureBlobMount`, `BoxMount`, and `S3FilesMount` with local strategies such as `InContainerMountStrategy` and `DockerVolumeMountStrategy`. |
| `ModalSandboxClient` | Supports Modal cloud bucket mounts with `ModalCloudBucketMountStrategy` on `S3Mount`, `R2Mount`, and HMAC-authenticated `GCSMount`. You can use inline credentials or a named Modal Secret. |
| `CloudflareSandboxClient` | Supports Cloudflare bucket mounts with `CloudflareBucketMountStrategy` on `S3Mount`, `R2Mount`, and HMAC-authenticated `GCSMount`. |
| `BlaxelSandboxClient` | Supports cloud bucket mounts with `BlaxelCloudBucketMountStrategy` on `S3Mount`, `R2Mount`, and `GCSMount`. Also supports persistent Blaxel Drives with `BlaxelDriveMount` and `BlaxelDriveMountStrategy` from `agents.extensions.sandbox.blaxel`. |
| `DaytonaSandboxClient` | Supports cloud bucket mounts with `DaytonaCloudBucketMountStrategy`; use it with `S3Mount`, `GCSMount`, `R2Mount`, and `AzureBlobMount`. |
| `E2BSandboxClient` | Supports cloud bucket mounts with `E2BCloudBucketMountStrategy`; use it with `S3Mount`, `GCSMount`, `R2Mount`, and `AzureBlobMount`. |
| `RunloopSandboxClient` | Supports cloud bucket mounts with `RunloopCloudBucketMountStrategy`; use it with `S3Mount`, `GCSMount`, `R2Mount`, and `AzureBlobMount`. |
| `DaytonaSandboxClient` | Supports rclone-backed cloud storage mounts with `DaytonaCloudBucketMountStrategy`; use it with `S3Mount`, `GCSMount`, `R2Mount`, `AzureBlobMount`, and `BoxMount`. |
| `E2BSandboxClient` | Supports rclone-backed cloud storage mounts with `E2BCloudBucketMountStrategy`; use it with `S3Mount`, `GCSMount`, `R2Mount`, `AzureBlobMount`, and `BoxMount`. |
| `RunloopSandboxClient` | Supports rclone-backed cloud storage mounts with `RunloopCloudBucketMountStrategy`; use it with `S3Mount`, `GCSMount`, `R2Mount`, `AzureBlobMount`, and `BoxMount`. |
| `VercelSandboxClient` | No hosted-specific mount strategy is currently exposed. Use manifest files, repos, or other workspace inputs instead. |

</div>
Expand All @@ -121,16 +121,16 @@ The table below summarizes which remote storage entries each backend can mount d

<div class="sandbox-nowrap-first-column-table" markdown="1">

| Backend | AWS S3 | Cloudflare R2 | GCS | Azure Blob Storage | S3 Files |
| --- | --- | --- | --- | --- | --- |
| Docker | ✓ | ✓ | ✓ | ✓ | ✓ |
| `ModalSandboxClient` | ✓ | ✓ | ✓ | - | - |
| `CloudflareSandboxClient` | ✓ | ✓ | ✓ | - | - |
| `BlaxelSandboxClient` | ✓ | ✓ | ✓ | - | - |
| `DaytonaSandboxClient` | ✓ | ✓ | ✓ | ✓ | - |
| `E2BSandboxClient` | ✓ | ✓ | ✓ | ✓ | - |
| `RunloopSandboxClient` | ✓ | ✓ | ✓ | ✓ | - |
| `VercelSandboxClient` | - | - | - | - | - |
| Backend | AWS S3 | Cloudflare R2 | GCS | Azure Blob Storage | Box | S3 Files |
| --- | --- | --- | --- | --- | --- | --- |
| Docker | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| `ModalSandboxClient` | ✓ | ✓ | ✓ | - | - | - |
| `CloudflareSandboxClient` | ✓ | ✓ | ✓ | - | - | - |
| `BlaxelSandboxClient` | ✓ | ✓ | ✓ | - | - | - |
| `DaytonaSandboxClient` | ✓ | ✓ | ✓ | ✓ | ✓ | - |
| `E2BSandboxClient` | ✓ | ✓ | ✓ | ✓ | ✓ | - |
| `RunloopSandboxClient` | ✓ | ✓ | ✓ | ✓ | ✓ | - |
| `VercelSandboxClient` | - | - | - | - | - | - |

</div>

Expand Down
2 changes: 1 addition & 1 deletion docs/sandbox/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ Use manifest entries for the material the agent needs before work begins:
| `File`, `Dir` | Small synthetic inputs, helper files, or output directories. |
| `LocalFile`, `LocalDir` | Host files or directories that should be materialized into the sandbox. |
| `GitRepo` | A repository that should be fetched into the workspace. |
| mounts such as `S3Mount`, `GCSMount`, `R2Mount`, `AzureBlobMount`, `S3FilesMount` | External storage that should appear inside the sandbox. |
| mounts such as `S3Mount`, `GCSMount`, `R2Mount`, `AzureBlobMount`, `BoxMount`, `S3FilesMount` | External storage that should appear inside the sandbox. |

</div>

Expand Down
10 changes: 5 additions & 5 deletions src/agents/extensions/sandbox/daytona/mounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
:class:`InContainerMountStrategy` that ensures ``rclone`` is installed inside
the sandbox before delegating to :class:`RcloneMountPattern`.

Supports S3, R2, GCS, and Azure Blob mounts through a single code path.
Supports S3, R2, GCS, Azure Blob, and Box mounts through a single code path.
"""

from __future__ import annotations
Expand Down Expand Up @@ -161,12 +161,12 @@ def _assert_daytona_session(session: BaseSandboxSession) -> None:


class DaytonaCloudBucketMountStrategy(MountStrategyBase):
"""Mount cloud buckets in Daytona sandboxes via rclone.
"""Mount rclone-backed cloud storage in Daytona sandboxes.

Wraps :class:`InContainerMountStrategy` with automatic ``rclone``
provisioning. Use with any provider mount (``S3Mount``, ``R2Mount``,
``GCSMount``, ``AzureBlobMount``) and let the generic framework handle
config generation and mount execution.
provisioning. Use with any rclone-backed provider mount (``S3Mount``,
``R2Mount``, ``GCSMount``, ``AzureBlobMount``, ``BoxMount``) and let the
generic framework handle config generation and mount execution.

Usage::

Expand Down
2 changes: 1 addition & 1 deletion src/agents/extensions/sandbox/e2b/mounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ def _assert_e2b_session(session: BaseSandboxSession) -> None:


class E2BCloudBucketMountStrategy(MountStrategyBase):
"""Mount cloud buckets in E2B sandboxes via rclone."""
"""Mount rclone-backed cloud storage in E2B sandboxes."""

type: Literal["e2b_cloud_bucket"] = "e2b_cloud_bucket"
pattern: RcloneMountPattern = RcloneMountPattern(mode="fuse")
Expand Down
2 changes: 1 addition & 1 deletion src/agents/extensions/sandbox/runloop/mounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ def _assert_runloop_session(session: BaseSandboxSession) -> None:


class RunloopCloudBucketMountStrategy(MountStrategyBase):
"""Mount cloud buckets in Runloop sandboxes via rclone."""
"""Mount rclone-backed cloud storage in Runloop sandboxes."""

type: Literal["runloop_cloud_bucket"] = "runloop_cloud_bucket"
pattern: RcloneMountPattern = RcloneMountPattern(mode="fuse")
Expand Down
2 changes: 2 additions & 0 deletions src/agents/sandbox/entries/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from .base import BaseEntry, resolve_workspace_path
from .mounts import (
AzureBlobMount,
BoxMount,
DockerVolumeMountStrategy,
FuseMountPattern,
GCSMount,
Expand All @@ -24,6 +25,7 @@
__all__ = [
"AzureBlobMount",
"BaseEntry",
"BoxMount",
"Dir",
"File",
"DockerVolumeMountStrategy",
Expand Down
3 changes: 2 additions & 1 deletion src/agents/sandbox/entries/mounts/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@
RcloneMountPattern,
S3FilesMountPattern,
)
from .providers import AzureBlobMount, GCSMount, R2Mount, S3FilesMount, S3Mount
from .providers import AzureBlobMount, BoxMount, GCSMount, R2Mount, S3FilesMount, S3Mount

__all__ = [
"AzureBlobMount",
"BoxMount",
"FuseMountPattern",
"GCSMount",
"DockerVolumeMountStrategy",
Expand Down
2 changes: 2 additions & 0 deletions src/agents/sandbox/entries/mounts/providers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

from .azure_blob import AzureBlobMount
from .box import BoxMount
from .gcs import GCSMount
from .r2 import R2Mount
from .s3 import S3Mount
Expand All @@ -12,4 +13,5 @@
"R2Mount",
"S3Mount",
"S3FilesMount",
"BoxMount",
]
126 changes: 126 additions & 0 deletions src/agents/sandbox/entries/mounts/providers/box.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
from __future__ import annotations

import builtins
from typing import TYPE_CHECKING, Literal

from ....errors import MountConfigError
from ..base import DockerVolumeMountStrategy
from ..patterns import MountPattern, MountPatternConfig, RcloneMountPattern
from .base import _ConfiguredMount

if TYPE_CHECKING:
from ....session.base_sandbox_session import BaseSandboxSession


class BoxMount(_ConfiguredMount):
"""Mount a Box folder using rclone.

See Box's JWT setup guide (https://developer.box.com/guides/authentication/jwt/jwt-setup/)
and rclone's Box guide (https://rclone.org/box/). Non-interactive mounts require
a minted `token` or `access_token`.
"""

type: Literal["box_mount"] = "box_mount"
path: str | None = None
client_id: str | None = None
client_secret: str | None = None
access_token: str | None = None
token: str | None = None
box_config_file: str | None = None
config_credentials: str | None = None
box_sub_type: Literal["user", "enterprise"] = "user"
root_folder_id: str | None = None
impersonate: str | None = None
owned_by: str | None = None

def supported_in_container_patterns(self) -> tuple[builtins.type[MountPattern], ...]:
return (RcloneMountPattern,)

def supported_docker_volume_drivers(self) -> frozenset[str]:
return frozenset({"rclone"})

def build_docker_volume_driver_config(
self,
strategy: DockerVolumeMountStrategy,
) -> tuple[str, dict[str, str], bool]:
options: dict[str, str] = {"type": "box", "path": self._remote_path()}
if self.client_id is not None:
options["box-client-id"] = self.client_id
if self.client_secret is not None:
options["box-client-secret"] = self.client_secret
if self.access_token is not None:
options["box-access-token"] = self.access_token
if self.token is not None:
options["box-token"] = self.token
if self.box_config_file is not None:
options["box-box-config-file"] = self.box_config_file
if self.config_credentials is not None:
options["box-config-credentials"] = self.config_credentials
if self.box_sub_type != "user":
options["box-box-sub-type"] = self.box_sub_type
if self.root_folder_id is not None:
options["box-root-folder-id"] = self.root_folder_id
if self.impersonate is not None:
options["box-impersonate"] = self.impersonate
if self.owned_by is not None:
options["box-owned-by"] = self.owned_by
return strategy.driver, options | strategy.driver_options, self.read_only

async def build_in_container_mount_config(
self,
session: BaseSandboxSession,
pattern: MountPattern,
*,
include_config_text: bool,
) -> MountPatternConfig:
if isinstance(pattern, RcloneMountPattern):
return await self._build_rclone_config(
session=session,
pattern=pattern,
remote_kind="box",
remote_path=self._remote_path(),
required_lines=self._rclone_required_lines(
pattern.resolve_remote_name(
session_id=self._require_session_id_hex(session, self.type),
remote_kind="box",
mount_type=self.type,
)
),
include_config_text=include_config_text,
)
raise MountConfigError(
message="invalid mount_pattern type",
context={"type": self.type},
)

def _remote_path(self) -> str:
if self.path is None:
return ""
return self.path.lstrip("/")

def _rclone_required_lines(self, remote_name: str) -> list[str]:
lines = [
f"[{remote_name}]",
"type = box",
]
if self.client_id is not None:
lines.append(f"client_id = {self.client_id}")
if self.client_secret is not None:
lines.append(f"client_secret = {self.client_secret}")
if self.access_token is not None:
lines.append(f"access_token = {self.access_token}")
if self.token is not None:
lines.append(f"token = {self.token}")
if self.box_config_file is not None:
lines.append(f"box_config_file = {self.box_config_file}")
if self.config_credentials is not None:
lines.append(f"config_credentials = {self.config_credentials}")
if self.box_sub_type != "user":
lines.append(f"box_sub_type = {self.box_sub_type}")
if self.root_folder_id is not None:
lines.append(f"root_folder_id = {self.root_folder_id}")
if self.impersonate is not None:
lines.append(f"impersonate = {self.impersonate}")
if self.owned_by is not None:
lines.append(f"owned_by = {self.owned_by}")
return lines
1 change: 1 addition & 0 deletions tests/sandbox/test_compatibility_guards.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ def test_core_sandbox_public_export_surface_is_stable() -> None:
"agents.sandbox.entries": {
"AzureBlobMount",
"BaseEntry",
"BoxMount",
"Dir",
"File",
"DockerVolumeMountStrategy",
Expand Down
61 changes: 61 additions & 0 deletions tests/sandbox/test_docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from agents.sandbox.config import DEFAULT_PYTHON_SANDBOX_IMAGE
from agents.sandbox.entries import (
AzureBlobMount,
BoxMount,
Dir,
DockerVolumeMountStrategy,
File,
Expand Down Expand Up @@ -1613,6 +1614,66 @@ async def test_docker_create_container_mounts_azure_with_rclone_driver(
]


@pytest.mark.asyncio
async def test_docker_create_container_mounts_box_with_rclone_driver(
monkeypatch: pytest.MonkeyPatch,
) -> None:
container = _ResumeContainer(status="created")
docker_client = _FakeCreateDockerClient(container)
client = DockerSandboxClient(docker_client=cast(object, docker_client))
manifest = Manifest(
entries={
"data": BoxMount(
path="/Shared/Finance",
client_id="client-id",
client_secret="client-secret",
access_token="access-token",
root_folder_id="12345",
impersonate="user-42",
mount_strategy=DockerVolumeMountStrategy(driver="rclone"),
read_only=False,
)
}
)

monkeypatch.setattr(client, "image_exists", lambda _image: True)

created = await client._create_container(DEFAULT_PYTHON_SANDBOX_IMAGE, manifest=manifest)

assert created is container
assert docker_client.containers.calls == [
{
"entrypoint": ["tail"],
"image": DEFAULT_PYTHON_SANDBOX_IMAGE,
"detach": True,
"command": ["-f", "/dev/null"],
"environment": {},
"mounts": [
{
"Target": "/workspace/data",
"Source": "sandbox_ac6cdb3eb035_workspace_data",
"Type": "volume",
"ReadOnly": False,
"VolumeOptions": {
"DriverConfig": {
"Name": "rclone",
"Options": {
"type": "box",
"path": "Shared/Finance",
"box-client-id": "client-id",
"box-client-secret": "client-secret",
"box-access-token": "access-token",
"box-root-folder-id": "12345",
"box-impersonate": "user-42",
},
}
},
}
],
}
]


@pytest.mark.asyncio
async def test_docker_delete_removes_generated_docker_volumes() -> None:
session_id = uuid.UUID("12345678-1234-5678-1234-567812345678")
Expand Down
Loading
Loading