diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d41d76d..03662ac 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,6 +29,7 @@ repos: - id: trailing-whitespace exclude: thirdparty/|docs/ - id: check-yaml + args: [--unsafe] exclude: thirdparty/ - id: end-of-file-fixer exclude: thirdparty/|docs/ diff --git a/README.md b/README.md index 0993cb7..01aa0a7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@


- +

diff --git a/README_zh.md b/README_zh.md index d1ed19d..5b0e72a 100644 --- a/README_zh.md +++ b/README_zh.md @@ -1,6 +1,6 @@


- +

diff --git a/doc/asset/image/logo.png b/docs/asset/image/logo.png similarity index 100% rename from doc/asset/image/logo.png rename to docs/asset/image/logo.png diff --git a/docs/en/.readthedocs.yaml b/docs/en/.readthedocs.yaml new file mode 100644 index 0000000..60ebd6e --- /dev/null +++ b/docs/en/.readthedocs.yaml @@ -0,0 +1,15 @@ +version: 2 + +build: + os: "ubuntu-24.04" + tools: + python: "3" + # We recommend using a requirements file for reproducible builds. + # This is just a quick example to get started. + # https://docs.readthedocs.io/page/guides/reproducible-builds.html + jobs: + pre_install: + - pip install mkdocs-material mkdocstrings-python + +mkdocs: + configuration: mkdocs.yml diff --git a/docs/en/docs/advanced/customization.md b/docs/en/docs/advanced/customization.md new file mode 100644 index 0000000..3dca8cf --- /dev/null +++ b/docs/en/docs/advanced/customization.md @@ -0,0 +1,395 @@ +# Customization & Extensions + +ms-enclave is highly modular and supports extending Tool, Sandbox, and SandboxManager. This guide covers required interfaces, registration, and minimal runnable examples to get you productive quickly. + +## Registry Overview + +A decorator-based registry enables type-driven creation: + +- Sandbox: + - Decorator: `@register_sandbox(SandboxType.XYZ)` + - Factory: `SandboxFactory.create_sandbox(sandbox_type, config, sandbox_id)` + +- Tool: + - Decorator: `@register_tool('tool_name')` + - Factory: `ToolFactory.create_tool('tool_name', **kwargs)` + +- SandboxManager: + - Decorator: `@register_manager(SandboxManagerType.XYZ)` + - Factory: `SandboxManagerFactory.create_manager(manager_type, config, **kwargs)` + +Tips: +- When adding a new Sandbox, extend `SandboxType` enum under `ms_enclave/sandbox/model` (e.g., `LOCAL_PROCESS`). +- When adding a new SandboxManager, extend `SandboxManagerType` (e.g., `LOCAL_INMEM`). + +> All examples use English comments and full type hints to match project style. + +--- + +## Custom Tool + +Implement/override: +- `required_sandbox_type`: declare compatible sandbox type (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`. + +### Example A: Minimal tool (no sandbox command) + +```python +from typing import Any, Dict, Optional +from ms_enclave.sandbox.tools.base import Tool, register_tool +from ms_enclave.sandbox.model import SandboxType + +@register_tool('hello') +class HelloTool(Tool): + def __init__(self, name: str = 'hello', description: str = 'Say hello', enabled: bool = True): + super().__init__(name=name, description=description, enabled=enabled) + + @property + def required_sandbox_type(self) -> Optional[SandboxType]: + return None + + async def execute(self, sandbox_context: Any, name: str = 'world', **kwargs) -> Dict[str, Any]: + return {'message': f'Hello, {name}!'} +``` + +Enable and run (Docker sandbox example): +```python +from ms_enclave.sandbox.boxes import SandboxFactory +from ms_enclave.sandbox.model import SandboxType, DockerSandboxConfig +import asyncio + +async def main(): + sb = SandboxFactory.create_sandbox(SandboxType.DOCKER, DockerSandboxConfig(image='python:3.11-slim')) + async with sb: + await sb.initialize_tools() + result = await sb.execute_tool('hello', {'name': 'ms-enclave'}) + print(result) + +asyncio.run(main()) +``` + +### Example B: Prefer in-sandbox command with local fallback + +```python +from typing import Any, Dict, Optional +from datetime import datetime, timezone +from ms_enclave.sandbox.tools.base import Tool, register_tool +from ms_enclave.sandbox.model import SandboxType + +@register_tool('time_teller') +class TimeTellerTool(Tool): + def __init__(self, name: str = 'time_teller', description: str = 'Tell current time', enabled: bool = True): + super().__init__(name=name, description=description, enabled=enabled) + + @property + def required_sandbox_type(self) -> Optional[SandboxType]: + return SandboxType.DOCKER + + async def execute(self, sandbox_context: Any, timezone_name: Optional[str] = None, **kwargs) -> Dict[str, Any]: + cmd = 'date' + if timezone_name: + cmd = f'TZ={timezone_name} date' + try: + exit_code, out, err = await sandbox_context.execute_command(cmd, timeout=5) + if exit_code == 0: + return {'time': out.strip()} + return {'error': err.strip() or 'unknown error'} + except Exception: + tz = timezone.utc if (timezone_name or '').upper() == 'UTC' else None + return {'time': datetime.now(tz=tz).isoformat()} +``` + +Enable via config: +```python +from ms_enclave.sandbox.model import DockerSandboxConfig + +config = DockerSandboxConfig( + image='debian:stable-slim', + tools_config={ + 'hello': {}, + 'time_teller': {} + } +) +``` + +--- + +## Custom Sandbox + +Implement: +- `sandbox_type` +- `start()`, `stop()`, `cleanup()` +- `execute_command(command, timeout=None, stream=True)` +- `get_execution_context()` + +Minimal demo: local process-based sandbox (for development/testing only). Assume you added `LOCAL_PROCESS` to `SandboxType`. + +```python +import asyncio +from typing import Any, Dict, List, Optional, Tuple, Union +from ms_enclave.sandbox.boxes.base import Sandbox, register_sandbox +from ms_enclave.sandbox.model import SandboxType, SandboxStatus, SandboxConfig + +CommandResult = Tuple[int, str, str] + +@register_sandbox(SandboxType.LOCAL_PROCESS) +class LocalProcessSandbox(Sandbox): + """Run host commands as a 'sandbox' (for demo/dev only).""" + + @property + def sandbox_type(self) -> SandboxType: + return SandboxType.LOCAL_PROCESS + + async def start(self) -> None: + self.update_status(SandboxStatus.RUNNING) + await self.initialize_tools() + + async def stop(self) -> None: + self.update_status(SandboxStatus.STOPPED) + + async def cleanup(self) -> None: + return + + async def execute_command( + self, + command: Union[str, List[str]], + timeout: Optional[int] = None, + stream: bool = True + ) -> CommandResult: + if isinstance(command, list): + shell_cmd = ' '.join(command) + else: + shell_cmd = command + + proc = await asyncio.create_subprocess_shell( + shell_cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + try: + if timeout: + stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout) + else: + stdout, stderr = await proc.communicate() + except asyncio.TimeoutError: + proc.kill() + return 124, '', 'command timed out' + return proc.returncode, (stdout or b'').decode(), (stderr or b'').decode() + + async def get_execution_context(self) -> Any: + return None +``` + +Quick check: +```python +from ms_enclave.sandbox.boxes import SandboxFactory +from ms_enclave.sandbox.model import SandboxType, SandboxConfig +import asyncio + +async def main(): + sb = SandboxFactory.create_sandbox(SandboxType.LOCAL_PROCESS, SandboxConfig()) + async with sb: + await sb.initialize_tools() + print(await sb.execute_tool('hello', {'name': 'sandbox'})) + print(await sb.execute_command('echo hi')) + +asyncio.run(main()) +``` + +> Production sandboxes (e.g., Docker) must implement image pull, container create/start, limits, mounts, etc. See `ms_enclave/sandbox/boxes/docker_sandbox.py`. + +--- + +## Custom SandboxManager + +Implement (as defined in `SandboxManager` ABC): +- Lifecycle: `start()`, `stop()` +- Sandbox ops: `create_sandbox()`, `get_sandbox_info()`, `list_sandboxes()`, `stop_sandbox()`, `delete_sandbox()` +- Tool exec: `execute_tool()`, `get_sandbox_tools()` +- Stats: `get_stats()` +- Cleanup: `cleanup_all_sandboxes()` +- Pool (abstract; provide minimal implementation): `initialize_pool()`, `execute_tool_in_pool()` + +Minimal runnable demo: in-memory manager. Assume you added `LOCAL_INMEM` to `SandboxManagerType`. + +```python +import asyncio +from typing import Any, Dict, List, Optional, Union +from ms_enclave.sandbox.manager.base import SandboxManager, register_manager +from ms_enclave.sandbox.boxes import SandboxFactory +from ms_enclave.sandbox.model import ( + SandboxConfig, SandboxInfo, SandboxManagerConfig, SandboxManagerType, + SandboxStatus, SandboxType, ToolResult +) + +@register_manager(SandboxManagerType.LOCAL_INMEM) +class LocalInMemoryManager(SandboxManager): + """A minimal in-memory manager for demo/dev.""" + + def __init__(self, config: Optional[SandboxManagerConfig] = None, **kwargs): + super().__init__(config=config, **kwargs) + self._running = False + + async def start(self) -> None: + self._running = True + + async def stop(self) -> None: + await self.cleanup_all_sandboxes() + self._running = False + + async def create_sandbox( + self, + sandbox_type: SandboxType, + config: Optional[Union[SandboxConfig, Dict]] = None, + sandbox_id: Optional[str] = None + ) -> str: + sb = SandboxFactory.create_sandbox(sandbox_type, config, sandbox_id) + await sb.start() + await sb.initialize_tools() + self._sandboxes[sb.id] = sb + async with self._pool_lock: + self._sandbox_pool.append(sb.id) + return sb.id + + async def get_sandbox_info(self, sandbox_id: str) -> Optional[SandboxInfo]: + sb = self._sandboxes.get(sandbox_id) + return sb.get_info() if sb else None + + async def list_sandboxes(self, status_filter: Optional[SandboxStatus] = None) -> List[SandboxInfo]: + infos = [sb.get_info() for sb in self._sandboxes.values()] + if status_filter: + return [i for i in infos if i.status == status_filter] + return infos + + async def stop_sandbox(self, sandbox_id: str) -> bool: + sb = self._sandboxes.get(sandbox_id) + if not sb: + return False + await sb.stop() + return True + + async def delete_sandbox(self, sandbox_id: str) -> bool: + sb = self._sandboxes.pop(sandbox_id, None) + if not sb: + return False + await sb.cleanup() + async with self._pool_lock: + try: + self._sandbox_pool.remove(sandbox_id) + except ValueError: + pass + return True + + async def execute_tool(self, sandbox_id: str, tool_name: str, parameters: Dict[str, Any]) -> ToolResult: + sb = self._sandboxes.get(sandbox_id) + if not sb: + raise ValueError(f'Sandbox {sandbox_id} not found') + if sb.status != SandboxStatus.RUNNING: + raise ValueError('Sandbox not running') + return await sb.execute_tool(tool_name, parameters) + + async def get_sandbox_tools(self, sandbox_id: str) -> Dict[str, Any]: + sb = self._sandboxes.get(sandbox_id) + if not sb: + raise ValueError(f'Sandbox {sandbox_id} not found') + return sb.get_available_tools() + + async def get_stats(self) -> Dict[str, Any]: + total = len(self._sandboxes) + running = sum(1 for s in self._sandboxes.values() if s.status == SandboxStatus.RUNNING) + return {'total': total, 'running': running} + + async def cleanup_all_sandboxes(self) -> None: + for sb in list(self._sandboxes.values()): + try: + await sb.stop() + await sb.cleanup() + except Exception: + pass + self._sandboxes.clear() + async with self._pool_lock: + self._sandbox_pool.clear() + + async def initialize_pool( + self, + pool_size: Optional[int] = None, + sandbox_type: Optional[SandboxType] = None, + config: Optional[Union[SandboxConfig, Dict]] = None + ) -> List[str]: + if self._pool_initialized: + return list(self._sandbox_pool) + size = pool_size or (self.config.pool_size if self.config else 0) or 0 + if size <= 0: + self._pool_initialized = True + return [] + st = sandbox_type or (self.config.sandbox_type if self.config else None) + if not st: + raise ValueError('sandbox_type required for pool initialization') + ids: List[str] = [] + for _ in range(size): + sb_id = await self.create_sandbox(st, config or (self.config.sandbox_config if self.config else None)) + ids.append(sb_id) + self._pool_initialized = True + return ids + + async def execute_tool_in_pool( + self, tool_name: str, parameters: Dict[str, Any], timeout: Optional[float] = None + ) -> ToolResult: + async def acquire_one() -> str: + start = asyncio.get_event_loop().time() + while True: + async with self._pool_lock: + if self._sandbox_pool: + return self._sandbox_pool.popleft() + if timeout and (asyncio.get_event_loop().time() - start) > timeout: + raise TimeoutError('No sandbox available from pool') + await asyncio.sleep(0.05) + + sandbox_id = await acquire_one() + try: + return await self.execute_tool(sandbox_id, tool_name, parameters) + finally: + async with self._pool_lock: + if sandbox_id in self._sandboxes: + self._sandbox_pool.append(sandbox_id) +``` + +Verify: +```python +from ms_enclave.sandbox.manager.base import SandboxManagerFactory +from ms_enclave.sandbox.model import SandboxManagerType, SandboxType, DockerSandboxConfig +import asyncio + +async def main(): + mgr = SandboxManagerFactory.create_manager(SandboxManagerType.LOCAL_INMEM) + async with mgr: + sb_id = await mgr.create_sandbox(SandboxType.DOCKER, DockerSandboxConfig(image='python:3.11-slim')) + print(await mgr.get_sandbox_tools(sb_id)) + print(await mgr.execute_tool(sb_id, 'hello', {'name': 'manager'})) + +asyncio.run(main()) +``` + +--- + +## Best Practices + +- Lifecycle & state: + - 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. + - `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 `{}`. +- Keep it simple: + - Small functions, clear names, English comments, and docstrings. +- Validate fast: + - Start from minimal (e.g., Example A/B) locally, then add pool/network/resources as needed. + +> In practice: for a new Sandbox, refer to `ms_enclave/sandbox/boxes/docker_sandbox.py`. For HTTP API changes, update `server/server.py` and mirror in `manager/http_manager.py` with synchronized Pydantic models. diff --git a/docs/en/docs/advanced/server.md b/docs/en/docs/advanced/server.md new file mode 100644 index 0000000..37b243d --- /dev/null +++ b/docs/en/docs/advanced/server.md @@ -0,0 +1,49 @@ +# Deploy HTTP Server + +ms-enclave ships a FastAPI-based HTTP server that exposes sandbox capabilities to remote clients — useful for distributed systems or microservice-style deployments. + +## Start the server + +### CLI + +```bash +ms-enclave server --host 0.0.0.0 --port 8000 +``` + +## Use HttpSandboxManager client + +Operate remote sandboxes just like local ones: + +```python +import asyncio +from ms_enclave.sandbox.manager import SandboxManagerFactory +from ms_enclave.sandbox.model import DockerSandboxConfig, SandboxType + +async def main(): + async with SandboxManagerFactory.create_manager(base_url='http://127.0.0.1:8000') as manager: + config = DockerSandboxConfig( + image='python:3.11-slim', + tools_config={'python_executor': {}} + ) + + sandbox_id = await manager.create_sandbox(SandboxType.DOCKER, config) + print(f"Remote sandbox ID: {sandbox_id}") + + result = await manager.execute_tool(sandbox_id, 'python_executor', { + 'code': 'import platform; print(platform.node())' + }) + print(f"Remote output: {result.output}") + +if __name__ == '__main__': + asyncio.run(main()) +``` + +## API overview + +- `POST /sandbox/create`: Create sandbox +- `GET /sandboxes`: List sandboxes +- `GET /sandbox/{sandbox_id}`: Get sandbox detail +- `POST /sandbox/{sandbox_id}/stop`: Stop sandbox +- `DELETE /sandbox/{sandbox_id}`: Delete sandbox +- `POST /sandbox/tool/execute`: Execute tool (Body: `ToolExecutionRequest`) +- `GET /sandbox/{sandbox_id}/tools`: List tools available in a sandbox diff --git a/docs/en/docs/api/index.md b/docs/en/docs/api/index.md new file mode 100644 index 0000000..15d80e2 --- /dev/null +++ b/docs/en/docs/api/index.md @@ -0,0 +1,11 @@ +# API Overview + +This section is generated by mkdocstrings from the source code and covers the core interfaces and factories: + +- Managers: `SandboxManager`, `SandboxManagerFactory` +- Sandboxes: `Sandbox`, `SandboxFactory` +- Tools: `Tool`, `ToolFactory` + +Signatures, parameters, and return values are derived from type hints and docstrings in the code. + +> If certain classes/functions are missing, ensure docstrings are complete and the module is included in mkdocstrings `handlers.python.paths` search path. diff --git a/docs/en/docs/api/manager.md b/docs/en/docs/api/manager.md new file mode 100644 index 0000000..56d59a9 --- /dev/null +++ b/docs/en/docs/api/manager.md @@ -0,0 +1,7 @@ +# Manager API + +::: ms_enclave.sandbox.manager.base.SandboxManager + +::: ms_enclave.sandbox.manager.base.SandboxManagerFactory + +::: ms_enclave.sandbox.manager.base.register_manager diff --git a/docs/en/docs/api/sandbox.md b/docs/en/docs/api/sandbox.md new file mode 100644 index 0000000..d2a4799 --- /dev/null +++ b/docs/en/docs/api/sandbox.md @@ -0,0 +1,9 @@ +# Sandbox API + +::: ms_enclave.sandbox.boxes.base.Sandbox + + +::: ms_enclave.sandbox.boxes.base.SandboxFactory + + +::: ms_enclave.sandbox.boxes.base.register_sandbox diff --git a/docs/en/docs/api/tools.md b/docs/en/docs/api/tools.md new file mode 100644 index 0000000..65c4df2 --- /dev/null +++ b/docs/en/docs/api/tools.md @@ -0,0 +1,8 @@ +# Tool API + + +::: ms_enclave.sandbox.tools.base.Tool + +::: ms_enclave.sandbox.tools.base.ToolFactory + +::: ms_enclave.sandbox.tools.base.register_tool diff --git a/docs/en/docs/basic/concepts.md b/docs/en/docs/basic/concepts.md new file mode 100644 index 0000000..35814a8 --- /dev/null +++ b/docs/en/docs/basic/concepts.md @@ -0,0 +1,98 @@ +# Core Concepts + +ms-enclave uses a modular, layered architecture decoupling runtime management, tool execution, and lifecycle maintenance. Understanding the following concepts and their relationships helps you build efficient Agent systems. + +## Architecture Overview + +Relationship between core components: + +```ascii ++------------------+ +----------------------+ +| User / Client | --------> | SandboxManager | ++------------------+ | (Local / HTTP Proxy) | + +----------+-----------+ + | + | 1. Manage + | + v ++----------------------+ +----------------------+ +| SandboxFactory | ----> | Sandbox | <---+ ++----------------------+ 2. | (Docker / Notebook) | | + Create+----------+-----------+ | + | | 4. + | 3. Run | Execute + v | + +----------------------+ | + | Runtime Envrion | | + | (Container / Kernel)| | + +----------------------+ | + | | ++----------------------+ +----------+-----------+ | +| ToolFactory | ----> | Tool | ----+ ++----------------------+ 2. | (Python/Shell/File) | + Create+----------------------+ +``` + +## 1. Sandbox System + +A sandbox is an isolated environment for secure code execution. + +### Sandbox (base class) +`Sandbox` is the central ABC defining standard behaviors. + +- Purpose: Encapsulates runtime details (e.g., Docker API) and provides unified interfaces: `start`, `stop`, `execute_command`. +- Lifecycle states: + - `CREATED`, `STARTING`, `RUNNING`, `STOPPING`, `STOPPED`, `ERROR`. + +### SandboxFactory + +- Purpose: Factory pattern that creates sandbox instances by `SandboxType`. +- Extensibility: Register new implementations via `@register_sandbox` without modifying the factory itself. + +### Implementations & Configs + +- DockerSandbox (`docker`): + - Docker-based sandbox with filesystem and network isolation. + - Key config (`DockerSandboxConfig`): `image`, `cpu_limit`, `memory_limit`, `auto_remove`, `volumes`. +- DockerNotebook (`docker_notebook`): + - Extends DockerSandbox with Jupyter Kernel Gateway for interactive Python via HTTP/WebSocket. + - Inherits Docker config and adds kernel port settings. + +## 2. Tool System + +Tools are the capability surface exposed to the LLM for interacting with the sandbox. + +### Tool (base class) + +- Purpose: Implements concrete operations via `execute(sandbox_context, **kwargs)`. +- Schema: `schema` follows OpenAI Function Calling style for discoverability. + +### ToolFactory + +- Purpose: Central registry/creator for tools. +- Register with `@register_tool("name")`. Enable tools with `tools_config` when creating a sandbox. + +### Common Tools + +- PythonExecutor: Execute Python snippets in the sandbox (non-interactive, or interactive in Notebook sandbox). +- ShellExecutor: Run Bash commands. +- FileOperation: `read_file`, `write_file`, `list_dir`, etc. + +## 3. Management Layer + +Managers orchestrate sandbox lifecycle and are the main integration point for apps. + +### SandboxManager (concept) +Defines standard management interfaces: `create_sandbox`, `get_sandbox`, `stop_sandbox`, etc. + +### LocalSandboxManager + +- Location: `ms_enclave.manager.local_manager` +- Runs in-process and manages sandbox objects directly. +- Features: background auto-cleanup for `RUNNING` > 48h and `ERROR/STOPPED` > 1h sandboxes. + +### HttpSandboxManager + +- Location: `ms_enclave.manager.http_manager` +- Client proxy talking to the remote FastAPI server. +- API shape mirrors local manager for easy switching between local and remote modes. diff --git a/docs/en/docs/basic/usage.md b/docs/en/docs/basic/usage.md new file mode 100644 index 0000000..659e4ba --- /dev/null +++ b/docs/en/docs/basic/usage.md @@ -0,0 +1,230 @@ +# Basic Usage and Scenarios + +`ms-enclave` supports several usage modes from lightweight one-offs to manager-driven orchestration. + +## 1. Typical Scenarios + +### 1.1 Quick Start: SandboxFactory (lightest) + +Use `SandboxFactory` to run temporary code without heavy management overhead. + +```python +import asyncio +from ms_enclave.sandbox.boxes import SandboxFactory +from ms_enclave.sandbox.model import DockerSandboxConfig, SandboxType + +async def demo_sandbox_factory(): + config = DockerSandboxConfig( + image='python:3.11-slim', + tools_config={'python_executor': {}} + ) + + async with SandboxFactory.create_sandbox(SandboxType.DOCKER, config) as sandbox: + result = await sandbox.execute_tool('python_executor', { + 'code': 'import sys; print(f"Python {sys.version}")' + }) + print(f"[SandboxFactory] Output: {result.output.strip()}") + +# asyncio.run(demo_sandbox_factory()) +``` + +### 1.2 Unified entry: SandboxManagerFactory + +Create a local or HTTP manager by config. + +```python +from ms_enclave.sandbox.manager import SandboxManagerFactory, SandboxManagerType +from ms_enclave.sandbox.model import SandboxManagerConfig + +async def demo_manager_factory(): + cfg = SandboxManagerConfig(cleanup_interval=600) + async with SandboxManagerFactory.create_manager( + manager_type=SandboxManagerType.LOCAL, config=cfg + ) as manager: + print(f"[ManagerFactory] Created manager: {type(manager).__name__}") +``` + +### 1.3 Local orchestration: LocalSandboxManager + +Create, manage, and auto-clean multiple sandboxes within the same process. + +```python +from ms_enclave.sandbox.manager import LocalSandboxManager + +async def demo_local_manager(): + async with LocalSandboxManager() as manager: + config = DockerSandboxConfig(tools_config={'shell_executor': {}}) + sandbox_id = await manager.create_sandbox(SandboxType.DOCKER, config) + + res = await manager.execute_tool( + sandbox_id, 'shell_executor', {'command': 'echo "Hello Local Manager"'} + ) + print(f"[LocalManager] Output: {res.output.strip()}") + + sandboxes = await manager.list_sandboxes() + print(f"[LocalManager] Active sandboxes: {len(sandboxes)}") +``` + +### 1.4 Remote management: HttpSandboxManager + +Operate sandboxes hosted by a remote ms-enclave server. + +```python +from ms_enclave.sandbox.manager import HttpSandboxManager + +async def demo_http_manager(): + try: + async with HttpSandboxManager(base_url='http://127.0.0.1:8000') as manager: + config = DockerSandboxConfig(tools_config={'python_executor': {}}) + sid = await manager.create_sandbox(SandboxType.DOCKER, config) + res = await manager.execute_tool(sid, 'python_executor', {'code': 'print("Remote Hello")'}) + print(f"[HttpManager] Output: {res.output.strip()}") + await manager.delete_sandbox(sid) + except Exception as e: + print(f"[HttpManager] Skipped: Server might not be running. {e}") +``` + +### 1.5 High-throughput: Sandbox Pool (prewarm & reuse) + +Prewarm sandbox pool to reduce cold-start latency; FIFO queueing supported. + +```python +async def demo_sandbox_pool(): + async with LocalSandboxManager() as manager: + config = DockerSandboxConfig( + image='python:3.11-slim', + tools_config={'python_executor': {}} + ) + print("[Pool] Initializing pool...") + await manager.initialize_pool(pool_size=2, sandbox_type=SandboxType.DOCKER, config=config) + + result = await manager.execute_tool_in_pool( + 'python_executor', + {'code': 'print("Executed in pool")'}, + timeout=30 + ) + print(f"[Pool] Output: {result.output.strip()}") + + stats = await manager.get_stats() + print(f"[Pool] Stats: {stats}") +``` + +## 2. Advanced + +### 2.1 Install extra dependencies in sandbox + +Write `requirements.txt` via `file_operation` and install with pip. + +```python +async def demo_install_dependencies(): + config = DockerSandboxConfig( + image='python:3.11-slim', + tools_config={'python_executor': {}, 'file_operation': {}, 'shell_executor': {}} + ) + + async with SandboxFactory.create_sandbox(SandboxType.DOCKER, config) as sandbox: + print("[Deps] Installing dependencies...") + await sandbox.execute_tool('file_operation', { + 'operation': 'write', + 'file_path': '/sandbox/requirements.txt', + 'content': 'numpy' + }) + + install_res = await sandbox.execute_command('pip install -r /sandbox/requirements.txt') + if install_res.exit_code != 0: + print(f"[Deps] Install failed: {install_res.stderr}") + return + + res = await sandbox.execute_tool('python_executor', { + 'code': 'import numpy; print(f"Numpy version: {numpy.__version__}")' + }) + print(f"[Deps] Result: {res.output.strip()}") +``` + +### 2.2 Read/write host files (volumes) + +Mount a host directory for large files or persistence. + +```python +import os + +async def demo_host_volume(): + host_dir = os.path.abspath("./temp_sandbox_data") + os.makedirs(host_dir, exist_ok=True) + with open(os.path.join(host_dir, "host_file.txt"), "w") as f: + f.write("Hello from Host") + + config = DockerSandboxConfig( + image='python:3.11-slim', + tools_config={'shell_executor': {}}, + volumes={host_dir: {'bind': '/sandbox/data', 'mode': 'rw'}} + ) + + async with SandboxFactory.create_sandbox(SandboxType.DOCKER, config) as sandbox: + res = await sandbox.execute_tool('shell_executor', { + 'command': 'cat /sandbox/data/host_file.txt' + }) + print(f"[Volume] Read from host: {res.output.strip()}") + + await sandbox.execute_tool('shell_executor', { + 'command': 'echo "Response from Sandbox" > /sandbox/data/sandbox_file.txt' + }) + + with open(os.path.join(host_dir, "sandbox_file.txt"), "r") as f: + print(f"[Volume] Read from sandbox write: {f.read().strip()}") + + import shutil + shutil.rmtree(host_dir) +``` + +## 3. Tools usage details + +Ensure the corresponding tools are enabled in `tools_config`. + +```python +async def demo_tools_usage(): + config = DockerSandboxConfig( + tools_config={ + 'python_executor': {}, + 'shell_executor': {}, + 'file_operation': {} + } + ) + + async with SandboxFactory.create_sandbox(SandboxType.DOCKER, config) as sb: + py_res = await sb.execute_tool('python_executor', {'code': 'print(1+1)'}) + print(f"[Tool] Python: {py_res.output.strip()}") + + sh_res = await sb.execute_tool('shell_executor', {'command': 'date'}) + print(f"[Tool] Shell: {sh_res.output.strip()}") + + await sb.execute_tool('file_operation', { + 'operation': 'write', 'file_path': '/sandbox/test.txt', 'content': 'content' + }) + read_res = await sb.execute_tool('file_operation', { + 'operation': 'read', 'file_path': '/sandbox/test.txt' + }) + print(f"[Tool] File Read: {read_res.output}") +``` + +## 4. Manual lifecycle management + +If not using `async with`, manage start/stop explicitly. + +```python +async def demo_manual_lifecycle(): + config = DockerSandboxConfig(tools_config={'shell_executor': {}}) + + sandbox = SandboxFactory.create_sandbox(SandboxType.DOCKER, config) + + try: + await sandbox.start() + print("[Manual] Sandbox started") + + res = await sandbox.execute_tool('shell_executor', {'command': 'echo manual'}) + print(f"[Manual] Output: {res.output.strip()}") + + finally: + await sandbox.stop() + print("[Manual] Cleanup done") +``` diff --git a/docs/en/docs/getting-started/installation.md b/docs/en/docs/getting-started/installation.md new file mode 100644 index 0000000..a9f5512 --- /dev/null +++ b/docs/en/docs/getting-started/installation.md @@ -0,0 +1,45 @@ +# Installation Guide + +## Prerequisites + +Before installing ms-enclave, ensure your environment meets the following: + +- Python: version >= 3.10 +- Docker: Docker daemon must be installed and running (ms-enclave relies on it for isolated sandboxes). + - If you plan to use the `Notebook` sandbox, ensure port 8888 can be exposed by containers. + +## Install + +### From PyPI (recommended) + +Use pip: + +```bash +pip install ms-enclave +``` + +If you need Docker support (usually yes), install extra dependencies: + +```bash +pip install 'ms-enclave[docker]' +``` + +### From source + +For the latest development version: + +```bash +git clone https://github.com/modelscope/ms-enclave.git +cd ms-enclave +pip install -e . +# Install Docker extras +pip install -e '.[docker]' +``` + +## Verify + +After installation, verify with: + +```shell +ms-enclave -v +``` \ No newline at end of file diff --git a/docs/en/docs/getting-started/intro.md b/docs/en/docs/getting-started/intro.md new file mode 100644 index 0000000..127c350 --- /dev/null +++ b/docs/en/docs/getting-started/intro.md @@ -0,0 +1,17 @@ +# ms-enclave Documentation + +ms-enclave is a modular and robust sandbox runtime providing a secure, isolated execution environment for your applications. It leverages Docker containers for strong isolation, ships with local/HTTP managers, and an extensible tool system to execute code safely and efficiently in a controlled environment. + +## Key Features + +- 🔒 Security: Docker-based isolation with resource limits +- 🧩 Modularity: Pluggable sandboxes and tools (registry/factory) +- ⚡ Stability: Lightweight, fast startup, lifecycle management +- 🌐 Remote Control: Built-in FastAPI server for HTTP management +- 🔧 Tooling: Standardized tools enabled per-sandbox (OpenAI-style schema) + +## Requirements + +- Python >= 3.10 +- OS: Linux, macOS, or Windows with Docker support +- A working local Docker daemon (Notebook sandbox needs port 8888 exposed) diff --git a/docs/en/docs/getting-started/quickstart.md b/docs/en/docs/getting-started/quickstart.md new file mode 100644 index 0000000..df40dad --- /dev/null +++ b/docs/en/docs/getting-started/quickstart.md @@ -0,0 +1,155 @@ +# Quickstart + +`ms-enclave` offers two primary usage patterns to fit different integration needs: + +1. SandboxFactory: create a sandbox instance directly — minimal overhead, ideal for scripts, tests, or one-off tasks. +2. SandboxManagerFactory: orchestrate sandboxes via a manager — suitable for services and background apps, with lifecycle management, pool prewarm, and auto-cleanup. + +Both approaches are shown below. + +## Option 1: Lightweight script + +Create a sandbox instance and use `async with` to ensure cleanup on exit. + +### When to use + +- Single-run jobs and quick scripts +- Unit tests requiring a fresh, clean environment per case +- Small experiments +- Need direct access to low-level sandbox methods + +### Example + +Save as `quickstart_script.py`: + +```python +import asyncio +from ms_enclave.sandbox.boxes import SandboxFactory +from ms_enclave.sandbox.model import DockerSandboxConfig, SandboxType + +async def main(): + # 1) Configure sandbox and enable tools + config = DockerSandboxConfig( + image='python:3.11-slim', + tools_config={ + 'python_executor': {}, + 'file_operation': {}, + } + ) + + print("Starting sandbox...") + # 2) Create and start sandbox + async with SandboxFactory.create_sandbox(SandboxType.DOCKER, config) as sandbox: + print(f"Sandbox ready ID: {sandbox.id}") + + # 3) Write a file + await sandbox.execute_tool('file_operation', { + 'operation': 'write', + 'file_path': '/sandbox/hello.txt', + 'content': 'Hello from ms-enclave!' + }) + + # 4) Execute Python code + result = await sandbox.execute_tool('python_executor', { + 'code': """ +print('Reading file...') +with open('/sandbox/hello.txt', 'r') as f: + content = f.read() +print(f'Content: {content}') +""" + }) + + # 5) Check output + print("Execution result:", result.output) + +if __name__ == '__main__': + asyncio.run(main()) +``` + +### Notes + +1. `SandboxFactory` returns an async context manager for the sandbox. +2. `DockerSandboxConfig`: + - `image`: Docker image to ensure consistent environment. + - `tools_config`: only tools explicitly enabled here can be used inside the sandbox. +3. `execute_tool(name, params)`: call tool by name with its parameters. +4. Lifecycle: `async with` guarantees `stop()` and container cleanup. + +### Run + +```bash +python quickstart_script.py +``` + +The first run may pull `python:3.11-slim`, which can take a while. + +--- + +## Option 2: Application integration (Manager) + +For web services or long-running apps, use a manager. It supports local mode and seamless switch to remote HTTP mode, plus pool prewarming. + +### When to use + +- Backend services serving concurrent requests +- Long-running processes with auto-cleanups +- Performance-sensitive scenarios (pool prewarming) +- Distributed deployments (remote HTTP server) + +### Example + +Save as `quickstart_app.py`: + +```python +import asyncio +from ms_enclave.sandbox.manager import SandboxManagerFactory +from ms_enclave.sandbox.model import DockerSandboxConfig, SandboxType, SandboxManagerConfig, SandboxManagerType + +async def main(): + # 1) Manager config + manager_config = SandboxManagerConfig(cleanup_interval=600) + + print("Initializing manager...") + # 2) Create local manager (or set base_url for HTTP mode) + async with SandboxManagerFactory.create_manager( + manager_type=SandboxManagerType.LOCAL, + config=manager_config + ) as manager: + + # 3) Sandbox config + sb_config = DockerSandboxConfig( + image='python:3.11-slim', + tools_config={'python_executor': {}} + ) + + # 4) Create sandbox and get id + sandbox_id = await manager.create_sandbox(SandboxType.DOCKER, sb_config) + print(f"Sandbox ID: {sandbox_id}") + + # 5) Execute a tool via manager + result = await manager.execute_tool( + sandbox_id, + 'python_executor', + {'code': 'import sys; print(f"Python Version: {sys.version}")'} + ) + print(f"Output:\n{result.output.strip()}") + + # 6) List sandboxes + sandboxes = await manager.list_sandboxes() + print(f"Active sandboxes: {len(sandboxes)}") + +if __name__ == '__main__': + asyncio.run(main()) +``` + +### Notes + +- `SandboxManagerFactory` creates a local or HTTP manager depending on `manager_type` or `base_url`. +- Manager API returns `sandbox_id` (string) instead of sandbox object. +- `LocalSandboxManager` includes a background cleaner for stale/errored sandboxes. + +### Run + +```bash +python quickstart_app.py +``` diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml new file mode 100644 index 0000000..c486b1e --- /dev/null +++ b/docs/en/mkdocs.yml @@ -0,0 +1,198 @@ +site_name: ms-enclave Documentation +site_url: !ENV READTHEDOCS_CANONICAL_URL + +# Add GitHub repo info for header link and star count +repo_name: modelscope/ms-enclave +repo_url: https://github.com/modelscope/ms-enclave +edit_uri: edit/main/docs/en/docs/ + +nav: + - Quick Start: + - Introduction: getting-started/intro.md + - Installation: getting-started/installation.md + - Quickstart: getting-started/quickstart.md + - Core Concepts: + - basic/concepts.md + - Basic Usage: + - basic/usage.md + - Advanced: + - HTTP Server: advanced/server.md + - Customization: advanced/customization.md + - API Reference: + - Overview: api/index.md + - Manager: api/manager.md + - Sandbox: api/sandbox.md + - Tools: api/tools.md + +theme: + name: material + features: + - navigation.instant + - navigation.tabs + - navigation.expand + - navigation.path + - navigation.indexes + - navigation.top + - toc.integrate + - toc.follow + - search + - search.highlight + - search.suggest + - content.code.copy + - content.code.annotate + + palette: + # Palette toggle for automatic mode + - media: "(prefers-color-scheme)" + primary: indigo + toggle: + icon: material/brightness-auto + name: Switch to light mode + + # Palette toggle for light mode + - media: "(prefers-color-scheme: light)" + primary: indigo + scheme: default + toggle: + icon: material/brightness-7 + name: Switch to dark mode + + # Palette toggle for dark mode + - media: "(prefers-color-scheme: dark)" + primary: indigo + scheme: slate + toggle: + icon: material/brightness-4 + name: Switch to system preference + +# Add social links (bottom right) +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/modelscope/ms-enclave + - icon: fontawesome/brands/python + link: https://pypi.org/project/ms-enclave/ + +# Configure extensions for highlighting and fancy components +markdown_extensions: + - admonition + - abbr + - attr_list + - def_list + - footnotes + - meta + - md_in_html + - toc: + permalink: true + - pymdownx.details + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.superfences + - pymdownx.tabbed: + alternate_style: true + - pymdownx.tasklist: + custom_checkbox: true + +plugins: + - search + - mkdocstrings: + handlers: + # See: https://mkdocstrings.github.io/python/usage/ + python: + # Where to find your sources, see "Finding modules". + paths: [../../] + + # Load object inventories to enable cross-references to other projects. + inventories: + - https://docs.python.org/3/objects.inv + # Also load inventories of your dependencies, generally served at + # https://docs-url-for-your-dependency/objects.inv. + + options: + + # DOCSTRINGS ------------------------------------------------------------- + docstring_options: + # Discard first line of `__init__` method docstrings, + # useful when merging such docstrings into their parent class'. + ignore_init_summary: true + + # Tables are generally too large, lists will fix this. + docstring_section_style: list + + # CROSS-REFERENCES ------------------------------------------------------- + # Enable relative crossrefs and scoped crossrefs, see Docstrings options. + relative_crossrefs: true # Sponsors only! + scoped_crossrefs: true # Sponsors only! + + # Enable cross-references in signatures. + signature_crossrefs: true + + # Unwrap actual types from `Annotated` type annotations. + unwrap_annotated: true + + # MEMBERS ---------------------------------------------------------------- + # Only render pulic symbols. + filters: public # Sponsors only! + # Comment the option otherwise to get the default filters. + + # Show class inherited members. + inherited_members: true + + # Render auto-generated summaries of attributes, functions, etc. + # at the start of each symbol's documentation. + summary: true + + # HEADINGS --------------------------------------------------------------- + # For auto-generated pages, one module per page, + # make the module heading be the H1 heading of the page. + heading_level: 1 + + # Render headings for parameters, making them linkable. + parameter_headings: true + + # Render headings for type parameters too. + type_parameter_headings: true + + # Always show the heading for the symbol you render with `::: id`. + show_root_heading: true + + # Only show the name of the symbols you inject render `::: id`. + show_root_full_path: false + + # Show the type of symbol (class, function, etc.) in the heading. + show_symbol_type_heading: true + + # Show the type of symbol (class, function, etc.) in the table of contents. + show_symbol_type_toc: true + + # SIGNATURES ------------------------------------------------------------- + # Format code to 80 + 10% margin (Black and Ruff defaults) + # in signatures and attribute value code blocks. + # Needs Black/Ruff installed. + line_length: 88 + + # Merge signature and docstring of `__init__` methods + # into their parent class signature and docstring. + merge_init_into_class: true + + # Render signatures and attribute values in a separate code block, + # below the symbol heading. + separate_signature: true + + # Show type annotations in signatures. + show_signature_annotations: true + + # Show type parameters in signatures. + show_signature_type_parameters: true + + # OTHER ------------------------------------------------------------------ + # Show backlinks to other documentation sections within each symbol. + backlinks: tree # Sponsors only! + + # Show base classes OR inheritance diagram. + show_bases: false + show_inheritance_diagram: true # Sponsors only! diff --git a/docs/zh/.readthedocs.yaml b/docs/zh/.readthedocs.yaml new file mode 100644 index 0000000..60ebd6e --- /dev/null +++ b/docs/zh/.readthedocs.yaml @@ -0,0 +1,15 @@ +version: 2 + +build: + os: "ubuntu-24.04" + tools: + python: "3" + # We recommend using a requirements file for reproducible builds. + # This is just a quick example to get started. + # https://docs.readthedocs.io/page/guides/reproducible-builds.html + jobs: + pre_install: + - pip install mkdocs-material mkdocstrings-python + +mkdocs: + configuration: mkdocs.yml diff --git a/docs/zh/docs/_static/logo.png b/docs/zh/docs/_static/logo.png new file mode 100644 index 0000000..7a0d2d3 Binary files /dev/null and b/docs/zh/docs/_static/logo.png differ diff --git a/docs/zh/docs/advanced/customization.md b/docs/zh/docs/advanced/customization.md new file mode 100644 index 0000000..6a7f288 --- /dev/null +++ b/docs/zh/docs/advanced/customization.md @@ -0,0 +1,418 @@ +# 自定义扩展 + +ms-enclave 采用高度模块化的设计,支持按需扩展 Tool、Sandbox 和 SandboxManager。本文给出所需接口、注册机制与可运行的最小示例,帮助你快速实现与验证。 + +## 注册机制总览 + +项目采用装饰器注册模式(Registry),便于按类型动态创建对象: + +* Sandbox: + + * 装饰器:`@register_sandbox(SandboxType.XYZ)` + * 工厂:`SandboxFactory.create_sandbox(sandbox_type, config, sandbox_id)` + +* Tool: + + * 装饰器:`@register_tool('tool_name')` + * 工厂:`ToolFactory.create_tool('tool_name', **kwargs)` + +* SandboxManager: + + * 装饰器:`@register_manager(SandboxManagerType.XYZ)` + * 工厂:`SandboxManagerFactory.create_manager(manager_type, config, **kwargs)` + +扩展类型建议: +- 新增 Sandbox 时,通常需要在 `ms_enclave/sandbox/model` 中为 `SandboxType` 扩展一个枚举值(例如:`LOCAL_PROCESS`)。 +- 新增 SandboxManager 时,通常需要在 `ms_enclave/sandbox/model` 中为 `SandboxManagerType` 扩展一个枚举值(例如:`LOCAL_INMEM`)。 + +> 提示:本文所有代码示例均使用英文注释与完整类型标注,符合项目的代码风格。 + +--- + +## 自定义 Tool + +必须实现/覆写: +- `required_sandbox_type`:声明该工具可运行的沙箱类型(返回 `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` 判定。 + +### 示例 A:最小可用工具(不依赖沙箱命令) + +```python +# 最少依赖:返回问候语,任何沙箱可用 +from typing import Any, Dict, Optional +from ms_enclave.sandbox.tools.base import Tool, register_tool +from ms_enclave.sandbox.model import SandboxType # 仅用于类型声明 + +@register_tool('hello') +class HelloTool(Tool): + def __init__(self, name: str = 'hello', description: str = 'Say hello', enabled: bool = True): + super().__init__(name=name, description=description, enabled=enabled) + + @property + def required_sandbox_type(self) -> Optional[SandboxType]: + # None => 任何沙箱可用 + return None + + async def execute(self, sandbox_context: Any, name: str = 'world', **kwargs) -> Dict[str, Any]: + return {'message': f'Hello, {name}!'} +``` + +使用(以 Docker 沙箱为例): +```python +from ms_enclave.sandbox.boxes import SandboxFactory +from ms_enclave.sandbox.model import SandboxType, DockerSandboxConfig +import asyncio + +async def main(): + sb = SandboxFactory.create_sandbox(SandboxType.DOCKER, DockerSandboxConfig(image='python:3.11-slim')) + async with sb: + await sb.initialize_tools() # 若你的 start() 未做初始化,这里手动初始化 + result = await sb.execute_tool('hello', {'name': 'ms-enclave'}) + print(result) # {'message': 'Hello, ms-enclave!'} + +asyncio.run(main()) +``` + +### 示例 B:优先在沙箱内执行命令的工具(可回退到本地逻辑) + +```python +# 在沙箱内执行 `date` 命令;若不可用则回退到 Python 计算当前时间 +from typing import Any, Dict, Optional +from datetime import datetime, timezone +from ms_enclave.sandbox.tools.base import Tool, register_tool +from ms_enclave.sandbox.model import SandboxType + +@register_tool('time_teller') +class TimeTellerTool(Tool): + def __init__(self, name: str = 'time_teller', description: str = 'Tell current time', enabled: bool = True): + super().__init__(name=name, description=description, enabled=enabled) + + @property + def required_sandbox_type(self) -> Optional[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' + if timezone_name: + cmd = f'TZ={timezone_name} date' + try: + # 约定:execute_command 返回 (exit_code, stdout, stderr) + exit_code, out, err = await sandbox_context.execute_command(cmd, timeout=5) + if exit_code == 0: + return {'time': out.strip()} + return {'error': err.strip() or 'unknown error'} + except Exception: + # 回退到 Python 计算 + tz = timezone.utc if (timezone_name or '').upper() == 'UTC' else None + return {'time': datetime.now(tz=tz).isoformat()} +``` + +启用工具(配置即可注入构造参数;此处无需参数): +```python +from ms_enclave.sandbox.model import DockerSandboxConfig + +config = DockerSandboxConfig( + image='debian:stable-slim', + tools_config={ + 'hello': {}, + 'time_teller': {} + } +) +``` + +--- + +## 自定义 Sandbox + +必须实现: +- `sandbox_type`:返回该实现的 `SandboxType`。 +- `async def start(self)`:启动沙箱并将 `status` 置为 `RUNNING`,建议在此调用 `await self.initialize_tools()`。 +- `async def stop(self)`:停止沙箱,将 `status` 置为 `STOPPED`。 +- `async def cleanup(self)`:释放资源。 +- `async def execute_command(self, command, timeout=None, stream=True)`:在沙箱内执行命令(若不支持可抛出 `NotImplementedError`)。 +- `async def get_execution_context(self)`:返回供工具使用的执行上下文(容器/进程句柄等,若无可返回 `None`)。 + +最小实现示例:本地进程型沙箱(演示用途) +> 假设你已在 `SandboxType` 中新增 `LOCAL_PROCESS` 枚举值。 + +```python +import asyncio +from typing import Any, Dict, List, Optional, Tuple, Union +from ms_enclave.sandbox.boxes.base import Sandbox, register_sandbox +from ms_enclave.sandbox.model import SandboxType, SandboxStatus, SandboxConfig + +CommandResult = Tuple[int, str, str] + +@register_sandbox(SandboxType.LOCAL_PROCESS) # 需先扩展枚举 +class LocalProcessSandbox(Sandbox): + """Run host commands as a 'sandbox' (for demo/dev only).""" + + @property + def sandbox_type(self) -> SandboxType: + return SandboxType.LOCAL_PROCESS + + async def start(self) -> None: + self.update_status(SandboxStatus.RUNNING) + await self.initialize_tools() + + async def stop(self) -> None: + self.update_status(SandboxStatus.STOPPED) + + async def cleanup(self) -> None: + # Nothing to cleanup for this simple demo + return + + async def execute_command( + self, + command: Union[str, List[str]], + timeout: Optional[int] = None, + stream: bool = True + ) -> CommandResult: + # Run command on host (demo): DO NOT use in production + if isinstance(command, list): + shell_cmd = ' '.join(command) + else: + shell_cmd = command + + proc = await asyncio.create_subprocess_shell( + shell_cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + try: + if timeout: + stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout) + else: + stdout, stderr = await proc.communicate() + except asyncio.TimeoutError: + proc.kill() + return 124, '', 'command timed out' + return proc.returncode, (stdout or b'').decode(), (stderr or b'').decode() + + async def get_execution_context(self) -> Any: + return None +``` + +快速验证: +```python +from ms_enclave.sandbox.boxes import SandboxFactory +from ms_enclave.sandbox.model import SandboxType, SandboxConfig +import asyncio + +async def main(): + sb = SandboxFactory.create_sandbox(SandboxType.LOCAL_PROCESS, SandboxConfig()) + async with sb: + await sb.initialize_tools() + # 直接运行 hello 工具(示例 A) + print(await sb.execute_tool('hello', {'name': 'sandbox'})) + # 直接运行命令 + print(await sb.execute_command('echo hi')) + +asyncio.run(main()) +``` + +> 注意:真实沙箱(如 Docker)需要实现镜像拉取、容器创建/启动、资源限制、挂载等。可参考 `ms_enclave/sandbox/boxes/docker_sandbox.py`。 + +--- + +## 自定义 SandboxManager + +必须实现(抽象基类 `SandboxManager` 中定义): +- 生命周期:`start()`, `stop()` +- 沙箱操作:`create_sandbox()`, `get_sandbox_info()`, `list_sandboxes()`, `stop_sandbox()`, `delete_sandbox()` +- 工具执行:`execute_tool()`, `get_sandbox_tools()` +- 统计:`get_stats()` +- 清理:`cleanup_all_sandboxes()` +- 连接池(可选但为抽象方法,需给出最小实现):`initialize_pool()`, `execute_tool_in_pool()` + +最小可运行示例:内存管理器 +> 假设你已在 `SandboxManagerType` 中新增 `LOCAL_INMEM` 枚举值。 + +```python +import asyncio +from typing import Any, Dict, List, Optional, Union +from ms_enclave.sandbox.manager.base import SandboxManager, register_manager +from ms_enclave.sandbox.boxes import SandboxFactory +from ms_enclave.sandbox.model import ( + SandboxConfig, SandboxInfo, SandboxManagerConfig, SandboxManagerType, + SandboxStatus, SandboxType, ToolResult +) + +@register_manager(SandboxManagerType.LOCAL_INMEM) # 需先扩展枚举 +class LocalInMemoryManager(SandboxManager): + """A minimal in-memory manager for demo/dev.""" + + def __init__(self, config: Optional[SandboxManagerConfig] = None, **kwargs): + super().__init__(config=config, **kwargs) + self._running = False + + async def start(self) -> None: + self._running = True + + async def stop(self) -> None: + await self.cleanup_all_sandboxes() + self._running = False + + async def create_sandbox( + self, + sandbox_type: SandboxType, + config: Optional[Union[SandboxConfig, Dict]] = None, + sandbox_id: Optional[str] = None + ) -> str: + sb = SandboxFactory.create_sandbox(sandbox_type, config, sandbox_id) + await sb.start() + await sb.initialize_tools() + self._sandboxes[sb.id] = sb + # 简单放入池(可选) + async with self._pool_lock: + self._sandbox_pool.append(sb.id) + return sb.id + + async def get_sandbox_info(self, sandbox_id: str) -> Optional[SandboxInfo]: + sb = self._sandboxes.get(sandbox_id) + return sb.get_info() if sb else None + + async def list_sandboxes(self, status_filter: Optional[SandboxStatus] = None) -> List[SandboxInfo]: + infos = [sb.get_info() for sb in self._sandboxes.values()] + if status_filter: + return [i for i in infos if i.status == status_filter] + return infos + + async def stop_sandbox(self, sandbox_id: str) -> bool: + sb = self._sandboxes.get(sandbox_id) + if not sb: + return False + await sb.stop() + return True + + async def delete_sandbox(self, sandbox_id: str) -> bool: + sb = self._sandboxes.pop(sandbox_id, None) + if not sb: + return False + await sb.cleanup() + # 同步移出池 + async with self._pool_lock: + try: + self._sandbox_pool.remove(sandbox_id) + except ValueError: + pass + return True + + async def execute_tool(self, sandbox_id: str, tool_name: str, parameters: Dict[str, Any]) -> ToolResult: + sb = self._sandboxes.get(sandbox_id) + if not sb: + raise ValueError(f'Sandbox {sandbox_id} not found') + if sb.status != SandboxStatus.RUNNING: + raise ValueError('Sandbox not running') + return await sb.execute_tool(tool_name, parameters) + + async def get_sandbox_tools(self, sandbox_id: str) -> Dict[str, Any]: + sb = self._sandboxes.get(sandbox_id) + if not sb: + raise ValueError(f'Sandbox {sandbox_id} not found') + return sb.get_available_tools() + + async def get_stats(self) -> Dict[str, Any]: + total = len(self._sandboxes) + running = sum(1 for s in self._sandboxes.values() if s.status == SandboxStatus.RUNNING) + return {'total': total, 'running': running} + + async def cleanup_all_sandboxes(self) -> None: + # 停止并清理所有沙箱 + for sb in list(self._sandboxes.values()): + try: + await sb.stop() + await sb.cleanup() + except Exception: + pass + self._sandboxes.clear() + async with self._pool_lock: + self._sandbox_pool.clear() + + async def initialize_pool( + self, + pool_size: Optional[int] = None, + sandbox_type: Optional[SandboxType] = None, + config: Optional[Union[SandboxConfig, Dict]] = None + ) -> List[str]: + if self._pool_initialized: + return list(self._sandbox_pool) + size = pool_size or (self.config.pool_size if self.config else 0) or 0 + if size <= 0: + self._pool_initialized = True + return [] + st = sandbox_type or (self.config.sandbox_type if self.config else None) + if not st: + raise ValueError('sandbox_type required for pool initialization') + ids: List[str] = [] + for _ in range(size): + sb_id = await self.create_sandbox(st, config or (self.config.sandbox_config if self.config else None)) + ids.append(sb_id) + self._pool_initialized = True + return ids + + async def execute_tool_in_pool( + self, tool_name: str, parameters: Dict[str, Any], timeout: Optional[float] = None + ) -> ToolResult: + # 简单 FIFO:取一个空闲沙箱,执行后归还队列 + async def acquire_one() -> str: + start = asyncio.get_event_loop().time() + while True: + async with self._pool_lock: + if self._sandbox_pool: + return self._sandbox_pool.popleft() + if timeout and (asyncio.get_event_loop().time() - start) > timeout: + raise TimeoutError('No sandbox available from pool') + await asyncio.sleep(0.05) + + sandbox_id = await acquire_one() + try: + return await self.execute_tool(sandbox_id, tool_name, parameters) + finally: + async with self._pool_lock: + # 若沙箱仍在管理器中,则归还 + if sandbox_id in self._sandboxes: + self._sandbox_pool.append(sandbox_id) +``` + +验证: +```python +from ms_enclave.sandbox.manager.base import SandboxManagerFactory +from ms_enclave.sandbox.model import SandboxManagerType, SandboxType, DockerSandboxConfig +import asyncio + +async def main(): + mgr = SandboxManagerFactory.create_manager(SandboxManagerType.LOCAL_INMEM) + async with mgr: + sb_id = await mgr.create_sandbox(SandboxType.DOCKER, DockerSandboxConfig(image='python:3.11-slim')) + print(await mgr.get_sandbox_tools(sb_id)) + print(await mgr.execute_tool(sb_id, 'hello', {'name': 'manager'})) + +asyncio.run(main()) +``` + +--- + +## 开发要点与最佳实践 + +- 生命周期与状态: + - 只有在 `SandboxStatus.RUNNING` 时才应执行工具。 + - 在 `start()` 中调用 `await self.initialize_tools()`,确保工具就绪。 +- 兼容性: + - 工具应通过 `required_sandbox_type` 明确要求;若无要求,返回 `None`。 + - `SandboxType.is_compatible` 用于允许子类型复用父类型工具(例如:`DOCKER_NOTEBOOK` 兼容 `DOCKER` 工具)。 +- 参数 Schema: + - 若需要严格参数校验/文档化,在构造 Tool 时传入 `parameters`(Pydantic 模型)。未指定时,schema 的 `parameters` 为 `{}`。 +- 简洁代码: + - 小函数、清晰命名、英文注释、必要的 docstring。 + - 优先使用早返回,避免嵌套。 +- 快速验证: + - 从最小实现开始(如示例 A/B),先本地跑通,再增加复杂度(如连接池、网络、资源限制)。 + +> 实战建议:扩展新 Sandbox 可直接参考 `ms_enclave/sandbox/boxes/docker_sandbox.py`;扩展 HTTP API 对应修改 `server/server.py` 与 `manager/http_manager.py` 并保证 Pydantic 模型同步。 diff --git a/docs/zh/docs/advanced/server.md b/docs/zh/docs/advanced/server.md new file mode 100644 index 0000000..542af8f --- /dev/null +++ b/docs/zh/docs/advanced/server.md @@ -0,0 +1,59 @@ +# 部署 HTTP 服务 + +ms-enclave 内置了一个基于 FastAPI 的 HTTP 服务器,运行该服务可以将沙箱能力暴露给远程调用者。这在构建分布式系统或需要微服务化沙箱能力时非常有用。 + +## 启动服务器 + +### 命令行启动 + +ms-enclave 提供了一个简单的入口来启动服务器: + +```bash +ms-enclave server --host 0.0.0.0 --port 8000 +``` + + +## 使用 HttpSandboxManager 客户端 + +一旦服务器启动,您就可以使用 `HttpSandboxManager` 像操作本地管理器一样操作远程沙箱。 + +```python +import asyncio +from ms_enclave.sandbox.manager import SandboxManagerFactory +from ms_enclave.sandbox.model import DockerSandboxConfig, SandboxType + +async def main(): + # 连接到远程服务器 + async with SandboxManagerFactory.create_manager(base_url='http://127.0.0.1:8000') as manager: + + # 创建沙箱(在服务器端创建 Docker 容器) + config = DockerSandboxConfig( + image='python:3.11-slim', + tools_config={'python_executor': {}} + ) + + sandbox_id = await manager.create_sandbox(SandboxType.DOCKER, config) + print(f"远程沙箱 ID: {sandbox_id}") + + # 执行工具 + result = await manager.execute_tool(sandbox_id, 'python_executor', { + 'code': 'import platform; print(platform.node())' + }) + print(f"远程执行结果: {result.output}") + + +if __name__ == '__main__': + asyncio.run(main()) +``` + +## API 概览 + +HTTP 服务器主要提供以下 API: + +* `POST /sandbox/create`: 创建沙箱 +* `GET /sandboxes`: 列出所有沙箱 +* `GET /sandbox/{sandbox_id}`: 获取沙箱详情 +* `POST /sandbox/{sandbox_id}/stop`: 停止沙箱 +* `DELETE /sandbox/{sandbox_id}`: 删除沙箱 +* `POST /sandbox/tool/execute`: 执行工具 (Body: `ToolExecutionRequest`) +* `GET /sandbox/{sandbox_id}/tools`: 列出沙箱可用工具 diff --git a/docs/zh/docs/api/index.md b/docs/zh/docs/api/index.md new file mode 100644 index 0000000..8376671 --- /dev/null +++ b/docs/zh/docs/api/index.md @@ -0,0 +1,11 @@ +# API 概览 + +本章节通过 mkdocstrings 从源码自动生成 API 文档,涵盖核心接口与工厂类: + +- 管理器:`SandboxManager`、`SandboxManagerFactory` +- 沙箱:`Sandbox`、`SandboxFactory` +- 工具:`Tool`、`ToolFactory` + +使用方式:页面中的签名、参数与返回值来自源码的类型注解与文档字符串,请以源码为准。 + +> 提示:若页面未展示某些类/函数,请确认其 docstring 是否完善,以及模块是否包含在 mkdocs 的 `handlers.python.paths` 配置搜索路径中。 diff --git a/docs/zh/docs/api/manager.md b/docs/zh/docs/api/manager.md new file mode 100644 index 0000000..56d59a9 --- /dev/null +++ b/docs/zh/docs/api/manager.md @@ -0,0 +1,7 @@ +# Manager API + +::: ms_enclave.sandbox.manager.base.SandboxManager + +::: ms_enclave.sandbox.manager.base.SandboxManagerFactory + +::: ms_enclave.sandbox.manager.base.register_manager diff --git a/docs/zh/docs/api/sandbox.md b/docs/zh/docs/api/sandbox.md new file mode 100644 index 0000000..d2a4799 --- /dev/null +++ b/docs/zh/docs/api/sandbox.md @@ -0,0 +1,9 @@ +# Sandbox API + +::: ms_enclave.sandbox.boxes.base.Sandbox + + +::: ms_enclave.sandbox.boxes.base.SandboxFactory + + +::: ms_enclave.sandbox.boxes.base.register_sandbox diff --git a/docs/zh/docs/api/tools.md b/docs/zh/docs/api/tools.md new file mode 100644 index 0000000..65c4df2 --- /dev/null +++ b/docs/zh/docs/api/tools.md @@ -0,0 +1,8 @@ +# Tool API + + +::: ms_enclave.sandbox.tools.base.Tool + +::: ms_enclave.sandbox.tools.base.ToolFactory + +::: ms_enclave.sandbox.tools.base.register_tool diff --git a/docs/zh/docs/basic/concepts.md b/docs/zh/docs/basic/concepts.md new file mode 100644 index 0000000..cab8df0 --- /dev/null +++ b/docs/zh/docs/basic/concepts.md @@ -0,0 +1,110 @@ +# 核心概念 + +ms-enclave 采用模块化分层架构,将运行时环境管理、工具执行和生命周期维护解耦。理解以下核心概念及其关系,有助于您构建高效的 Agent 系统。 + +## 架构概览 + +核心组件之间的关系如下图所示: + +```ascii ++------------------+ +----------------------+ +| User / Client | --------> | SandboxManager | ++------------------+ | (Local / HTTP Proxy) | + +----------+-----------+ + | + | 1. Manage + | + v ++----------------------+ +----------------------+ +| SandboxFactory | ----> | Sandbox | <---+ ++----------------------+ 2. | (Docker / Notebook) | | + Create+----------+-----------+ | + | | 4. + | 3. Run | Execute + v | + +----------------------+ | + | Runtime Envrion | | + | (Container / Kernel)| | + +----------------------+ | + | | ++----------------------+ +----------+-----------+ | +| ToolFactory | ----> | Tool | ----+ ++----------------------+ 2. | (Python/Shell/File) | + Create+----------------------+ +``` + +## 1. 沙箱系统 (Sandbox System) + +沙箱是代码安全执行的隔离环境。 + +### Sandbox (沙箱基类) +`Sandbox` 是核心抽象类 (ABC),定义了隔离环境的标准行为。 + +* **作用**: 封装底层的运行时细节(如 Docker API 调用),提供统一的 `start` (启动)、`stop` (停止)、`execute_command` (执行底层命令) 接口。 +* **状态管理**: 维护严格的生命周期状态: + * `CREATED`: 已初始化但未分配资源。 + * `STARTING`: 正在启动容器或分配资源。 + * `RUNNING`: 正常运行中,可接受指令。 + * `STOPPING` / `STOPPED`: 正在停止或已停止。 + * `ERROR`: 发生运行时错误。 + +### SandboxFactory (沙箱工厂) + +* **作用**: 这是一个工厂模式实现,负责根据配置类型(`SandboxType`)动态创建具体的沙箱实例。 +* **扩展性**: 使用 `@register_sandbox` 装饰器注册新的沙箱类型,无需修改工厂代码即可扩展系统。 + +### 具体实现与配置 + +* **DockerSandbox (类型: `docker`)**: + * 基于 Docker 容器的标准沙箱,提供文件系统和网络隔离。 + * **主要配置 (`DockerSandboxConfig`)**: + * `image` (str): Docker 镜像名 (如 `python:3.10-slim`)。 + * `cpu_limit` (float): CPU 核心数限制 (如 `1.0`)。 + * `memory_limit` (str): 内存限制 (如 `"512m"`, `"1g"`). + * `auto_remove` (bool): 停止后是否自动删除容器。 + * `volumes` (dict): 挂载卷配置。 +* **DockerNotebook (类型: `docker_notebook`)**: + * 继承自 DockerSandbox,内置 Jupyter Kernel Gateway。支持通过 HTTP/WebSocket 进行交互式代码执行(保持变量状态)。 + * **主要配置**: 继承自 `DockerSandboxConfig`,额外包含内核通信端口配置。 + +## 2. 工具系统 (Tool System) + +工具是 LLM 与沙箱交互的能力载体。 + +### Tool (工具基类) + +* **作用**: 抽象了具体的操作逻辑。必须实现 `execute(sandbox_context, **kwargs)` 方法。 +* **Schema**: 每个工具通过 `schema` 属性暴露符合 OpenAI Function Calling 标准的定义,方便 LLM 决策。 + +### ToolFactory (工具工厂) + +* **作用**: 集中管理工具的实例化。 +* **机制**: 通过 `@register_tool("name")` 进行注册。在创建沙箱配置时,可以通过工具名称列表启用特定工具。 + +### 常用工具 +工具主要分为通用工具和特定环境工具: + +* **PythonExecutor**: 在沙箱内执行 Python 代码片段(非交互式,或通过 Notebook 交互)。 +* **ShellExecutor**: 执行 Bash 命令。 +* **FileOperation**: 提供 `read_file`, `write_file`, `list_dir` 等文件操作。 + +## 3. 管理层 (Management Layer) + +管理器用于编排沙箱的生命周期,是用户通过代码直接交互的对象。 + +### SandboxManager (概念) +定义了 `create_sandbox`, `get_sandbox`, `stop_sandbox` 等标准管理接口。 + +### LocalSandboxManager (本地管理器) + +* **位置**: `ms_enclave.manager.local_manager` +* **作用**: 在当前 Python 进程中直接管理沙箱对象。 +* **特性**: + * **自动清理**: 内置后台线程,定期清理超时 (`RUNNING` > 48h) 或异常 (`ERROR/STOPPED` > 1h) 的沙箱,防止资源泄露。 + * 适合开发调试、单机部署或作为 Server 端的内部实现。 + +### HttpSandboxManager (HTTP 管理器) + +* **位置**: `ms_enclave.manager.http_manager` +* **作用**: 一个客户端代理,负责与远程的 `ms-enclave` Server (FastAPI) 通信。 +* **特性**: API 签名与本地管理器保持高度一致,使得从本地模式切换到远程服务模式几乎无需修改业务代码。 diff --git a/docs/zh/docs/basic/usage.md b/docs/zh/docs/basic/usage.md new file mode 100644 index 0000000..9fcf794 --- /dev/null +++ b/docs/zh/docs/basic/usage.md @@ -0,0 +1,264 @@ +# 基础使用与场景示例 + +`ms-enclave` 提供了多种使用模式以适应不同的集成需求,从轻量级的单次执行到复杂的集群管理。 + +## 1. 典型使用场景 + +### 1.1 快速开始:SandboxFactory (最轻量) + +如果您只需要临时运行一段代码,不需要复杂的管理功能,可以直接使用 `SandboxFactory`。这是最轻量的方式,适合脚本或一次性任务。 + +```python +import asyncio +from ms_enclave.sandbox.boxes import SandboxFactory +from ms_enclave.sandbox.model import DockerSandboxConfig, SandboxType + +async def demo_sandbox_factory(): + # 配置沙箱,启用 python_executor 工具 + config = DockerSandboxConfig( + image='python:3.11-slim', + tools_config={'python_executor': {}} + ) + + # 使用 async with 自动管理生命周期(创建 -> 启动 -> 使用 -> 停止 -> 清理) + async with SandboxFactory.create_sandbox(SandboxType.DOCKER, config) as sandbox: + result = await sandbox.execute_tool('python_executor', { + 'code': 'import sys; print(f"Python {sys.version}")' + }) + print(f"[SandboxFactory] Output: {result.output.strip()}") + +# asyncio.run(demo_sandbox_factory()) +``` + +### 1.2 统一管理入口:SandboxManagerFactory + +如果您希望代码能够灵活切换本地或远程模式,或者统一管理配置,可以使用工厂模式。 + +- 传入 `base_url` 时自动创建 HTTP 管理器。 +- 传入 `manager_type=SandboxManagerType.LOCAL` 时创建本地管理器。 + +```python +from ms_enclave.sandbox.manager import SandboxManagerFactory, SandboxManagerType +from ms_enclave.sandbox.model import SandboxManagerConfig + +async def demo_manager_factory(): + # 示例:创建本地管理器 + cfg = SandboxManagerConfig(cleanup_interval=600) + async with SandboxManagerFactory.create_manager( + manager_type=SandboxManagerType.LOCAL, config=cfg + ) as manager: + print(f"[ManagerFactory] Created manager: {type(manager).__name__}") +``` + +### 1.3 本地编排:LocalSandboxManager (多任务并行) + +适合在同一进程内需要创建、管理多个沙箱,并需要后台自动清理机制的场景。 + +```python +from ms_enclave.sandbox.manager import LocalSandboxManager + +async def demo_local_manager(): + async with LocalSandboxManager() as manager: + config = DockerSandboxConfig(tools_config={'shell_executor': {}}) + # 创建沙箱 + sandbox_id = await manager.create_sandbox(SandboxType.DOCKER, config) + + # 执行工具 + res = await manager.execute_tool( + sandbox_id, 'shell_executor', {'command': 'echo "Hello Local Manager"'} + ) + print(f"[LocalManager] Output: {res.output.strip()}") + + # 获取列表 + sandboxes = await manager.list_sandboxes() + print(f"[LocalManager] Active sandboxes: {len(sandboxes)}") +``` + +### 1.4 远程管理:HttpSandboxManager (分布式/隔离) + +当沙箱服务运行在独立服务器上时使用。 + +> **前提**:需要先启动服务器 `ms-enclave server`。 + +```python +from ms_enclave.sandbox.manager import HttpSandboxManager + +async def demo_http_manager(): + # 假设服务运行在本地 8000 端口 + try: + async with HttpSandboxManager(base_url='http://127.0.0.1:8000') as manager: + config = DockerSandboxConfig(tools_config={'python_executor': {}}) + sid = await manager.create_sandbox(SandboxType.DOCKER, config) + res = await manager.execute_tool(sid, 'python_executor', {'code': 'print("Remote Hello")'}) + print(f"[HttpManager] Output: {res.output.strip()}") + await manager.delete_sandbox(sid) + except Exception as e: + print(f"[HttpManager] Skipped: Server might not be running. {e}") +``` + +### 1.5 高性能模式:Sandbox Pool (预热复用) + +通过预热沙箱池来减少容器启动时间,提高并发吞吐量。支持 FIFO 排队。 + +```python +async def demo_sandbox_pool(): + async with LocalSandboxManager() as manager: + config = DockerSandboxConfig( + image='python:3.11-slim', + tools_config={'python_executor': {}} + ) + # 初始化池,预热 2 个沙箱 + print("[Pool] Initializing pool...") + await manager.initialize_pool(pool_size=2, sandbox_type=SandboxType.DOCKER, config=config) + + # 借用沙箱执行任务,完成后自动归还 + result = await manager.execute_tool_in_pool( + 'python_executor', + {'code': 'print("Executed in pool")'}, + timeout=30 # 等待空闲沙箱的超时时间 + ) + print(f"[Pool] Output: {result.output.strip()}") + + stats = await manager.get_stats() + print(f"[Pool] Stats: {stats}") +``` + +## 2. 高级功能 + +### 2.1 在 Sandbox 中安装额外依赖 + +由于沙箱通常是隔离的,如果需要第三方库,可以通过 `file_operation` 写入 `requirements.txt` 并执行安装命令。 + +```python +async def demo_install_dependencies(): + config = DockerSandboxConfig( + image='python:3.11-slim', + tools_config={'python_executor': {}, 'file_operation': {}, 'shell_executor': {}} + ) + + async with SandboxFactory.create_sandbox(SandboxType.DOCKER, config) as sandbox: + print("[Deps] Installing dependencies...") + # 1. 写入 requirements.txt + await sandbox.execute_tool('file_operation', { + 'operation': 'write', + 'file_path': '/sandbox/requirements.txt', + 'content': 'numpy' # 示例依赖 + }) + + # 2. 执行安装命令 + install_res = await sandbox.execute_command('pip install -r /sandbox/requirements.txt') + if install_res.exit_code != 0: + print(f"[Deps] Install failed: {install_res.stderr}") + return + + # 3. 验证安装 + res = await sandbox.execute_tool('python_executor', { + 'code': 'import numpy; print(f"Numpy version: {numpy.__version__}")' + }) + print(f"[Deps] Result: {res.output.strip()}") +``` + +### 2.2 读写宿主机文件 (挂载 Volume) + +通过 Docker 挂载,可以让沙箱读写宿主机上的文件。这对于处理大文件或持久化数据非常有用。 + +```python +import os + +async def demo_host_volume(): + # 在宿主机创建一个临时目录用于测试 + host_dir = os.path.abspath("./temp_sandbox_data") + os.makedirs(host_dir, exist_ok=True) + with open(os.path.join(host_dir, "host_file.txt"), "w") as f: + f.write("Hello from Host") + + # 配置挂载:宿主机路径 -> 容器内路径 + config = DockerSandboxConfig( + image='python:3.11-slim', + tools_config={'shell_executor': {}}, + volumes={host_dir: {'bind': '/sandbox/data', 'mode': 'rw'}} + ) + + async with SandboxFactory.create_sandbox(SandboxType.DOCKER, config) as sandbox: + # 读取宿主机文件 + res = await sandbox.execute_tool('shell_executor', { + 'command': 'cat /sandbox/data/host_file.txt' + }) + print(f"[Volume] Read from host: {res.output.strip()}") + + # 写入文件回宿主机 + await sandbox.execute_tool('shell_executor', { + 'command': 'echo "Response from Sandbox" > /sandbox/data/sandbox_file.txt' + }) + + # 验证宿主机上的文件 + with open(os.path.join(host_dir, "sandbox_file.txt"), "r") as f: + print(f"[Volume] Read from sandbox write: {f.read().strip()}") + + # 清理 + import shutil + shutil.rmtree(host_dir) +``` + +## 3. 工具使用详解 + +以下是常用工具的参数示例。请确保在 `tools_config` 中启用了对应工具。 + +```python +async def demo_tools_usage(): + config = DockerSandboxConfig( + tools_config={ + 'python_executor': {}, + 'shell_executor': {}, + 'file_operation': {} + } + ) + + async with SandboxFactory.create_sandbox(SandboxType.DOCKER, config) as sb: + # 1. Python Executor + # 参数: code (str) + py_res = await sb.execute_tool('python_executor', {'code': 'print(1+1)'}) + print(f"[Tool] Python: {py_res.output.strip()}") + + # 2. Shell Executor + # 参数: command (str), timeout (int, optional) + sh_res = await sb.execute_tool('shell_executor', {'command': 'date'}) + print(f"[Tool] Shell: {sh_res.output.strip()}") + + # 3. File Operation + # 写入 + await sb.execute_tool('file_operation', { + 'operation': 'write', 'file_path': '/sandbox/test.txt', 'content': 'content' + }) + # 读取 + read_res = await sb.execute_tool('file_operation', { + 'operation': 'read', 'file_path': '/sandbox/test.txt' + }) + print(f"[Tool] File Read: {read_res.output}") +``` + +## 4. 手动生命周期管理 + +如果不使用 `async with` 上下文管理器,您需要手动处理启动和释放资源,这在某些异步流控制复杂的场景(如长期持有的连接)中很有用。 + +```python +async def demo_manual_lifecycle(): + config = DockerSandboxConfig(tools_config={'shell_executor': {}}) + + # 1. 创建实例 + sandbox = SandboxFactory.create_sandbox(SandboxType.DOCKER, config) + + try: + # 2. 显式启动 + await sandbox.start() + print("[Manual] Sandbox started") + + # 3. 执行操作 + res = await sandbox.execute_tool('shell_executor', {'command': 'echo manual'}) + print(f"[Manual] Output: {res.output.strip()}") + + finally: + # 4. 显式停止 + await sandbox.stop() + print("[Manual] Cleanup done") +``` diff --git a/docs/zh/docs/getting-started/installation.md b/docs/zh/docs/getting-started/installation.md new file mode 100644 index 0000000..a1942f0 --- /dev/null +++ b/docs/zh/docs/getting-started/installation.md @@ -0,0 +1,45 @@ +# 安装指南 + +## 环境准备 + +在安装 ms-enclave 之前,请确保您的环境满足以下要求: + +* **Python**: 版本 >= 3.10 +* **Docker**: 必须安装并运行 Docker Daemon。这是 ms-enclave 创建隔离沙箱的基础。 + * 如果您打算使用 `Notebook` 沙箱,请确保 Docker 容器可以对外暴露 8888 端口。 + +## 安装 + +### 从 PyPI 安装(推荐) + +使用 pip 直接安装: + +```bash +pip install ms-enclave +``` + +如果需要 Docker 支持(通常都需要),请安装额外依赖: + +```bash +pip install 'ms-enclave[docker]' +``` + +### 从源码安装 + +如果您需要最新的开发版本,可以从 GitHub 克隆源码安装: + +```bash +git clone https://github.com/modelscope/ms-enclave.git +cd ms-enclave +pip install -e . +# 安装 Docker 依赖 +pip install -e '.[docker]' +``` + +## 验证安装 + +安装完成后,您可以运行以下代码来验证是否安装成功: + +```shell +ms-enclave -v +``` diff --git a/docs/zh/docs/getting-started/intro.md b/docs/zh/docs/getting-started/intro.md new file mode 100644 index 0000000..1547085 --- /dev/null +++ b/docs/zh/docs/getting-started/intro.md @@ -0,0 +1,19 @@ +# ms-enclave 中文文档 + +![image](../_static/logo.png) + +ms-enclave 是一个模块化且稳定的沙箱运行时环境,为应用程序提供安全的隔离执行环境。它通过 Docker 容器实现强隔离,配套本地/HTTP 管理器与可扩展工具系统,帮助你在受控环境中安全、高效地执行代码。 + +## 主要特性 + +- **🔒 安全隔离**:基于 Docker 的完全隔离与资源限制 +- **🧩 模块化**:沙箱与工具均可扩展(注册工厂) +- **⚡ 稳定性能**:简洁实现,快速启动,带生命周期管理 +- **🌐 远程管理**:内置 FastAPI 服务,支持 HTTP 管理 +- **🔧 工具体系**:按沙箱类型启用的标准化工具(OpenAI 风格 schema) + +## 系统要求 + +- Python >= 3.10 +- 操作系统:Linux、macOS 或支持 Docker 的 Windows +- 需本机可用的 Docker 守护进程(Notebook 沙箱需开放 8888 端口) diff --git a/docs/zh/docs/getting-started/quickstart.md b/docs/zh/docs/getting-started/quickstart.md new file mode 100644 index 0000000..4225147 --- /dev/null +++ b/docs/zh/docs/getting-started/quickstart.md @@ -0,0 +1,215 @@ +# 快速上手 + +`ms-enclave` 提供了两种主要的使用方式来满足不同的集成需求: + +1. **SandboxFactory**:直接创建沙箱实例。最轻量,适合脚本、测试或一次性任务。 +2. **SandboxManagerFactory**:通过管理器编排沙箱。适合构建服务、后台应用,提供生命周期管理、池化预热和自动清理功能。 + +下面将分别演示这两种方法。 + +## 方式一:轻量级脚本 + +这种方式直接实例化沙箱对象,使用 `async with` 语法确保上下文退出时销毁容器。 + +### 适用场景 + +- **单次任务**: 跑完即走的脚本。 +- **单元测试**: 每个测试用例创建一个全新干净的环境。 +- **简单实验**: 快速验证代码或工具功能。 +- **精细控制**: 需要直接访问沙箱对象底层方法的情况。 + +### 代码示例 + +将以下代码保存为 `quickstart_script.py`: + +```python +import asyncio +from ms_enclave.sandbox.boxes import SandboxFactory +from ms_enclave.sandbox.model import DockerSandboxConfig, SandboxType + +async def main(): + # 1. 配置沙箱 + # 指定镜像和需要启用的工具(如 python_executor, file_operation) + config = DockerSandboxConfig( + image='python:3.11-slim', + tools_config={ + 'python_executor': {}, # 启用代码执行工具 + 'file_operation': {}, # 启用文件操作工具 + } + ) + + print("正在启动沙箱...") + # 2. 创建并启动沙箱 + # 使用 async with 自动管理生命周期(结束时自动销毁容器) + async with SandboxFactory.create_sandbox(SandboxType.DOCKER, config) as sandbox: + print(f"沙箱已就绪 ID: {sandbox.id}") + + # 3. 写入文件 + # 调用 file_operation 工具 + print("正在写入文件...") + await sandbox.execute_tool('file_operation', { + 'operation': 'write', + 'file_path': '/sandbox/hello.txt', + 'content': 'Hello from ms-enclave!' + }) + + # 4. 执行 Python 代码 + # 调用 python_executor 工具读取刚才写入的文件 + print("正在执行代码...") + result = await sandbox.execute_tool('python_executor', { + 'code': """ +print('正在读取文件...') +with open('/sandbox/hello.txt', 'r') as f: + content = f.read() +print(f'文件内容: {content}') +""" + }) + + # 5. 查看输出 + print("执行结果:", result.output) + +if __name__ == '__main__': + asyncio.run(main()) +``` + +### 代码详解 + +1. **`SandboxFactory`**: 它是最底层的工厂类,用于直接创建沙箱实例。 + - `create_sandbox` 返回一个实现了异步上下文管理器协议的对象 (`AsyncContextManager`)。 +2. **`DockerSandboxConfig`**: + - `image`: 指定 Docker 镜像,确保环境一致性。 + - `tools_config`: **关键点**。只有在这里显式启用的工具,才能在沙箱中使用。 +3. **`execute_tool`**: + - 这是与沙箱交互的主要方式。 + - 第一个参数是工具名称(如 `'python_executor'`),这个名字必须对应 `tools_config` 中的键。 + - 第二个参数是传递给工具的参数字典(如 `code`, `file_path` 等),由具体的工具定义。 +4. **生命周期**: + - `async with` 块结束时,会自动调用沙箱的 `stop()` 方法,停止并删除 Docker 容器,防止资源泄漏。 + + +### 运行 + +```bash +python quickstart_script.py +``` + +> **注意**:首次运行时需要拉取 Docker 镜像(如 `python:3.11-slim`),可能需要一些时间。 + +输出示例: +```text +正在启动沙箱... +沙箱已就绪 ID: u53rksn7 +正在写入文件... +正在执行代码... +[INFO:ms_enclave] [📦 u53rksn7] 正在读取文件... +[INFO:ms_enclave] [📦 u53rksn7] 文件内容: Hello from ms-enclave! +执行结果: 正在读取文件... +文件内容: Hello from ms-enclave! +``` + +--- + +## 方式二:应用集成 + +在开发 Web 服务或长期运行的应用时,推荐使用管理器(Manager)。它不仅能在本地运行(`LocalSandboxManager`),还可以无缝切换到远程 HTTP 模式,并提供沙箱池等高级功能。 + +### 适用场景 + +- **Web 服务后端**: 为多个用户请求同时提供沙箱环境。 +- **长期运行的进程**: 需要自动清理过期沙箱,防止资源泄露。 +- **性能敏感**: 利用沙箱池(Pool)技术预热容器,减少启动延迟。 +- **分布式部署**: 将沙箱运行在远程服务器上,通过 HTTP 调用。 + +### 代码示例 + +将以下代码保存为 `quickstart_app.py`: + +```python +import asyncio +from ms_enclave.sandbox.manager import SandboxManagerFactory +from ms_enclave.sandbox.model import DockerSandboxConfig, SandboxType, SandboxManagerConfig, SandboxManagerType + +async def main(): + # 1. 配置管理器 + # 如需使用远程服务,可配置 base_url;这里演示本地模式 + manager_config = SandboxManagerConfig(cleanup_interval=600) # 每10分钟后台清理一次过期沙箱 + + print("正在初始化管理器...") + # 2. 创建管理器 + # 显式指定 Local 类型,或者不传参也会默认使用 Local + async with SandboxManagerFactory.create_manager( + manager_type=SandboxManagerType.LOCAL, + config=manager_config + ) as manager: + + # 3. 配置沙箱 + sb_config = DockerSandboxConfig( + image='python:3.11-slim', + tools_config={'python_executor': {}} + ) + + # 4. 通过管理器创建沙箱 + # 管理器会跟踪这个沙箱的状态,并返回 sandbox_id + sandbox_id = await manager.create_sandbox(SandboxType.DOCKER, sb_config) + print(f"沙箱已创建 ID: {sandbox_id}") + + # 5. 执行工具 + # 所有的操作都通过 manager 代理进行,需传入 sandbox_id + print("正在执行代码...") + result = await manager.execute_tool( + sandbox_id, + 'python_executor', + {'code': 'import sys; print(f"Python Version: {sys.version}")'} + ) + print(f"输出结果:\n{result.output.strip()}") + + # 6. 获取沙箱列表 + # 查看当前管理器纳管的所有沙箱 + sandboxes = await manager.list_sandboxes() + print(f"当前活跃沙箱数: {len(sandboxes)}") + +if __name__ == '__main__': + asyncio.run(main()) +``` + +### 代码详解 + +1. **`SandboxManagerFactory`**: 它是管理器的入口。 + - 如果提供了 `base_url`(如 `http://localhost:8000`),它会创建一个连接远程服务的 `HttpSandboxManager`。 + - 否则,它创建运行在当前进程内的 `LocalSandboxManager`。 + - 这使得你的业务代码可以在本地开发和分布式部署之间无缝切换。 + +2. **管理器操作 (`manager`)**: + - `create_sandbox`: 不同于 `SandboxFactory`,这里返回的是 `sandbox_id` 字符串,而不是对象。 + - `execute_tool`: 需要传入 `sandbox_id` 来指定目标沙箱。 + - `list_sandboxes`: 方便监控系统内所有沙箱的状态。 + +3. **资源清理**: + - `LocalSandboxManager` 包含一个后台任务,会自动清理状态异常或长期闲置(默认 48小时)的沙箱,增强了系统的健壮性。 + +### 运行 + +```bash +python quickstart_app.py +``` + +输出示例: +```text +正在初始化管理器... +[INFO:ms_enclave] Local sandbox manager started +[INFO:ms_enclave] Created and started sandbox 98to5a2p of type docker +沙箱已创建 ID: 98to5a2p +正在执行代码... +[INFO:ms_enclave] [📦 98to5a2p] Python Version: 3.11.14 (main, Nov 18 2025, 04:42:43) [GCC 14.2.0] +输出结果: +Python Version: 3.11.14 (main, Nov 18 2025, 04:42:43) [GCC 14.2.0] +当前活跃沙箱数: 1 +[INFO:ms_enclave] Cleaning up 1 sandboxes +[INFO:ms_enclave] Deleted sandbox 98to5a2p +[INFO:ms_enclave] Local sandbox manager stopped +``` + +## 总结 + +- **做实验、写脚本、单元测试** -> 推荐 **SandboxFactory**。 +- **写后端服务、任务调度、生产环境** -> 推荐 **SandboxManagerFactory**。 diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml new file mode 100644 index 0000000..35c804e --- /dev/null +++ b/docs/zh/mkdocs.yml @@ -0,0 +1,198 @@ +site_name: ms-enclave 文档 +site_url: !ENV READTHEDOCS_CANONICAL_URL + +# Add GitHub repo info for header link and star count +repo_name: modelscope/ms-enclave +repo_url: https://github.com/modelscope/ms-enclave +edit_uri: edit/main/docs/zh/docs/ + +nav: + - 快速开始: + - 介绍: getting-started/intro.md + - 安装: getting-started/installation.md + - 上手示例: getting-started/quickstart.md + - 核心概念: + - basic/concepts.md + - 基础使用: + - basic/usage.md + - 进阶教程: + - HTTP 服务: advanced/server.md + - 自定义扩展: advanced/customization.md + - API 参考: + - 概览: api/index.md + - 管理器: api/manager.md + - 沙箱: api/sandbox.md + - 工具: api/tools.md + +theme: + name: material + features: + - navigation.instant + - navigation.tabs + - navigation.expand + - navigation.path + - navigation.indexes + - navigation.top + - toc.integrate + - toc.follow + - search + - search.highlight + - search.suggest + - content.code.copy + - content.code.annotate + + palette: + # Palette toggle for automatic mode + - media: "(prefers-color-scheme)" + primary: indigo + toggle: + icon: material/brightness-auto + name: Switch to light mode + + # Palette toggle for light mode + - media: "(prefers-color-scheme: light)" + primary: indigo + scheme: default + toggle: + icon: material/brightness-7 + name: Switch to dark mode + + # Palette toggle for dark mode + - media: "(prefers-color-scheme: dark)" + primary: indigo + scheme: slate + toggle: + icon: material/brightness-4 + name: Switch to system preference + +# Add social links (bottom right) +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/modelscope/ms-enclave + - icon: fontawesome/brands/python + link: https://pypi.org/project/ms-enclave/ + +# Configure extensions for highlighting and fancy components +markdown_extensions: + - admonition + - abbr + - attr_list + - def_list + - footnotes + - meta + - md_in_html + - toc: + permalink: true + - pymdownx.details + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.superfences + - pymdownx.tabbed: + alternate_style: true + - pymdownx.tasklist: + custom_checkbox: true + +plugins: + - search + - mkdocstrings: + handlers: + # See: https://mkdocstrings.github.io/python/usage/ + python: + # Where to find your sources, see "Finding modules". + paths: [../../] + + # Load object inventories to enable cross-references to other projects. + inventories: + - https://docs.python.org/3/objects.inv + # Also load inventories of your dependencies, generally served at + # https://docs-url-for-your-dependency/objects.inv. + + options: + + # DOCSTRINGS ------------------------------------------------------------- + docstring_options: + # Discard first line of `__init__` method docstrings, + # useful when merging such docstrings into their parent class'. + ignore_init_summary: true + + # Tables are generally too large, lists will fix this. + docstring_section_style: list + + # CROSS-REFERENCES ------------------------------------------------------- + # Enable relative crossrefs and scoped crossrefs, see Docstrings options. + relative_crossrefs: true # Sponsors only! + scoped_crossrefs: true # Sponsors only! + + # Enable cross-references in signatures. + signature_crossrefs: true + + # Unwrap actual types from `Annotated` type annotations. + unwrap_annotated: true + + # MEMBERS ---------------------------------------------------------------- + # Only render pulic symbols. + filters: public # Sponsors only! + # Comment the option otherwise to get the default filters. + + # Show class inherited members. + inherited_members: true + + # Render auto-generated summaries of attributes, functions, etc. + # at the start of each symbol's documentation. + summary: true + + # HEADINGS --------------------------------------------------------------- + # For auto-generated pages, one module per page, + # make the module heading be the H1 heading of the page. + heading_level: 1 + + # Render headings for parameters, making them linkable. + parameter_headings: true + + # Render headings for type parameters too. + type_parameter_headings: true + + # Always show the heading for the symbol you render with `::: id`. + show_root_heading: true + + # Only show the name of the symbols you inject render `::: id`. + show_root_full_path: false + + # Show the type of symbol (class, function, etc.) in the heading. + show_symbol_type_heading: true + + # Show the type of symbol (class, function, etc.) in the table of contents. + show_symbol_type_toc: true + + # SIGNATURES ------------------------------------------------------------- + # Format code to 80 + 10% margin (Black and Ruff defaults) + # in signatures and attribute value code blocks. + # Needs Black/Ruff installed. + line_length: 88 + + # Merge signature and docstring of `__init__` methods + # into their parent class signature and docstring. + merge_init_into_class: true + + # Render signatures and attribute values in a separate code block, + # below the symbol heading. + separate_signature: true + + # Show type annotations in signatures. + show_signature_annotations: true + + # Show type parameters in signatures. + show_signature_type_parameters: true + + # OTHER ------------------------------------------------------------------ + # Show backlinks to other documentation sections within each symbol. + backlinks: tree # Sponsors only! + + # Show base classes OR inheritance diagram. + show_bases: false + show_inheritance_diagram: true # Sponsors only! diff --git a/examples/basic_usage.py b/examples/basic_usage.py new file mode 100644 index 0000000..27f6b92 --- /dev/null +++ b/examples/basic_usage.py @@ -0,0 +1,284 @@ +import asyncio +import os +import shutil + +from ms_enclave.sandbox.boxes import SandboxFactory +from ms_enclave.sandbox.manager import HttpSandboxManager, LocalSandboxManager, SandboxManagerFactory +from ms_enclave.sandbox.model import DockerSandboxConfig, SandboxManagerConfig, SandboxManagerType, SandboxType + +# ========================================== +# 1. 典型使用场景 +# ========================================== + +async def demo_sandbox_factory(): + """ + 1.1 快速开始:SandboxFactory (最轻量) + 适合脚本或一次性任务。 + """ + print('\n--- Demo: SandboxFactory ---') + config = DockerSandboxConfig( + image='python:3.11-slim', + tools_config={'python_executor': {}} + ) + + async with SandboxFactory.create_sandbox(SandboxType.DOCKER, config) as sandbox: + result = await sandbox.execute_tool('python_executor', { + 'code': 'import sys; print(f"Python {sys.version.split()[0]}")' + }) + print(f'[SandboxFactory] Output: {result.output.strip()}') + + +async def demo_manager_factory(): + """ + 1.2 统一管理入口:SandboxManagerFactory + 自动选择本地或远程管理器。 + """ + print('\n--- Demo: SandboxManagerFactory ---') + cfg = SandboxManagerConfig(cleanup_interval=600) + async with SandboxManagerFactory.create_manager( + manager_type=SandboxManagerType.LOCAL, config=cfg + ) as manager: + print(f'[ManagerFactory] Created manager: {type(manager).__name__}') + + +async def demo_local_manager(): + """ + 1.3 本地编排:LocalSandboxManager (多任务并行) + 适合在同一进程内需要创建、管理多个沙箱。 + """ + print('\n--- Demo: LocalSandboxManager ---') + async with LocalSandboxManager() as manager: + config = DockerSandboxConfig(tools_config={'shell_executor': {}}) + # 创建沙箱 + sandbox_id = await manager.create_sandbox(SandboxType.DOCKER, config) + + # 执行工具 + res = await manager.execute_tool( + sandbox_id, 'shell_executor', {'command': 'echo "Hello Local Manager"'} + ) + print(f'[LocalManager] Output: {res.output.strip()}') + + # 获取列表 + sandboxes = await manager.list_sandboxes() + print(f'[LocalManager] Active sandboxes: {len(sandboxes)}') + + # 手动清理(在 async with 退出时其实也会清理,这里演示显式调用) + await manager.delete_sandbox(sandbox_id) + + +async def demo_http_manager(): + """ + 1.4 远程管理:HttpSandboxManager + 需要先启动 server。这里加了 try-except 以防止未启动 server 导致脚本 crash。 + """ + print('\n--- Demo: HttpSandboxManager ---') + try: + # 假设服务运行在本地 8000 端口 + async with HttpSandboxManager(base_url='http://127.0.0.1:8000') as manager: + # 简单的连通性测试,如果连不上 create_sandbox 会报错 + config = DockerSandboxConfig(tools_config={'python_executor': {}}) + sid = await manager.create_sandbox(SandboxType.DOCKER, config) + res = await manager.execute_tool(sid, 'python_executor', {'code': 'print("Remote Hello")'}) + print(f'[HttpManager] Output: {res.output.strip()}') + await manager.delete_sandbox(sid) + except Exception as e: + print(f'[HttpManager] Skipped or Failed: {e}') + + +async def demo_sandbox_pool(): + """ + 1.5 高性能模式:Sandbox Pool (预热复用) + """ + print('\n--- Demo: Sandbox Pool ---') + async with LocalSandboxManager() as manager: + config = DockerSandboxConfig( + image='python:3.11-slim', + tools_config={'python_executor': {}} + ) + # 初始化池,预热 1 个沙箱 (为了演示速度设为1) + print('[Pool] Initializing pool...') + await manager.initialize_pool(pool_size=1, sandbox_type=SandboxType.DOCKER, config=config) + + # 借用沙箱执行任务 + result = await manager.execute_tool_in_pool( + 'python_executor', + {'code': 'print("Executed in pool")'}, + timeout=30 + ) + print(f'[Pool] Output: {result.output.strip()}') + + stats = await manager.get_stats() + print(f'[Pool] Stats: {stats}') + + +# ========================================== +# 2. 高级功能 +# ========================================== + +async def demo_install_dependencies(): + """ + 2.1 在 Sandbox 中安装额外依赖 + """ + print('\n--- Demo: Install Dependencies ---') + config = DockerSandboxConfig( + image='python:3.11-slim', + tools_config={'python_executor': {}, 'file_operation': {}, 'shell_executor': {}} + ) + + async with SandboxFactory.create_sandbox(SandboxType.DOCKER, config) as sandbox: + print('[Deps] Installing dependencies (this may take a moment)...') + # 1. 写入 requirements.txt + await sandbox.execute_tool('file_operation', { + 'operation': 'write', + 'file_path': '/sandbox/requirements.txt', + 'content': 'packaging' # 使用一个较小的包演示 + }) + + # 2. 执行安装命令 + try: + # 注意:实际环境中可能需要网络权限,默认 DockerSandbox 是开启网络的 + install_res = await sandbox.execute_command('pip install -r /sandbox/requirements.txt') + if install_res.exit_code != 0: + print(f'[Deps] Install failed: {install_res.stderr} {install_res.stdout}') + else: + # 3. 验证安装 + res = await sandbox.execute_tool('python_executor', { + 'code': 'import packaging; print(f"Packaging version: {packaging.__version__}")' + }) + print(f'[Deps] Result: {res.output.strip()}') + except Exception as e: + print(f'[Deps] Error: {e}') + + +async def demo_host_volume(): + """ + 2.2 读写宿主机文件 + """ + print('\n--- Demo: Host Volume Mounting ---') + # 在宿主机创建一个临时目录用于测试 + host_dir = os.path.abspath('./temp_sandbox_data') + os.makedirs(host_dir, exist_ok=True) + try: + with open(os.path.join(host_dir, 'host_file.txt'), 'w') as f: + f.write('Hello from Host') + + # 配置挂载:宿主机路径 -> 容器内路径 + config = DockerSandboxConfig( + image='python:3.11-slim', + tools_config={'shell_executor': {}}, + volumes={host_dir: {'bind': '/sandbox/data', 'mode': 'rw'}} + ) + + async with SandboxFactory.create_sandbox(SandboxType.DOCKER, config) as sandbox: + # 读取宿主机文件 + res = await sandbox.execute_tool('shell_executor', { + 'command': 'cat /sandbox/data/host_file.txt' + }) + print(f'[Volume] Read from host: {res.output.strip()}') + + # 写入文件回宿主机 + await sandbox.execute_tool('shell_executor', { + 'command': 'echo "Response from Sandbox" > /sandbox/data/sandbox_file.txt' + }) + + # 验证宿主机上的文件 + if os.path.exists(os.path.join(host_dir, 'sandbox_file.txt')): + with open(os.path.join(host_dir, 'sandbox_file.txt'), 'r') as f: + print(f'[Volume] Read from sandbox write: {f.read().strip()}') + else: + print('[Volume] File not written back to host.') + + finally: + # 清理 + if os.path.exists(host_dir): + shutil.rmtree(host_dir) + +# ========================================== +# 3. 工具使用详解 +# ========================================== + +async def demo_tools_usage(): + """ + 展示常用工具 + """ + print('\n--- Demo: Tools Usage ---') + config = DockerSandboxConfig( + image='python:3.11-slim', + tools_config={ + 'python_executor': {}, + 'shell_executor': {}, + 'file_operation': {} + } + ) + + async with SandboxFactory.create_sandbox(SandboxType.DOCKER, config) as sb: + # 1. Python Executor + py_res = await sb.execute_tool('python_executor', {'code': 'print(100 * 2)'}) + print(f'[Tool] Python: {py_res.output.strip()}') + + # 2. Shell Executor + sh_res = await sb.execute_tool('shell_executor', {'command': 'echo "shell works"'}) + print(f'[Tool] Shell: {sh_res.output.strip()}') + + # 3. File Operation + await sb.execute_tool('file_operation', { + 'operation': 'write', 'file_path': '/sandbox/test.txt', 'content': 'file content' + }) + read_res = await sb.execute_tool('file_operation', { + 'operation': 'read', 'file_path': '/sandbox/test.txt' + }) + print(f'[Tool] File Read: {read_res.output}') + +# ========================================== +# 4. 手动生命周期管理 +# ========================================== + +async def demo_manual_lifecycle(): + """ + 不使用 async with + """ + print('\n--- Demo: Manual Lifecycle ---') + config = DockerSandboxConfig(tools_config={'shell_executor': {}}) + + # 1. 创建实例 + sandbox = SandboxFactory.create_sandbox(SandboxType.DOCKER, config) + + try: + # 2. 显式启动 + await sandbox.start() + print('[Manual] Sandbox started') + + # 3. 执行操作 + res = await sandbox.execute_tool('shell_executor', {'command': 'echo manual'}) + print(f'[Manual] Output: {res.output.strip()}') + + finally: + # 4. 显式停止 + await sandbox.stop() + print('[Manual] Cleanup done') + + +async def main(): + print('Starting ms-enclave basic usage demos...') + + # 典型场景 + # await demo_sandbox_factory() + # await demo_manager_factory() + # await demo_local_manager() + # # await demo_http_manager() # 需要手动开启 Server,默认注释 + # await demo_sandbox_pool() + + # # 高级功能 + # await demo_install_dependencies() + # await demo_host_volume() + + # # 工具详解 + # await demo_tools_usage() + + # 手动生命周期 + await demo_manual_lifecycle() + + print('\nAll demos finished.') + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/examples/client_script.py b/examples/client_script.py new file mode 100644 index 0000000..9f46ece --- /dev/null +++ b/examples/client_script.py @@ -0,0 +1,28 @@ +import asyncio + +from ms_enclave.sandbox.manager import SandboxManagerFactory +from ms_enclave.sandbox.model import DockerSandboxConfig, SandboxType + + +async def main(): + # 连接到远程服务器 + async with SandboxManagerFactory.create_manager(base_url='http://127.0.0.1:8000') as manager: + + # 创建沙箱(在服务器端创建 Docker 容器) + config = DockerSandboxConfig( + image='python:3.11-slim', + tools_config={'python_executor': {}} + ) + + sandbox_id = await manager.create_sandbox(SandboxType.DOCKER, config) + print(f'远程沙箱 ID: {sandbox_id}') + + # 执行工具 + result = await manager.execute_tool(sandbox_id, 'python_executor', { + 'code': 'import platform; print(platform.node())' + }) + print(f'远程执行结果: {result.output}') + + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/examples/local_manager_example.py b/examples/local_manager_example.py deleted file mode 100644 index d508f87..0000000 --- a/examples/local_manager_example.py +++ /dev/null @@ -1,250 +0,0 @@ -#!/usr/bin/env python3 -"""Example demonstrating local sandbox manager usage with Docker Notebook.""" - -import asyncio -from typing import Any, Dict - -from ms_enclave.sandbox.manager import HttpSandboxManager, LocalSandboxManager -from ms_enclave.sandbox.model import DockerNotebookConfig, SandboxStatus, SandboxType -from ms_enclave.utils import get_logger - -logger = get_logger() - - -async def demonstrate_local_manager(): - """Demonstrate local manager functionality with Docker Notebook.""" - # Initialize local manager - # manager = HttpSandboxManager(base_url='http://127.0.0.1:8000') - manager = LocalSandboxManager(cleanup_interval=300) - - try: - # Start the manager - await manager.start() - logger.info('Local manager started') - - # Get initial stats - logger.info('=== Initial Manager Stats ===') - initial_stats = await manager.get_stats() - logger.info(f'Initial stats: {initial_stats}') - - # Create a Docker Notebook sandbox - logger.info('=== Creating Docker Notebook Sandbox ===') - config = DockerNotebookConfig( - image='jupyter-kernel-gateway', - tools_config={ - 'notebook_executor': {} - } - ) - - sandbox_id = await manager.create_sandbox( - sandbox_type=SandboxType.DOCKER_NOTEBOOK, - config=config - ) - logger.info(f'Created notebook sandbox: {sandbox_id}') - - # Get sandbox info - logger.info('=== Getting Sandbox Info ===') - sandbox_info = await manager.get_sandbox_info(sandbox_id) - if sandbox_info: - logger.info(f'Sandbox info: {sandbox_info.model_dump_json()}') - else: - logger.error('Failed to get sandbox info') - return - - # List all sandboxes - logger.info('=== Listing All Sandboxes ===') - sandboxes = await manager.list_sandboxes() - logger.info(f'Total sandboxes: {len(sandboxes)}') - for sb in sandboxes: - logger.info(f' - {sb.id} ({sb.status}) - {sb.type}') - - # Wait for sandbox to be running - logger.info('=== Waiting for Sandbox to be Ready ===') - max_wait = 60 - for i in range(max_wait): - info = await manager.get_sandbox_info(sandbox_id) - if info and info.status == SandboxStatus.RUNNING: - logger.info('Sandbox is running!') - break - elif info and info.status == SandboxStatus.ERROR: - logger.error('Sandbox failed to start') - break - logger.info(f'Waiting for sandbox to start... ({i+1}/{max_wait})') - await asyncio.sleep(2) - - # Get available tools - logger.info('=== Getting Available Tools ===') - try: - tools = await manager.get_sandbox_tools(sandbox_id) - logger.info(f'Available tools: {tools}') - except Exception as e: - logger.error(f'Error getting tools: {e}') - - # Execute notebook code - Basic Python - logger.info('=== Executing Notebook Code - Basic Python ===') - try: - result = await manager.execute_tool( - sandbox_id=sandbox_id, - tool_name='notebook_executor', - parameters={ - 'code': ''' -import sys -import os -print(f"Python version: {sys.version}") -print(f"Current directory: {os.getcwd()}") - -# Simple calculation -x = 10 -y = 20 -result = x + y -print(f"{x} + {y} = {result}") -''' - } - ) - logger.info(f'Notebook execution result:') - logger.info(f' Status: {result.status}') - logger.info(f' Output: {result.output}') - if result.error: - logger.info(f' Error: {result.error}') - except Exception as e: - logger.error(f'Error executing notebook code: {e}') - - # Execute notebook code - Data Analysis - logger.info('=== Executing Notebook Code - Data Analysis ===') - try: - data_analysis_code = ''' -# Create and analyze some data -import json -import statistics - -# Sample data -data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] -print(f"Original data: {data}") - -# Calculate statistics -mean = statistics.mean(data) -median = statistics.median(data) -stdev = statistics.stdev(data) - -results = { - "mean": mean, - "median": median, - "standard_deviation": stdev, - "min": min(data), - "max": max(data) -} - -print("Statistical Analysis:") -for key, value in results.items(): - print(f" {key}: {value:.2f}") - -# Show the results as JSON -print("\\nJSON Output:") -print(json.dumps(results, indent=2)) -''' - - result = await manager.execute_tool( - sandbox_id=sandbox_id, - tool_name='notebook_executor', - parameters={'code': data_analysis_code} - ) - logger.info(f'Data analysis result:') - logger.info(f' Status: {result.status}') - logger.info(f' Output: {result.output}') - if result.error: - logger.info(f' Error: {result.error}') - except Exception as e: - logger.error(f'Error executing data analysis code: {e}') - - # Execute notebook code - State Persistence Test - logger.info('=== Testing State Persistence ===') - try: - # First execution - set variables - result1 = await manager.execute_tool( - sandbox_id=sandbox_id, - tool_name='notebook_executor', - parameters={ - 'code': ''' -# Set some variables -global_var = "Hello from notebook!" -counter = 42 -my_list = [1, 2, 3] - -print(f"Set global_var = '{global_var}'") -print(f"Set counter = {counter}") -print(f"Set my_list = {my_list}") -''' - } - ) - logger.info(f'State setting result: {result1.status}') - - # Second execution - use variables from previous execution - result2 = await manager.execute_tool( - sandbox_id=sandbox_id, - tool_name='notebook_executor', - parameters={ - 'code': ''' -# Use variables from previous execution -print(f"Retrieved global_var = '{global_var}'") -print(f"Retrieved counter = {counter}") -print(f"Retrieved my_list = {my_list}") - -# Modify and extend -counter += 10 -my_list.append(4) - -print(f"Modified counter = {counter}") -print(f"Modified my_list = {my_list}") -''' - } - ) - logger.info(f'State persistence result: {result2.status}') - logger.info(f' Output: {result2.output}') - - except Exception as e: - logger.error(f'Error testing state persistence: {e}') - - # List sandboxes with status filter - logger.info('=== Listing Running Sandboxes ===') - running_sandboxes = await manager.list_sandboxes(status_filter=SandboxStatus.RUNNING) - logger.info(f'Running sandboxes: {len(running_sandboxes)}') - - # Clean up - delete the sandbox - logger.info('=== Cleaning Up ===') - success = await manager.delete_sandbox(sandbox_id) - if success: - logger.info(f'Successfully deleted sandbox {sandbox_id}') - else: - logger.error(f'Failed to delete sandbox {sandbox_id}') - - # Final stats - logger.info('=== Final Manager Stats ===') - final_stats = await manager.get_stats() - logger.info(f'Final stats: {final_stats}') - - except Exception as e: - logger.error(f'Error in demonstration: {e}') - finally: - # Stop the manager - await manager.stop() - logger.info('Local manager stopped') - - -async def main(): - """Main example function.""" - logger.info('=== Local Sandbox Manager with Docker Notebook Example ===') - - # Demonstrate local manager functionality - await demonstrate_local_manager() - - logger.info('=== Example Complete ===') - - -if __name__ == '__main__': - try: - asyncio.run(main()) - except KeyboardInterrupt: - logger.info('Example interrupted by user') - except Exception as e: - logger.error(f'Example failed: {e}') - raise diff --git a/examples/quickstart_app.py b/examples/quickstart_app.py new file mode 100644 index 0000000..1dffaee --- /dev/null +++ b/examples/quickstart_app.py @@ -0,0 +1,47 @@ +import asyncio + +from ms_enclave.sandbox.manager import SandboxManagerFactory +from ms_enclave.sandbox.model import DockerSandboxConfig, SandboxManagerConfig, SandboxManagerType, SandboxType + + +async def main(): + # 1. 配置管理器 + # 如需使用远程服务,可配置 base_url;这里演示本地模式 + manager_config = SandboxManagerConfig(cleanup_interval=600) # 每10分钟后台清理一次过期沙箱 + + print('正在初始化管理器...') + # 2. 创建管理器 + # 显式指定 Local 类型,或者不传参也会默认使用 Local + async with SandboxManagerFactory.create_manager( + manager_type=SandboxManagerType.LOCAL, + config=manager_config + ) as manager: + + # 3. 配置沙箱 + sb_config = DockerSandboxConfig( + image='python:3.11-slim', + tools_config={'python_executor': {}} + ) + + # 4. 通过管理器创建沙箱 + # 管理器会跟踪这个沙箱的状态,并返回 sandbox_id + sandbox_id = await manager.create_sandbox(SandboxType.DOCKER, sb_config) + print(f'沙箱已创建 ID: {sandbox_id}') + + # 5. 执行工具 + # 所有的操作都通过 manager 代理进行,需传入 sandbox_id + print('正在执行代码...') + result = await manager.execute_tool( + sandbox_id, + 'python_executor', + {'code': 'import sys; print(f"Python Version: {sys.version}")'} + ) + print(f'输出结果:\n{result.output.strip()}') + + # 6. 获取沙箱列表 + # 查看当前管理器纳管的所有沙箱 + sandboxes = await manager.list_sandboxes() + print(f'当前活跃沙箱数: {len(sandboxes)}') + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/examples/quickstart_script.py b/examples/quickstart_script.py new file mode 100644 index 0000000..91d6a07 --- /dev/null +++ b/examples/quickstart_script.py @@ -0,0 +1,49 @@ +import asyncio + +from ms_enclave.sandbox.boxes import SandboxFactory +from ms_enclave.sandbox.model import DockerSandboxConfig, SandboxType + + +async def main(): + # 1. 配置沙箱 + # 指定镜像和需要启用的工具(如 python_executor, file_operation) + config = DockerSandboxConfig( + image='python:3.11-slim', + tools_config={ + 'python_executor': {}, # 启用代码执行工具 + 'file_operation': {}, # 启用文件操作工具 + } + ) + + print('正在启动沙箱...') + # 2. 创建并启动沙箱 + # 使用 async with 自动管理生命周期(结束时自动销毁容器) + async with SandboxFactory.create_sandbox(SandboxType.DOCKER, config) as sandbox: + print(f'沙箱已就绪 ID: {sandbox.id}') + + # 3. 写入文件 + # 调用 file_operation 工具 + print('正在写入文件...') + await sandbox.execute_tool('file_operation', { + 'operation': 'write', + 'file_path': '/sandbox/hello.txt', + 'content': 'Hello from ms-enclave!' + }) + + # 4. 执行 Python 代码 + # 调用 python_executor 工具读取刚才写入的文件 + print('正在执行代码...') + result = await sandbox.execute_tool('python_executor', { + 'code': """ +print('正在读取文件...') +with open('/sandbox/hello.txt', 'r') as f: + content = f.read() +print(f'文件内容: {content}') +""" + }) + + # 5. 查看输出 + print('执行结果:', result.output) + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/examples/sandbox_usage_examples.py b/examples/sandbox_usage_examples.py deleted file mode 100644 index 8f206fb..0000000 --- a/examples/sandbox_usage_examples.py +++ /dev/null @@ -1,187 +0,0 @@ -"""Example usage of the sandbox system.""" - -import asyncio - -from ms_enclave.sandbox.boxes import SandboxFactory -from ms_enclave.sandbox.model import DockerSandboxConfig, SandboxType -from ms_enclave.sandbox.tools import ToolFactory - - -async def direct_sandbox_example(): - """Example using sandbox directly.""" - print('=== Direct Sandbox Example ===') - - # Create Docker sandbox configuration - config = DockerSandboxConfig( - image='python:3.11-slim', - timeout=30, - memory_limit='512m', - cpu_limit=1.0, - tools_config={ - 'python_executor': {} # Enable Python executor tool - } - ) - - # Create and use sandbox with context manager - async with SandboxFactory.create_sandbox(SandboxType.DOCKER, config) as sandbox: - print(f'Created sandbox: {sandbox.id}') - print(f'Sandbox status: {sandbox.status}') - - # Execute Python code using tool - result = await sandbox.execute_tool('python_executor', { - 'code': "print('Hello from sandbox!')\nresult = 2 + 2\nprint(f'2 + 2 = {result}')", - 'timeout': 30 - }) - print(f'Python execution result: {result.output}') - if result.error: - print(f'Error: {result.error}') - - # Execute another Python script - result = await sandbox.execute_tool('python_executor', { - 'code': ''' -import os -import sys -print(f"Python version: {sys.version}") -print(f"Working directory: {os.getcwd()}") -print(f"Current user: {os.getenv('USER', 'unknown')}") - -# Create some data -data = [i**2 for i in range(10)] -print(f"Squares: {data}") -''' - }) - print(f'System info result: {result.output}') - - # Get available tools - tools = sandbox.get_available_tools() - print(f'Available tools: {list(tools.keys())}') - - # Get sandbox info - info = sandbox.get_info() - print(f'Sandbox info: {info.type}, Status: {info.status}') - - print('Sandbox automatically cleaned up') - - -async def tool_factory_example(): - """Example using ToolFactory directly.""" - print('\n=== Tool Factory Example ===') - - # Get available tools - available_tools = ToolFactory.get_available_tools() - print(f'Available tools: {available_tools}') - - # Create a Python executor tool - try: - python_tool = ToolFactory.create_tool('python_executor') - print(f'Created tool: {python_tool.name}') - print(f'Tool description: {python_tool.description}') - print(f'Tool schema: {python_tool.schema}') - print(f'Required sandbox type: {python_tool.required_sandbox_type}') - except Exception as e: - print(f'Failed to create tool: {e}') - - -async def multiple_sandboxes_example(): - """Example using multiple sandboxes.""" - print('\n=== Multiple Sandboxes Example ===') - - config1 = DockerSandboxConfig( - image='python:3.11-slim', - tools_config={'python_executor': {}}, - working_dir='/workspace' - ) - - config2 = DockerSandboxConfig( - image='python:3.9-slim', - tools_config={'python_executor': {}}, - working_dir='/app' - ) - - # Create multiple sandboxes - sandbox1 = SandboxFactory.create_sandbox(SandboxType.DOCKER, config1) - sandbox2 = SandboxFactory.create_sandbox(SandboxType.DOCKER, config2) - - try: - await sandbox1.start() - await sandbox2.start() - - print(f'Sandbox 1: {sandbox1.id} (Python 3.11)') - print(f'Sandbox 2: {sandbox2.id} (Python 3.9)') - - # Execute code in both sandboxes - code = """ -import sys -print(f"Python version: {sys.version_info.major}.{sys.version_info.minor}") -print(f"Working directory: {__import__('os').getcwd()}") -""" - - result1 = await sandbox1.execute_tool('python_executor', {'code': code}) - result2 = await sandbox2.execute_tool('python_executor', {'code': code}) - - print(f'Sandbox 1 result:\n{result1.output}') - print(f'Sandbox 2 result:\n{result2.output}') - - finally: - await sandbox1.stop() - await sandbox1.cleanup() - await sandbox2.stop() - await sandbox2.cleanup() - - -async def error_handling_example(): - """Example demonstrating error handling.""" - print('\n=== Error Handling Example ===') - - config = DockerSandboxConfig( - image='python:3.11-slim', - tools_config={'python_executor': {}}, - timeout=5 # Short timeout for demonstration - ) - - async with SandboxFactory.create_sandbox(SandboxType.DOCKER, config) as sandbox: - # Test various error scenarios - - # 1. Syntax error - print('1. Testing syntax error...') - result = await sandbox.execute_tool('python_executor', { - 'code': 'print("Hello" # Missing closing parenthesis' - }) - print(f'Syntax error result: {result.status}') - if result.error: - print(f'Error: {result.error[:100]}...') - - # 2. Runtime error - print('\n2. Testing runtime error...') - result = await sandbox.execute_tool('python_executor', { - 'code': 'print(1/0)' # Division by zero - }) - print(f'Runtime error result: {result.status}') - if result.error: - print(f'Error: {result.error[:100]}...') - - # 3. Successful execution - print('\n3. Testing successful execution...') - result = await sandbox.execute_tool('python_executor', { - 'code': 'print("This should work fine!")' - }) - print(f'Success result: {result.status}') - print(f'Output: {result.output.strip()}') - - -async def main(): - """Run all examples.""" - print('Sandbox System Examples') - print('======================') - - # Run all examples - await direct_sandbox_example() - await tool_factory_example() - await multiple_sandboxes_example() - await error_handling_example() - - print('\n=== Examples completed ===') - - -if __name__ == '__main__': - asyncio.run(main()) diff --git a/examples/server_manager_example.py b/examples/server_manager_example.py deleted file mode 100644 index 09e1c2d..0000000 --- a/examples/server_manager_example.py +++ /dev/null @@ -1,215 +0,0 @@ -#!/usr/bin/env python3 -"""Example demonstrating sandbox server startup and HTTP manager usage.""" - -import asyncio -import threading -from typing import Any, Dict - -from ms_enclave.sandbox import create_server -from ms_enclave.sandbox.manager import HttpSandboxManager -from ms_enclave.sandbox.model import DockerSandboxConfig, SandboxStatus, SandboxType -from ms_enclave.utils import get_logger - -logger = get_logger() - - -def start_server_in_thread(): - """Start the sandbox server in a separate thread.""" - logger.info('Starting sandbox server in background thread...') - server = create_server(cleanup_interval=300) - server.run(host='127.0.0.1', port=8000, log_level='info') - - -async def wait_for_server(base_url: str, max_attempts: int = 30, delay: float = 1.0) -> bool: - """Wait for server to be ready.""" - import aiohttp - - for attempt in range(max_attempts): - try: - async with aiohttp.ClientSession() as session: - async with session.get(f'{base_url}/health') as response: - if response.status == 200: - logger.info('Server is ready!') - return True - except: - pass - - logger.info(f'Waiting for server... (attempt {attempt + 1}/{max_attempts})') - await asyncio.sleep(delay) - - return False - - -async def demonstrate_http_manager(): - """Demonstrate HTTP manager functionality.""" - # Initialize HTTP manager - base_url = 'http://127.0.0.1:8000' - manager = HttpSandboxManager(base_url=base_url, timeout=30) - - try: - # Start the manager - await manager.start() - logger.info('HTTP manager started') - - # Health check - logger.info('=== Health Check ===') - health = await manager.health_check() - logger.info(f'Health status: {health}') - - # Get server stats - logger.info('=== Server Stats ===') - server_stats = await manager.get_stats() - logger.info(f'Server stats: {server_stats}') - - # Create a Docker sandbox - logger.info('=== Creating Docker Sandbox ===') - config = DockerSandboxConfig( - image='python:3.11-slim', - tools_config={ - 'shell_executor': {}, - 'python_executor': {} - } - ) - - sandbox_id = await manager.create_sandbox( - sandbox_type=SandboxType.DOCKER, - config=config - ) - logger.info(f'Created sandbox: {sandbox_id}') - - # Get sandbox info - logger.info('=== Getting Sandbox Info ===') - sandbox_info = await manager.get_sandbox_info(sandbox_id) - if sandbox_info: - logger.info(f'Sandbox info: {sandbox_info.model_dump_json()}') - else: - logger.error('Failed to get sandbox info') - return - - # List all sandboxes - logger.info('=== Listing All Sandboxes ===') - sandboxes = await manager.list_sandboxes() - logger.info(f'Total sandboxes: {len(sandboxes)}') - for sb in sandboxes: - logger.info(f' - {sb.id} ({sb.status}) - {sb.type}') - - # Wait for sandbox to be running - logger.info('=== Waiting for Sandbox to be Ready ===') - max_wait = 30 - for i in range(max_wait): - info = await manager.get_sandbox_info(sandbox_id) - if info and info.status == SandboxStatus.RUNNING: - logger.info('Sandbox is running!') - break - elif info and info.status == SandboxStatus.ERROR: - logger.error('Sandbox failed to start') - break - logger.info(f'Waiting for sandbox to start... ({i+1}/{max_wait})') - await asyncio.sleep(2) - - # Get available tools - logger.info('=== Getting Available Tools ===') - try: - tools = await manager.get_sandbox_tools(sandbox_id) - logger.info(f'Available tools: {tools}') - except Exception as e: - logger.error(f'Error getting tools: {e}') - - # Execute a tool (bash command) - logger.info('=== Executing Tool - Bash Command ===') - try: - result = await manager.execute_tool( - sandbox_id=sandbox_id, - tool_name='shell_executor', - parameters={ - 'command': "echo 'Hello from sandbox!' && python --version && pwd && ls -la" - } - ) - logger.info(f'Tool execution result:') - logger.info(f' Status: {result.status}') - logger.info(f' Output: {result.output}') - if result.error: - logger.info(f' Error: {result.error}') - except Exception as e: - logger.error(f'Error executing tool: {e}') - - # Execute Python code - logger.info('=== Executing Tool - Python Code ===') - try: - python_code = """ -import sys -import os -print(f"Python version: {sys.version}") -print(f"Current directory: {os.getcwd()}") -print(f"Environment variables:") -for key, value in os.environ.items(): - if 'PYTHON' in key: - print(f" {key}: {value}") -""" - - result = await manager.execute_tool( - sandbox_id=sandbox_id, - tool_name='python_executor', - parameters={'code': python_code} - ) - logger.info(f'Python execution result:') - logger.info(f' Status: {result.status}') - logger.info(f' Output: {result.output}') - if result.error: - logger.info(f' Error: {result.error}') - except Exception as e: - logger.error(f'Error executing Python code: {e}') - - # List sandboxes with status filter - logger.info('=== Listing Running Sandboxes ===') - running_sandboxes = await manager.list_sandboxes(status_filter=SandboxStatus.RUNNING) - logger.info(f'Running sandboxes: {len(running_sandboxes)}') - - # Clean up - delete the sandbox - logger.info('=== Cleaning Up ===') - success = await manager.delete_sandbox(sandbox_id) - if success: - logger.info(f'Successfully deleted sandbox {sandbox_id}') - else: - logger.error(f'Failed to delete sandbox {sandbox_id}') - - # Final stats - logger.info('=== Final Server Stats ===') - final_stats = await manager.get_stats() - logger.info(f'Final server stats: {final_stats}') - - except Exception as e: - logger.error(f'Error in demonstration: {e}') - finally: - # Stop the manager - await manager.stop() - logger.info('HTTP manager stopped') - - -async def main(): - """Main example function.""" - logger.info('=== Sandbox Server and HTTP Manager Example ===') - - # Start server in background thread - server_thread = threading.Thread(target=start_server_in_thread, daemon=True) - server_thread.start() - - # Wait for server to be ready - base_url = 'http://127.0.0.1:8000' - if not await wait_for_server(base_url): - logger.error('Server failed to start within timeout') - return - - # Demonstrate HTTP manager functionality - await demonstrate_http_manager() - - logger.info('=== Example Complete ===') - -if __name__ == '__main__': - try: - asyncio.run(main()) - except KeyboardInterrupt: - logger.info('Example interrupted by user') - except Exception as e: - logger.error(f'Example failed: {e}') - raise diff --git a/ms_enclave/sandbox/manager/base.py b/ms_enclave/sandbox/manager/base.py index 42eaff2..39fe3a2 100644 --- a/ms_enclave/sandbox/manager/base.py +++ b/ms_enclave/sandbox/manager/base.py @@ -3,7 +3,7 @@ import asyncio from abc import ABC, abstractmethod from collections import deque -from typing import TYPE_CHECKING, Any, Deque, Dict, List, Optional, Union +from typing import TYPE_CHECKING, Any, Callable, Deque, Dict, List, Optional, Union from ..model import ( SandboxConfig, @@ -235,7 +235,7 @@ def create_manager( cls, manager_type: Optional[SandboxManagerType] = None, config: Optional[SandboxManagerConfig] = None, - **kwargs + **kwargs: Any ) -> SandboxManager: """Create a sandbox manager instance. @@ -276,7 +276,7 @@ def get_registered_types(cls) -> List[SandboxManagerType]: return list(cls._registry.keys()) -def register_manager(manager_type: SandboxManagerType): +def register_manager(manager_type: SandboxManagerType) -> Callable[..., type]: """Decorator to register a sandbox manager class. Args: diff --git a/pyproject.toml b/pyproject.toml index 5a9b951..000c1fe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,3 +54,8 @@ docker = [ "docker>=7.1.0", "websocket-client" ] + +docs = [ + "mkdocs-material", + "mkdocstrings-python" +]