From bf299b8f6d44b36c8193a46f64b957b47bf11f39 Mon Sep 17 00:00:00 2001 From: Sodawyx Date: Tue, 3 Mar 2026 17:38:58 +0800 Subject: [PATCH] fix(sandbox): add optional configurations for OSS, NAS, and PolarFS - Introduced optional parameters for OSSMountConfig, NASConfig, and PolarFsConfig in SandboxToolSet and its subclasses. - Updated sandbox_toolset function to accept new configuration options. - Adjusted imports in sandbox module to include PolarFsMountConfig. This enhancement allows for more flexible sandbox configurations, improving integration capabilities. Signed-off-by: Sodawyx --- agentrun/integration/builtin/sandbox.py | 32 ++ agentrun/sandbox/__init__.py | 3 +- .../test_sandbox_toolset_storage_config.py | 282 ++++++++++++++++++ 3 files changed, 316 insertions(+), 1 deletion(-) create mode 100644 tests/unittests/integration/test_sandbox_toolset_storage_config.py diff --git a/agentrun/integration/builtin/sandbox.py b/agentrun/integration/builtin/sandbox.py index a61b357..0456799 100644 --- a/agentrun/integration/builtin/sandbox.py +++ b/agentrun/integration/builtin/sandbox.py @@ -14,6 +14,7 @@ if TYPE_CHECKING: from agentrun.sandbox.api.playwright_sync import BrowserPlaywrightSync + from agentrun.sandbox.model import NASConfig, OSSMountConfig, PolarFsConfig try: from playwright.sync_api import Error as PlaywrightError @@ -38,6 +39,9 @@ def __init__( *, sandbox_idle_timeout_seconds: int, config: Optional[Config], + oss_mount_config: Optional["OSSMountConfig"] = None, + nas_config: Optional["NASConfig"] = None, + polar_fs_config: Optional["PolarFsConfig"] = None, ): super().__init__() @@ -49,6 +53,10 @@ def __init__( self.template_type = template_type self.sandbox_idle_timeout_seconds = sandbox_idle_timeout_seconds + self.oss_mount_config = oss_mount_config + self.nas_config = nas_config + self.polar_fs_config = polar_fs_config + self.sandbox: Optional[Sandbox] = None self.sandbox_id = "" @@ -73,6 +81,9 @@ def _ensure_sandbox(self): template_type=self.template_type, template_name=self.template_name, sandbox_idle_timeout_seconds=self.sandbox_idle_timeout_seconds, + oss_mount_config=self.oss_mount_config, + nas_config=self.nas_config, + polar_fs_config=self.polar_fs_config, config=self.config, ) self.sandbox_id = self.sandbox.sandbox_id @@ -182,12 +193,18 @@ def __init__( template_name: str, config: Optional[Config], sandbox_idle_timeout_seconds: int, + oss_mount_config: Optional["OSSMountConfig"] = None, + nas_config: Optional["NASConfig"] = None, + polar_fs_config: Optional["PolarFsConfig"] = None, ) -> None: super().__init__( template_name=template_name, template_type=TemplateType.CODE_INTERPRETER, sandbox_idle_timeout_seconds=sandbox_idle_timeout_seconds, config=config, + oss_mount_config=oss_mount_config, + nas_config=nas_config, + polar_fs_config=polar_fs_config, ) # ==================== 健康检查 / Health Check ==================== @@ -695,6 +712,9 @@ def __init__( template_name: str, config: Optional[Config], sandbox_idle_timeout_seconds: int, + oss_mount_config: Optional["OSSMountConfig"] = None, + nas_config: Optional["NASConfig"] = None, + polar_fs_config: Optional["PolarFsConfig"] = None, ) -> None: super().__init__( @@ -702,6 +722,9 @@ def __init__( template_type=TemplateType.BROWSER, sandbox_idle_timeout_seconds=sandbox_idle_timeout_seconds, config=config, + oss_mount_config=oss_mount_config, + nas_config=nas_config, + polar_fs_config=polar_fs_config, ) self._playwright_sync: Optional["BrowserPlaywrightSync"] = None @@ -1349,6 +1372,9 @@ def sandbox_toolset( template_type: TemplateType = TemplateType.CODE_INTERPRETER, config: Optional[Config] = None, sandbox_idle_timeout_seconds: int = 5 * 60, + oss_mount_config: Optional["OSSMountConfig"] = None, + nas_config: Optional["NASConfig"] = None, + polar_fs_config: Optional["PolarFsConfig"] = None, ) -> CommonToolSet: """将沙箱模板封装为 LangChain ``StructuredTool`` 列表。""" @@ -1357,10 +1383,16 @@ def sandbox_toolset( template_name=template_name, config=config, sandbox_idle_timeout_seconds=sandbox_idle_timeout_seconds, + oss_mount_config=oss_mount_config, + nas_config=nas_config, + polar_fs_config=polar_fs_config, ) else: return CodeInterpreterToolSet( template_name=template_name, config=config, sandbox_idle_timeout_seconds=sandbox_idle_timeout_seconds, + oss_mount_config=oss_mount_config, + nas_config=nas_config, + polar_fs_config=polar_fs_config, ) diff --git a/agentrun/sandbox/__init__.py b/agentrun/sandbox/__init__.py index 65aaa96..5390060 100644 --- a/agentrun/sandbox/__init__.py +++ b/agentrun/sandbox/__init__.py @@ -17,6 +17,7 @@ OSSMountPoint, PageableInput, PolarFsConfig, + PolarFsMountConfig, SandboxInput, TemplateArmsConfiguration, TemplateContainerConfiguration, @@ -66,6 +67,6 @@ "OSSMountConfig", "OSSMountPoint", "PolarFsConfig", - "PolarFsConfig", + "PolarFsMountConfig", "CustomSandbox", ] diff --git a/tests/unittests/integration/test_sandbox_toolset_storage_config.py b/tests/unittests/integration/test_sandbox_toolset_storage_config.py new file mode 100644 index 0000000..609edf9 --- /dev/null +++ b/tests/unittests/integration/test_sandbox_toolset_storage_config.py @@ -0,0 +1,282 @@ +"""SandboxToolSet 存储挂载配置透传单元测试 + +Tests that oss_mount_config, nas_config, and polar_fs_config are correctly +stored and passed through to Sandbox.create() in the ToolSet hierarchy. +""" + +import threading +from unittest.mock import MagicMock, patch + +import pytest + +from agentrun.integration.builtin.sandbox import ( + BrowserToolSet, + CodeInterpreterToolSet, + sandbox_toolset, + SandboxToolSet, +) +from agentrun.sandbox.model import ( + NASConfig, + NASMountConfig, + OSSMountConfig, + OSSMountPoint, + PolarFsConfig, + PolarFsMountConfig, + TemplateType, +) + + +@pytest.fixture +def oss_config(): + return OSSMountConfig( + mount_points=[ + OSSMountPoint( + bucket_name="test-bucket", + bucket_path="/data", + endpoint="oss-cn-hangzhou.aliyuncs.com", + mount_dir="/mnt/oss", + read_only=False, + ) + ] + ) + + +@pytest.fixture +def nas_config(): + return NASConfig( + group_id=1000, + user_id=1000, + mount_points=[ + NASMountConfig( + enable_tls=True, + mount_dir="/mnt/nas", + server_addr="file-system-id.cn-hangzhou.nas.aliyuncs.com", + ) + ], + ) + + +@pytest.fixture +def polar_fs_config(): + return PolarFsConfig( + group_id=1000, + user_id=1000, + mount_points=[ + PolarFsMountConfig( + instance_id="polar-instance-001", + mount_dir="/mnt/polar", + remote_dir="/shared", + ) + ], + ) + + +class TestSandboxToolSetStorageConfig: + """Test that SandboxToolSet base class stores storage configs correctly.""" + + def test_stores_all_storage_configs( + self, oss_config, nas_config, polar_fs_config + ): + with patch.object(SandboxToolSet, "__init__", lambda self, **kw: None): + ts = SandboxToolSet.__new__(SandboxToolSet) + + SandboxToolSet.__init__( + ts, + template_name="test-tpl", + template_type=TemplateType.CODE_INTERPRETER, + sandbox_idle_timeout_seconds=600, + config=None, + oss_mount_config=oss_config, + nas_config=nas_config, + polar_fs_config=polar_fs_config, + ) + + assert ts.oss_mount_config is oss_config + assert ts.nas_config is nas_config + assert ts.polar_fs_config is polar_fs_config + + def test_defaults_to_none(self): + with patch.object(SandboxToolSet, "__init__", lambda self, **kw: None): + ts = SandboxToolSet.__new__(SandboxToolSet) + + SandboxToolSet.__init__( + ts, + template_name="test-tpl", + template_type=TemplateType.CODE_INTERPRETER, + sandbox_idle_timeout_seconds=600, + config=None, + ) + + assert ts.oss_mount_config is None + assert ts.nas_config is None + assert ts.polar_fs_config is None + + +class TestEnsureSandboxPassthrough: + """Test that _ensure_sandbox passes storage configs to Sandbox.create().""" + + @patch("agentrun.integration.builtin.sandbox.Sandbox") + def test_passes_all_storage_configs( + self, mock_sandbox_cls, oss_config, nas_config, polar_fs_config + ): + mock_sb = MagicMock() + mock_sb.sandbox_id = "sb-123" + mock_sandbox_cls.create.return_value = mock_sb + + ts = CodeInterpreterToolSet( + template_name="test-tpl", + config=None, + sandbox_idle_timeout_seconds=600, + oss_mount_config=oss_config, + nas_config=nas_config, + polar_fs_config=polar_fs_config, + ) + + ts._ensure_sandbox() + + mock_sandbox_cls.create.assert_called_once_with( + template_type=TemplateType.CODE_INTERPRETER, + template_name="test-tpl", + sandbox_idle_timeout_seconds=600, + oss_mount_config=oss_config, + nas_config=nas_config, + polar_fs_config=polar_fs_config, + config=None, + ) + + @patch("agentrun.integration.builtin.sandbox.Sandbox") + def test_passes_none_when_not_provided(self, mock_sandbox_cls): + mock_sb = MagicMock() + mock_sb.sandbox_id = "sb-456" + mock_sandbox_cls.create.return_value = mock_sb + + ts = CodeInterpreterToolSet( + template_name="test-tpl", + config=None, + sandbox_idle_timeout_seconds=600, + ) + + ts._ensure_sandbox() + + mock_sandbox_cls.create.assert_called_once_with( + template_type=TemplateType.CODE_INTERPRETER, + template_name="test-tpl", + sandbox_idle_timeout_seconds=600, + oss_mount_config=None, + nas_config=None, + polar_fs_config=None, + config=None, + ) + + +class TestCodeInterpreterToolSetStorageConfig: + """Test CodeInterpreterToolSet correctly passes storage configs to base.""" + + def test_passes_storage_configs_to_base( + self, oss_config, nas_config, polar_fs_config + ): + ts = CodeInterpreterToolSet( + template_name="ci-tpl", + config=None, + sandbox_idle_timeout_seconds=300, + oss_mount_config=oss_config, + nas_config=nas_config, + polar_fs_config=polar_fs_config, + ) + + assert ts.oss_mount_config is oss_config + assert ts.nas_config is nas_config + assert ts.polar_fs_config is polar_fs_config + assert ts.template_type == TemplateType.CODE_INTERPRETER + + def test_backward_compatible_without_storage_configs(self): + ts = CodeInterpreterToolSet( + template_name="ci-tpl", + config=None, + sandbox_idle_timeout_seconds=300, + ) + + assert ts.oss_mount_config is None + assert ts.nas_config is None + assert ts.polar_fs_config is None + + +class TestBrowserToolSetStorageConfig: + """Test BrowserToolSet correctly passes storage configs to base.""" + + def test_passes_storage_configs_to_base( + self, oss_config, nas_config, polar_fs_config + ): + ts = BrowserToolSet( + template_name="br-tpl", + config=None, + sandbox_idle_timeout_seconds=300, + oss_mount_config=oss_config, + nas_config=nas_config, + polar_fs_config=polar_fs_config, + ) + + assert ts.oss_mount_config is oss_config + assert ts.nas_config is nas_config + assert ts.polar_fs_config is polar_fs_config + assert ts.template_type == TemplateType.BROWSER + + def test_backward_compatible_without_storage_configs(self): + ts = BrowserToolSet( + template_name="br-tpl", + config=None, + sandbox_idle_timeout_seconds=300, + ) + + assert ts.oss_mount_config is None + assert ts.nas_config is None + assert ts.polar_fs_config is None + + +class TestSandboxToolsetFactory: + """Test sandbox_toolset factory function passes storage configs.""" + + def test_code_interpreter_with_storage_configs( + self, oss_config, nas_config, polar_fs_config + ): + ts = sandbox_toolset( + "ci-tpl", + template_type=TemplateType.CODE_INTERPRETER, + sandbox_idle_timeout_seconds=600, + oss_mount_config=oss_config, + nas_config=nas_config, + polar_fs_config=polar_fs_config, + ) + + assert isinstance(ts, CodeInterpreterToolSet) + assert ts.oss_mount_config is oss_config + assert ts.nas_config is nas_config + assert ts.polar_fs_config is polar_fs_config + + def test_browser_with_storage_configs( + self, oss_config, nas_config, polar_fs_config + ): + ts = sandbox_toolset( + "br-tpl", + template_type=TemplateType.BROWSER, + sandbox_idle_timeout_seconds=600, + oss_mount_config=oss_config, + nas_config=nas_config, + polar_fs_config=polar_fs_config, + ) + + assert isinstance(ts, BrowserToolSet) + assert ts.oss_mount_config is oss_config + assert ts.nas_config is nas_config + assert ts.polar_fs_config is polar_fs_config + + def test_factory_backward_compatible(self): + ts = sandbox_toolset( + "ci-tpl", + template_type=TemplateType.CODE_INTERPRETER, + ) + + assert isinstance(ts, CodeInterpreterToolSet) + assert ts.oss_mount_config is None + assert ts.nas_config is None + assert ts.polar_fs_config is None