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
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@
"*test*.py"
],
"python.testing.pytestEnabled": false,
"python.testing.unittestEnabled": true
"python.testing.unittestEnabled": true,
"python.languageServer": "None"
}
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,7 @@ Current built-in sandbox types:
Tool loading rules:

- Tools are only initialized and available when explicitly declared in `tools_config`
- Tools validate `required_sandbox_type`, automatically ignored if mismatched
- Tools validate `required_sandbox_types`, automatically ignored if mismatched

Example:

Expand Down
2 changes: 1 addition & 1 deletion README_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,7 @@ asyncio.run(main())
工具加载规则:

- 仅当在 `tools_config` 中显式声明时,工具才会初始化并可用
- 工具会校验 `required_sandbox_type`,不匹配则自动忽略
- 工具会校验 `required_sandbox_types`,不匹配则自动忽略

示例:

Expand Down
16 changes: 8 additions & 8 deletions docs/en/docs/advanced/customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,18 @@ Tips:
## Custom Tool

Implement/override:
- `required_sandbox_type`: declare compatible sandbox type (return `None` for any).
- `required_sandbox_types`: declare compatible sandbox types (return `None` for any).
- `async def execute(self, sandbox_context, **kwargs)`: implement tool logic and return `ToolResult`.
- Optional constructor args: `name/description/parameters/enabled/timeout`. If no params are needed, you may omit `parameters`.

Notes:
- The framework exposes an OpenAI-style function schema via `Tool.schema`. For strict validation, pass a Pydantic model via `parameters` (see `tools/tool_info.py`).
- Compatibility is checked by `Tool.is_compatible_with_sandbox` using `required_sandbox_type` plus `SandboxType.is_compatible`.
- Compatibility is checked by `Tool.is_compatible_with_sandbox` using `required_sandbox_types` plus `SandboxType.is_compatible`.

### Example A: Minimal tool (no sandbox command)

```python
from typing import Any, Dict, Optional
from typing import Any, Dict, List, Optional
from ms_enclave.sandbox.tools.base import Tool, register_tool
from ms_enclave.sandbox.model import SandboxType

Expand All @@ -50,7 +50,7 @@ class HelloTool(Tool):
super().__init__(name=name, description=description, enabled=enabled)

@property
def required_sandbox_type(self) -> Optional[SandboxType]:
def required_sandbox_types(self) -> Optional[List[SandboxType]]:
return None

async def execute(self, sandbox_context: Any, name: str = 'world', **kwargs) -> Dict[str, Any]:
Expand All @@ -76,7 +76,7 @@ asyncio.run(main())
### Example B: Prefer in-sandbox command with local fallback

```python
from typing import Any, Dict, Optional
from typing import Any, Dict, List, Optional
from datetime import datetime, timezone
from ms_enclave.sandbox.tools.base import Tool, register_tool
from ms_enclave.sandbox.model import SandboxType
Expand All @@ -87,8 +87,8 @@ class TimeTellerTool(Tool):
super().__init__(name=name, description=description, enabled=enabled)

@property
def required_sandbox_type(self) -> Optional[SandboxType]:
return SandboxType.DOCKER
def required_sandbox_types(self) -> Optional[List[SandboxType]]:
return [SandboxType.DOCKER]

async def execute(self, sandbox_context: Any, timezone_name: Optional[str] = None, **kwargs) -> Dict[str, Any]:
cmd = 'date'
Expand Down Expand Up @@ -383,7 +383,7 @@ asyncio.run(main())
- Only execute tools when status is `SandboxStatus.RUNNING`.
- Call `await self.initialize_tools()` in `start()`.
- Compatibility:
- Tools should declare `required_sandbox_type`; return `None` if no restriction.
- Tools should declare `required_sandbox_types`; return `None` if no restriction.
- `SandboxType.is_compatible` enables subtypes to reuse parent tools (e.g., `DOCKER_NOTEBOOK` with `DOCKER`).
- Parameter schema:
- Pass a Pydantic model via `parameters` for validation and documentation. Otherwise, `parameters` defaults to `{}`.
Expand Down
18 changes: 9 additions & 9 deletions docs/zh/docs/advanced/customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,19 @@ ms-enclave 采用高度模块化的设计,支持按需扩展 Tool、Sandbox
## 自定义 Tool

必须实现/覆写:
- `required_sandbox_type`:声明该工具可运行的沙箱类型(返回 `None` 表示所有类型均可)。
- `required_sandbox_types`:声明该工具可运行的沙箱类型列表(返回 `None` 表示所有类型均可)。
- `async def execute(self, sandbox_context, **kwargs)`:执行工具逻辑,返回 `ToolResult`(字典即可)。
- 可选:构造函数参数 `name/description/parameters/enabled/timeout`。若不需要参数,可以不设置 `parameters`。

注意:
- 框架会通过 `Tool.schema` 暴露 OpenAI 风格的 function schema。若需要严格的参数校验,可传入 `parameters`(Pydantic 模型,参见 `tools/tool_info.py`)。
- 工具与沙箱兼容性:`Tool.is_compatible_with_sandbox` 会根据 `required_sandbox_type` 与 `SandboxType.is_compatible` 判定。
- 工具与沙箱兼容性:`Tool.is_compatible_with_sandbox` 会根据 `required_sandbox_types` 与 `SandboxType.is_compatible` 判定。

### 示例 A:最小可用工具(不依赖沙箱命令)

```python
# 最少依赖:返回问候语,任何沙箱可用
from typing import Any, Dict, Optional
from typing import Any, Dict, List, Optional
from ms_enclave.sandbox.tools.base import Tool, register_tool
from ms_enclave.sandbox.model import SandboxType # 仅用于类型声明

Expand All @@ -54,7 +54,7 @@ class HelloTool(Tool):
super().__init__(name=name, description=description, enabled=enabled)

@property
def required_sandbox_type(self) -> Optional[SandboxType]:
def required_sandbox_types(self) -> Optional[List[SandboxType]]:
# None => 任何沙箱可用
return None

Expand Down Expand Up @@ -82,7 +82,7 @@ asyncio.run(main())

```python
# 在沙箱内执行 `date` 命令;若不可用则回退到 Python 计算当前时间
from typing import Any, Dict, Optional
from typing import Any, Dict, List, Optional
from datetime import datetime, timezone
from ms_enclave.sandbox.tools.base import Tool, register_tool
from ms_enclave.sandbox.model import SandboxType
Expand All @@ -93,9 +93,9 @@ class TimeTellerTool(Tool):
super().__init__(name=name, description=description, enabled=enabled)

@property
def required_sandbox_type(self) -> Optional[SandboxType]:
# DOCKER 工具在 DOCKER、DOCKER_NOTEBOOK 等兼容类型中可用
return SandboxType.DOCKER
def required_sandbox_types(self) -> Optional[List[SandboxType]]:
# 声明为 DOCKER => 在 DOCKER、DOCKER_NOTEBOOK 等兼容类型中可用
return [SandboxType.DOCKER]

async def execute(self, sandbox_context: Any, timezone_name: Optional[str] = None, **kwargs) -> Dict[str, Any]:
cmd = 'date'
Expand Down Expand Up @@ -405,7 +405,7 @@ asyncio.run(main())
- 只有在 `SandboxStatus.RUNNING` 时才应执行工具。
- 在 `start()` 中调用 `await self.initialize_tools()`,确保工具就绪。
- 兼容性:
- 工具应通过 `required_sandbox_type` 明确要求;若无要求,返回 `None`。
- 工具应通过 `required_sandbox_types` 明确要求;若无要求,返回 `None`。
- `SandboxType.is_compatible` 用于允许子类型复用父类型工具(例如:`DOCKER_NOTEBOOK` 兼容 `DOCKER` 工具)。
- 参数 Schema:
- 若需要严格参数校验/文档化,在构造 Tool 时传入 `parameters`(Pydantic 模型)。未指定时,schema 的 `parameters` 为 `{}`。
Expand Down
79 changes: 79 additions & 0 deletions examples/volcengine_usage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""Example: use the VolcEngine / SandboxFusion stateless sandbox.

Prerequisite — start the SandboxFusion HTTP service manually::

docker run -it -p 8080:8080 \
vemlp-cn-beijing.cr.volces.com/preset-images/code-sandbox:server-20250609

Then run::

python examples/volcengine_usage.py
"""

from __future__ import annotations

import asyncio

from ms_enclave.sandbox.manager import VolcengineSandboxManager
from ms_enclave.sandbox.model import SandboxType, VolcengineSandboxConfig, VolcengineSandboxManagerConfig


async def main() -> None:
manager_config = VolcengineSandboxManagerConfig(
base_url='http://localhost:8080',
max_concurrency=4,
request_timeout=30.0,
)

sandbox_config = VolcengineSandboxConfig(
tools_config=['python_executor', 'shell_executor', 'multi_code_executor'],
)

async with VolcengineSandboxManager(config=manager_config) as manager:
sandbox_id = await manager.create_sandbox(SandboxType.VOLCENGINE, sandbox_config)
print(f'Created sandbox: {sandbox_id}')
print('Available tools:', list((await manager.get_sandbox_tools(sandbox_id)).keys()))

# 1) Python
r = await manager.execute_tool(
sandbox_id,
'python_executor',
{'code': 'print("hello from python:", 1 + 2)'},
)
print('\n[python_executor]', r.status.value)
print('stdout:', r.output)
if r.error:
print('stderr:', r.error)

# 2) Shell
r = await manager.execute_tool(
sandbox_id,
'shell_executor',
{'command': 'echo hello from bash && uname -a'},
)
print('\n[shell_executor]', r.status.value)
print('stdout:', r.output)
if r.error:
print('stderr:', r.error)

# 3) Multi-language: C++
cpp_code = (
'#include <iostream>\n'
'int main() {\n'
' std::cout << "hello from c++" << std::endl;\n'
' return 0;\n'
'}\n'
)
r = await manager.execute_tool(
sandbox_id,
'multi_code_executor',
{'language': 'cpp', 'code': cpp_code},
)
print('\n[multi_code_executor / cpp]', r.status.value)
print('stdout:', r.output)
if r.error:
print('stderr:', r.error)


if __name__ == '__main__':
asyncio.run(main())
4 changes: 4 additions & 0 deletions ms_enclave/sandbox/boxes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,18 @@
from .base import Sandbox, SandboxFactory, register_sandbox
from .docker_notebook import DockerNotebookSandbox
from .docker_sandbox import DockerSandbox
from .stateless_sandbox import StatelessSandbox
from .volcengine import VolcengineSandbox

__all__ = [
# Base interfaces
'Sandbox',
'SandboxFactory',
'register_sandbox',
'StatelessSandbox',

# Implementations
'DockerSandbox',
'DockerNotebookSandbox',
'VolcengineSandbox',
]
7 changes: 6 additions & 1 deletion ms_enclave/sandbox/boxes/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
SandboxStatus,
SandboxType,
ToolResult,
VolcengineSandboxConfig,
)
from ..tools import Tool, ToolFactory

Expand Down Expand Up @@ -100,7 +101,7 @@ def add_tool(self, tool: Tool) -> None:
self._tools[tool.name] = tool
else:
logger.warning(
f"Tool '{tool.name}' requires sandbox type '{tool.required_sandbox_type}' "
f"Tool '{tool.name}' requires sandbox types '{tool.required_sandbox_types}' "
f"but this is a '{self.sandbox_type}' sandbox. "
f'Compatible types: {SandboxType.get_compatible_types(self.sandbox_type)}'
)
Expand Down Expand Up @@ -237,13 +238,17 @@ def create_sandbox(
config = DockerSandboxConfig()
elif sandbox_type == SandboxType.DOCKER_NOTEBOOK:
config = DockerNotebookConfig()
elif sandbox_type == SandboxType.VOLCENGINE:
config = VolcengineSandboxConfig()
else:
Comment thread
Yunnglin marked this conversation as resolved.
config = SandboxConfig()
elif isinstance(config, dict):
if sandbox_type == SandboxType.DOCKER:
config = DockerSandboxConfig(**config)
elif sandbox_type == SandboxType.DOCKER_NOTEBOOK:
config = DockerNotebookConfig(**config)
elif sandbox_type == SandboxType.VOLCENGINE:
config = VolcengineSandboxConfig(**config)
else:
Comment thread
Yunnglin marked this conversation as resolved.
config = SandboxConfig(**config)

Expand Down
Loading
Loading