refactor(sandbox): share E2B rclone setup (#3798)

This commit is contained in:
Kazuhiro Sera
2026-07-11 07:32:27 +09:00
committed by GitHub
parent 4c5d630c45
commit bc0dbb3ead
3 changed files with 90 additions and 71 deletions
+82
View File
@@ -0,0 +1,82 @@
from __future__ import annotations
from ...sandbox.entries.mounts.patterns import RcloneMountPattern
from ...sandbox.errors import MountConfigError
from ...sandbox.session.base_sandbox_session import BaseSandboxSession
_APT = "DEBIAN_FRONTEND=noninteractive DEBCONF_NOWARNINGS=yes apt-get -o Dpkg::Use-Pty=0"
_RCLONE_CHECK = "command -v rclone >/dev/null 2>&1 || test -x /usr/local/bin/rclone"
_INSTALL_RCLONE_COMMANDS = (
f"{_APT} update -qq",
f"{_APT} install -y -qq curl unzip ca-certificates",
"curl -fsSL https://rclone.org/install.sh | bash",
)
async def ensure_rclone(session: BaseSandboxSession) -> None:
rclone = await session.exec("sh", "-lc", _RCLONE_CHECK, shell=False)
if rclone.ok():
return
apt = await session.exec("sh", "-lc", "command -v apt-get >/dev/null 2>&1", shell=False)
if not apt.ok():
raise MountConfigError(
message="rclone is not installed and apt-get is unavailable; preinstall rclone",
context={"package": "rclone"},
)
for command in _INSTALL_RCLONE_COMMANDS:
install = await session.exec(
"sh",
"-lc",
command,
shell=False,
timeout=300,
user="root",
)
if not install.ok():
raise MountConfigError(
message="failed to install rclone",
context={"package": "rclone", "exit_code": install.exit_code},
)
rclone = await session.exec("sh", "-lc", _RCLONE_CHECK, shell=False)
if not rclone.ok():
raise MountConfigError(
message="rclone was installed but is still not available on PATH",
context={"package": "rclone"},
)
async def _default_user_ids(session: BaseSandboxSession) -> tuple[str, str] | None:
result = await session.exec("sh", "-lc", "id -u; id -g", shell=False, timeout=30)
if not result.ok():
return None
lines = result.stdout.decode("utf-8", errors="replace").splitlines()
if len(lines) < 2 or not lines[0].isdigit() or not lines[1].isdigit():
return None
return lines[0], lines[1]
def _append_option(args: list[str], option: str, *values: str) -> None:
if option not in args:
args.extend([option, *values])
async def rclone_pattern_for_session(
session: BaseSandboxSession,
pattern: RcloneMountPattern,
) -> RcloneMountPattern:
if pattern.mode != "fuse":
return pattern
extra_args = list(pattern.extra_args)
_append_option(extra_args, "--allow-other")
user_ids = await _default_user_ids(session)
if user_ids is not None:
uid, gid = user_ids
_append_option(extra_args, "--uid", uid)
_append_option(extra_args, "--gid", gid)
return pattern.model_copy(update={"extra_args": extra_args})
+4 -69
View File
@@ -10,14 +10,11 @@ from ....sandbox.entries.mounts.patterns import RcloneMountPattern
from ....sandbox.errors import MountConfigError
from ....sandbox.materialization import MaterializedFile
from ....sandbox.session.base_sandbox_session import BaseSandboxSession
_APT = "DEBIAN_FRONTEND=noninteractive DEBCONF_NOWARNINGS=yes apt-get -o Dpkg::Use-Pty=0"
_RCLONE_CHECK = "command -v rclone >/dev/null 2>&1 || test -x /usr/local/bin/rclone"
_INSTALL_RCLONE_COMMANDS = (
f"{_APT} update -qq",
f"{_APT} install -y -qq curl unzip ca-certificates",
"curl -fsSL https://rclone.org/install.sh | bash",
from .._rclone import (
ensure_rclone as _ensure_rclone,
rclone_pattern_for_session as _rclone_pattern_for_session,
)
_FUSE_ALLOW_OTHER = (
"chmod a+rw /dev/fuse && "
"touch /etc/fuse.conf && "
@@ -55,68 +52,6 @@ async def _ensure_fuse_support(session: BaseSandboxSession) -> None:
)
async def _ensure_rclone(session: BaseSandboxSession) -> None:
rclone = await session.exec("sh", "-lc", _RCLONE_CHECK, shell=False)
if rclone.ok():
return
apt = await session.exec("sh", "-lc", "command -v apt-get >/dev/null 2>&1", shell=False)
if not apt.ok():
raise MountConfigError(
message="rclone is not installed and apt-get is unavailable; preinstall rclone",
context={"package": "rclone"},
)
for command in _INSTALL_RCLONE_COMMANDS:
install = await session.exec("sh", "-lc", command, shell=False, timeout=300, user="root")
if not install.ok():
raise MountConfigError(
message="failed to install rclone",
context={"package": "rclone", "exit_code": install.exit_code},
)
rclone = await session.exec("sh", "-lc", _RCLONE_CHECK, shell=False)
if not rclone.ok():
raise MountConfigError(
message="rclone was installed but is still not available on PATH",
context={"package": "rclone"},
)
async def _default_user_ids(session: BaseSandboxSession) -> tuple[str, str] | None:
result = await session.exec("sh", "-lc", "id -u; id -g", shell=False, timeout=30)
if not result.ok():
return None
lines = result.stdout.decode("utf-8", errors="replace").splitlines()
if len(lines) < 2 or not lines[0].isdigit() or not lines[1].isdigit():
return None
return lines[0], lines[1]
def _append_option(args: list[str], option: str, *values: str) -> None:
if option not in args:
args.extend([option, *values])
async def _rclone_pattern_for_session(
session: BaseSandboxSession,
pattern: RcloneMountPattern,
) -> RcloneMountPattern:
if pattern.mode != "fuse":
return pattern
extra_args = list(pattern.extra_args)
_append_option(extra_args, "--allow-other")
user_ids = await _default_user_ids(session)
if user_ids is not None:
uid, gid = user_ids
_append_option(extra_args, "--uid", uid)
_append_option(extra_args, "--gid", gid)
return pattern.model_copy(update={"extra_args": extra_args})
def _assert_e2b_session(session: BaseSandboxSession) -> None:
if type(session).__name__ != "E2BSandboxSession":
raise MountConfigError(
+4 -2
View File
@@ -16,12 +16,14 @@ import pytest
from pydantic import Field, PrivateAttr
import agents.extensions.sandbox.e2b.sandbox as e2b_module
from agents.extensions.sandbox._rclone import (
ensure_rclone as _ensure_rclone,
rclone_pattern_for_session as _rclone_pattern_for_session,
)
from agents.extensions.sandbox.e2b.mounts import (
E2BCloudBucketMountStrategy,
_assert_e2b_session,
_ensure_fuse_support,
_ensure_rclone,
_rclone_pattern_for_session,
)
from agents.extensions.sandbox.e2b.sandbox import (
E2BSandboxClient,