refactor(sandbox): share E2B rclone setup (#3798)
This commit is contained in:
@@ -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})
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user