diff --git a/docs/ref/sandbox/entries.md b/docs/ref/sandbox/entries.md
index 47f59d9fc0..f8ddb0a11f 100644
--- a/docs/ref/sandbox/entries.md
+++ b/docs/ref/sandbox/entries.md
@@ -14,3 +14,4 @@
- R2Mount
- S3Mount
- S3FilesMount
+ - BoxMount
diff --git a/docs/sandbox/clients.md b/docs/sandbox/clients.md
index 683e8bc47b..bd21da63d3 100644
--- a/docs/sandbox/clients.md
+++ b/docs/sandbox/clients.md
@@ -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`. |
@@ -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. |
@@ -121,16 +121,16 @@ The table below summarizes which remote storage entries each backend can mount d
-| 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` | - | - | - | - | - | - |
diff --git a/docs/sandbox/guide.md b/docs/sandbox/guide.md
index fb444e0e3d..7e08c6a103 100644
--- a/docs/sandbox/guide.md
+++ b/docs/sandbox/guide.md
@@ -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. |
diff --git a/src/agents/extensions/sandbox/daytona/mounts.py b/src/agents/extensions/sandbox/daytona/mounts.py
index 2d8fc25926..038473e70e 100644
--- a/src/agents/extensions/sandbox/daytona/mounts.py
+++ b/src/agents/extensions/sandbox/daytona/mounts.py
@@ -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
@@ -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::
diff --git a/src/agents/extensions/sandbox/e2b/mounts.py b/src/agents/extensions/sandbox/e2b/mounts.py
index 5b552028af..3e37eda803 100644
--- a/src/agents/extensions/sandbox/e2b/mounts.py
+++ b/src/agents/extensions/sandbox/e2b/mounts.py
@@ -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")
diff --git a/src/agents/extensions/sandbox/runloop/mounts.py b/src/agents/extensions/sandbox/runloop/mounts.py
index d55fa04856..4c1daec892 100644
--- a/src/agents/extensions/sandbox/runloop/mounts.py
+++ b/src/agents/extensions/sandbox/runloop/mounts.py
@@ -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")
diff --git a/src/agents/sandbox/entries/__init__.py b/src/agents/sandbox/entries/__init__.py
index 23b05431cc..a08f6b796d 100644
--- a/src/agents/sandbox/entries/__init__.py
+++ b/src/agents/sandbox/entries/__init__.py
@@ -4,6 +4,7 @@
from .base import BaseEntry, resolve_workspace_path
from .mounts import (
AzureBlobMount,
+ BoxMount,
DockerVolumeMountStrategy,
FuseMountPattern,
GCSMount,
@@ -24,6 +25,7 @@
__all__ = [
"AzureBlobMount",
"BaseEntry",
+ "BoxMount",
"Dir",
"File",
"DockerVolumeMountStrategy",
diff --git a/src/agents/sandbox/entries/mounts/__init__.py b/src/agents/sandbox/entries/mounts/__init__.py
index 61e2dc868a..4c9c5e2a66 100644
--- a/src/agents/sandbox/entries/mounts/__init__.py
+++ b/src/agents/sandbox/entries/mounts/__init__.py
@@ -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",
diff --git a/src/agents/sandbox/entries/mounts/providers/__init__.py b/src/agents/sandbox/entries/mounts/providers/__init__.py
index b5155d3c47..22f46a5623 100644
--- a/src/agents/sandbox/entries/mounts/providers/__init__.py
+++ b/src/agents/sandbox/entries/mounts/providers/__init__.py
@@ -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
@@ -12,4 +13,5 @@
"R2Mount",
"S3Mount",
"S3FilesMount",
+ "BoxMount",
]
diff --git a/src/agents/sandbox/entries/mounts/providers/box.py b/src/agents/sandbox/entries/mounts/providers/box.py
new file mode 100644
index 0000000000..444129159e
--- /dev/null
+++ b/src/agents/sandbox/entries/mounts/providers/box.py
@@ -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
diff --git a/tests/sandbox/test_compatibility_guards.py b/tests/sandbox/test_compatibility_guards.py
index e3ecba3349..ccf71f0441 100644
--- a/tests/sandbox/test_compatibility_guards.py
+++ b/tests/sandbox/test_compatibility_guards.py
@@ -127,6 +127,7 @@ def test_core_sandbox_public_export_surface_is_stable() -> None:
"agents.sandbox.entries": {
"AzureBlobMount",
"BaseEntry",
+ "BoxMount",
"Dir",
"File",
"DockerVolumeMountStrategy",
diff --git a/tests/sandbox/test_docker.py b/tests/sandbox/test_docker.py
index d57ce2e00a..52701274eb 100644
--- a/tests/sandbox/test_docker.py
+++ b/tests/sandbox/test_docker.py
@@ -24,6 +24,7 @@
from agents.sandbox.config import DEFAULT_PYTHON_SANDBOX_IMAGE
from agents.sandbox.entries import (
AzureBlobMount,
+ BoxMount,
Dir,
DockerVolumeMountStrategy,
File,
@@ -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")
diff --git a/tests/sandbox/test_mounts.py b/tests/sandbox/test_mounts.py
index 255ccff788..da1ddbe46a 100644
--- a/tests/sandbox/test_mounts.py
+++ b/tests/sandbox/test_mounts.py
@@ -9,6 +9,7 @@
from agents.sandbox import Manifest
from agents.sandbox.entries import (
AzureBlobMount,
+ BoxMount,
DockerVolumeMountStrategy,
FuseMountPattern,
GCSMount,
@@ -289,6 +290,54 @@ async def test_azure_blob_mount_builds_rclone_runtime_config_without_hidden_patt
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()