ms-enclave 0.0.0__py3-none-any.whl → 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of ms-enclave might be problematic. Click here for more details.
- ms_enclave/__init__.py +2 -2
- ms_enclave/cli/__init__.py +1 -0
- ms_enclave/cli/base.py +20 -0
- ms_enclave/cli/cli.py +27 -0
- ms_enclave/cli/start_server.py +84 -0
- ms_enclave/sandbox/__init__.py +27 -0
- ms_enclave/sandbox/boxes/__init__.py +16 -0
- ms_enclave/sandbox/boxes/base.py +267 -0
- ms_enclave/sandbox/boxes/docker_notebook.py +216 -0
- ms_enclave/sandbox/boxes/docker_sandbox.py +252 -0
- ms_enclave/sandbox/manager/__init__.py +11 -0
- ms_enclave/sandbox/manager/base.py +155 -0
- ms_enclave/sandbox/manager/http_manager.py +405 -0
- ms_enclave/sandbox/manager/local_manager.py +295 -0
- ms_enclave/sandbox/model/__init__.py +21 -0
- ms_enclave/sandbox/model/base.py +36 -0
- ms_enclave/sandbox/model/config.py +97 -0
- ms_enclave/sandbox/model/requests.py +57 -0
- ms_enclave/sandbox/model/responses.py +57 -0
- ms_enclave/sandbox/server/__init__.py +0 -0
- ms_enclave/sandbox/server/server.py +195 -0
- ms_enclave/sandbox/tools/__init__.py +4 -0
- ms_enclave/sandbox/tools/base.py +95 -0
- ms_enclave/sandbox/tools/sandbox_tool.py +46 -0
- ms_enclave/sandbox/tools/sandbox_tools/__init__.py +4 -0
- ms_enclave/sandbox/tools/sandbox_tools/file_operation.py +215 -0
- ms_enclave/sandbox/tools/sandbox_tools/notebook_executor.py +167 -0
- ms_enclave/sandbox/tools/sandbox_tools/python_executor.py +87 -0
- ms_enclave/sandbox/tools/sandbox_tools/shell_executor.py +63 -0
- ms_enclave/sandbox/tools/tool_info.py +141 -0
- ms_enclave/utils/__init__.py +1 -0
- ms_enclave/utils/json_schema.py +208 -0
- ms_enclave/utils/logger.py +106 -0
- ms_enclave/version.py +2 -2
- ms_enclave-0.0.1.dist-info/METADATA +314 -0
- ms_enclave-0.0.1.dist-info/RECORD +40 -0
- {ms_enclave-0.0.0.dist-info → ms_enclave-0.0.1.dist-info}/WHEEL +1 -1
- ms_enclave-0.0.1.dist-info/entry_points.txt +2 -0
- ms_enclave/run_server.py +0 -21
- ms_enclave-0.0.0.dist-info/METADATA +0 -329
- ms_enclave-0.0.0.dist-info/RECORD +0 -8
- {ms_enclave-0.0.0.dist-info → ms_enclave-0.0.1.dist-info}/licenses/LICENSE +0 -0
- {ms_enclave-0.0.0.dist-info → ms_enclave-0.0.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Data models for sandbox system."""
|
|
2
|
+
|
|
3
|
+
from .base import ExecutionStatus, SandboxStatus, SandboxType, ToolType
|
|
4
|
+
from .config import (
|
|
5
|
+
DockerNotebookConfig,
|
|
6
|
+
DockerSandboxConfig,
|
|
7
|
+
FileOperationConfig,
|
|
8
|
+
PythonExecutorConfig,
|
|
9
|
+
SandboxConfig,
|
|
10
|
+
ShellExecutorConfig,
|
|
11
|
+
ToolConfig,
|
|
12
|
+
)
|
|
13
|
+
from .requests import (
|
|
14
|
+
ExecuteCodeRequest,
|
|
15
|
+
ExecuteCommandRequest,
|
|
16
|
+
FileOperationRequest,
|
|
17
|
+
ReadFileRequest,
|
|
18
|
+
ToolExecutionRequest,
|
|
19
|
+
WriteFileRequest,
|
|
20
|
+
)
|
|
21
|
+
from .responses import CommandResult, HealthCheckResult, SandboxInfo, ToolResult
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Base data models."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SandboxStatus(str, Enum):
|
|
7
|
+
"""Sandbox status enumeration."""
|
|
8
|
+
|
|
9
|
+
INITIALIZING = 'initializing'
|
|
10
|
+
RUNNING = 'running'
|
|
11
|
+
STOPPING = 'stopping'
|
|
12
|
+
STOPPED = 'stopped'
|
|
13
|
+
ERROR = 'error'
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SandboxType(str, Enum):
|
|
17
|
+
"""Sandbox type enumeration."""
|
|
18
|
+
DOCKER = 'docker'
|
|
19
|
+
DOCKER_NOTEBOOK = 'docker_notebook'
|
|
20
|
+
DUMMY = 'dummy'
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ToolType(str, Enum):
|
|
24
|
+
"""Tool type enumeration."""
|
|
25
|
+
SANDBOX = 'sandbox'
|
|
26
|
+
FUNCTION = 'function'
|
|
27
|
+
EXTERNAL = 'external'
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ExecutionStatus(str, Enum):
|
|
31
|
+
"""Execution status enumeration."""
|
|
32
|
+
|
|
33
|
+
SUCCESS = 'success'
|
|
34
|
+
ERROR = 'error'
|
|
35
|
+
TIMEOUT = 'timeout'
|
|
36
|
+
CANCELLED = 'cancelled'
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Configuration data models."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List, Optional, Union
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field, field_validator
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SandboxConfig(BaseModel):
|
|
9
|
+
"""Base sandbox configuration."""
|
|
10
|
+
|
|
11
|
+
timeout: int = Field(default=30, description='Default timeout in seconds')
|
|
12
|
+
tools_config: Dict[str, Dict[
|
|
13
|
+
str, Any]] = Field(default_factory=dict, description='Configuration for tools within the sandbox')
|
|
14
|
+
working_dir: str = Field(default='/sandbox', description='Default working directory')
|
|
15
|
+
env_vars: Dict[str, str] = Field(default_factory=dict, description='Environment variables')
|
|
16
|
+
resource_limits: Dict[str, Any] = Field(default_factory=dict, description='Resource limits')
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DockerSandboxConfig(SandboxConfig):
|
|
20
|
+
"""Docker-specific sandbox configuration."""
|
|
21
|
+
|
|
22
|
+
image: str = Field('python:3.11-slim', description='Docker image name')
|
|
23
|
+
command: Optional[Union[str, List[str]]] = Field(None, description='Container command')
|
|
24
|
+
volumes: Dict[str, Dict[str, str]] = Field(
|
|
25
|
+
default_factory=dict,
|
|
26
|
+
description="Volume mounts. Format: { host_path: {'bind': container_path, 'mode': 'rw|ro'} }"
|
|
27
|
+
)
|
|
28
|
+
ports: Dict[str, str] = Field(default_factory=dict, description='Port mappings')
|
|
29
|
+
network: Optional[str] = Field('bridge', description='Network name')
|
|
30
|
+
memory_limit: str = Field(default='1g', description='Memory limit')
|
|
31
|
+
cpu_limit: float = Field(default=1.0, description='CPU limit')
|
|
32
|
+
network_enabled: bool = Field(default=True, description='Enable network access')
|
|
33
|
+
privileged: bool = Field(default=False, description='Run in privileged mode')
|
|
34
|
+
remove_on_exit: bool = Field(default=True, description='Remove container on exit')
|
|
35
|
+
|
|
36
|
+
@field_validator('memory_limit')
|
|
37
|
+
def validate_memory_limit(cls, v):
|
|
38
|
+
"""Validate memory limit format."""
|
|
39
|
+
if not isinstance(v, str):
|
|
40
|
+
raise ValueError('Memory limit must be a string')
|
|
41
|
+
# Basic validation for memory format (e.g., '512m', '1g', '2G')
|
|
42
|
+
import re
|
|
43
|
+
if not re.match(r'^\d+[kmgKMG]?$', v):
|
|
44
|
+
raise ValueError('Invalid memory limit format')
|
|
45
|
+
return v
|
|
46
|
+
|
|
47
|
+
@field_validator('cpu_limit')
|
|
48
|
+
def validate_cpu_limit(cls, v):
|
|
49
|
+
"""Validate CPU limit."""
|
|
50
|
+
if v <= 0:
|
|
51
|
+
raise ValueError('CPU limit must be positive')
|
|
52
|
+
return v
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class DockerNotebookConfig(DockerSandboxConfig):
|
|
56
|
+
"""Docker Notebook-specific sandbox configuration."""
|
|
57
|
+
|
|
58
|
+
image: str = Field('jupyter-kernel-gateway', description='Docker image name for Jupyter Notebook')
|
|
59
|
+
host: str = Field('127.0.0.1', description='Host for Jupyter Notebook')
|
|
60
|
+
port: int = Field(8888, description='Port for Jupyter Notebook')
|
|
61
|
+
token: Optional[str] = Field(None, description='Token for Jupyter Notebook access')
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class ToolConfig(BaseModel):
|
|
65
|
+
"""Tool configuration."""
|
|
66
|
+
|
|
67
|
+
enabled: bool = Field(default=True, description='Whether tool is enabled')
|
|
68
|
+
timeout: int = Field(default=30, description='Tool execution timeout')
|
|
69
|
+
parameters: Dict[str, Any] = Field(default_factory=dict, description='Tool parameters')
|
|
70
|
+
restrictions: Dict[str, Any] = Field(default_factory=dict, description='Tool restrictions')
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class PythonExecutorConfig(ToolConfig):
|
|
74
|
+
"""Python executor configuration."""
|
|
75
|
+
|
|
76
|
+
python_path: str = Field(default='python3', description='Python executable path')
|
|
77
|
+
allowed_modules: Optional[List[str]] = Field(None, description='Allowed modules (None = all)')
|
|
78
|
+
blocked_modules: List[str] = Field(default_factory=list, description='Blocked modules')
|
|
79
|
+
max_output_size: int = Field(default=1024 * 1024, description='Maximum output size in bytes')
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class ShellExecutorConfig(ToolConfig):
|
|
83
|
+
"""Shell executor configuration."""
|
|
84
|
+
|
|
85
|
+
shell_path: str = Field(default='/bin/bash', description='Shell executable path')
|
|
86
|
+
allowed_commands: Optional[List[str]] = Field(None, description='Allowed commands (None = all)')
|
|
87
|
+
blocked_commands: List[str] = Field(default_factory=list, description='Blocked commands')
|
|
88
|
+
max_output_size: int = Field(default=1024 * 1024, description='Maximum output size in bytes')
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class FileOperationConfig(ToolConfig):
|
|
92
|
+
"""File operation configuration."""
|
|
93
|
+
|
|
94
|
+
allowed_paths: Optional[List[str]] = Field(None, description='Allowed paths (None = all)')
|
|
95
|
+
blocked_paths: List[str] = Field(default_factory=list, description='Blocked paths')
|
|
96
|
+
max_file_size: int = Field(default=10 * 1024 * 1024, description='Maximum file size in bytes')
|
|
97
|
+
allowed_extensions: Optional[List[str]] = Field(None, description='Allowed file extensions')
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Request data models."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List, Optional, Union
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ExecuteCodeRequest(BaseModel):
|
|
9
|
+
"""Request model for code execution."""
|
|
10
|
+
|
|
11
|
+
code: str = Field(..., description='Code to execute')
|
|
12
|
+
language: str = Field(default='python', description='Programming language')
|
|
13
|
+
timeout: Optional[int] = Field(None, description='Execution timeout in seconds')
|
|
14
|
+
working_dir: Optional[str] = Field(None, description='Working directory')
|
|
15
|
+
env: Dict[str, str] = Field(default_factory=dict, description='Environment variables')
|
|
16
|
+
capture_output: bool = Field(True, description='Whether to capture stdout/stderr')
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ExecuteCommandRequest(BaseModel):
|
|
20
|
+
"""Request model for shell command execution."""
|
|
21
|
+
|
|
22
|
+
command: Union[str, List[str]] = Field(..., description='Command to execute')
|
|
23
|
+
timeout: Optional[int] = Field(None, description='Execution timeout in seconds')
|
|
24
|
+
working_dir: Optional[str] = Field(None, description='Working directory')
|
|
25
|
+
env: Dict[str, str] = Field(default_factory=dict, description='Environment variables')
|
|
26
|
+
shell: bool = Field(True, description='Execute in shell')
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class FileOperationRequest(BaseModel):
|
|
30
|
+
"""Base request model for file operations."""
|
|
31
|
+
|
|
32
|
+
path: str = Field(..., description='File path')
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ReadFileRequest(FileOperationRequest):
|
|
36
|
+
"""Request model for reading files."""
|
|
37
|
+
|
|
38
|
+
encoding: Optional[str] = Field('utf-8', description='File encoding')
|
|
39
|
+
binary: bool = Field(False, description='Read as binary')
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class WriteFileRequest(FileOperationRequest):
|
|
43
|
+
"""Request model for writing files."""
|
|
44
|
+
|
|
45
|
+
content: Union[str, bytes] = Field(..., description='File content')
|
|
46
|
+
encoding: Optional[str] = Field('utf-8', description='File encoding')
|
|
47
|
+
binary: bool = Field(False, description='Write as binary')
|
|
48
|
+
create_dirs: bool = Field(True, description='Create parent directories if needed')
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ToolExecutionRequest(BaseModel):
|
|
52
|
+
"""Request model for tool execution."""
|
|
53
|
+
|
|
54
|
+
sandbox_id: str = Field(..., description='Sandbox identifier')
|
|
55
|
+
tool_name: str = Field(..., description='Name of tool to execute')
|
|
56
|
+
parameters: Dict[str, Any] = Field(default_factory=dict, description='Tool parameters')
|
|
57
|
+
timeout: Optional[int] = Field(None, description='Execution timeout in seconds')
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Response data models."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Any, Dict, List, Optional, Union
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
from .base import ExecutionStatus, SandboxStatus
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SandboxInfo(BaseModel):
|
|
12
|
+
"""Information about a sandbox instance."""
|
|
13
|
+
|
|
14
|
+
id: str = Field(..., description='Sandbox identifier')
|
|
15
|
+
status: SandboxStatus = Field(..., description='Current status')
|
|
16
|
+
type: str = Field(..., description="Sandbox type (e.g., 'docker')")
|
|
17
|
+
config: Dict[str, Any] = Field(default_factory=dict, description='Sandbox configuration')
|
|
18
|
+
created_at: datetime = Field(default_factory=datetime.now, description='Creation timestamp')
|
|
19
|
+
updated_at: datetime = Field(default_factory=datetime.now, description='Last update timestamp')
|
|
20
|
+
metadata: Dict[str, Any] = Field(default_factory=dict, description='Additional metadata')
|
|
21
|
+
available_tools: Dict[str, Any] = Field(default_factory=dict, description='Available tools')
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ExecutionResult(BaseModel):
|
|
25
|
+
"""Base class for execution results."""
|
|
26
|
+
|
|
27
|
+
status: ExecutionStatus = Field(..., description='Execution status')
|
|
28
|
+
execution_time: Optional[float] = Field(None, description='Execution time in seconds')
|
|
29
|
+
timestamp: datetime = Field(default_factory=datetime.now, description='Execution timestamp')
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ToolResult(ExecutionResult):
|
|
33
|
+
"""Result of tool execution."""
|
|
34
|
+
|
|
35
|
+
tool_name: str = Field(..., description='Name of tool executed')
|
|
36
|
+
output: Any = Field(None, description='Tool execution result')
|
|
37
|
+
metadata: Dict[str, Any] = Field(default_factory=dict, description='Additional metadata')
|
|
38
|
+
error: Optional[str] = Field(None, description='Error message if failed')
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class CommandResult(ExecutionResult):
|
|
42
|
+
"""Command execution result."""
|
|
43
|
+
|
|
44
|
+
command: Union[str, List[str]] = Field(..., description='Executed command')
|
|
45
|
+
exit_code: int = Field(..., description='Exit code of the command')
|
|
46
|
+
stdout: Optional[str] = Field(None, description='Standard output')
|
|
47
|
+
stderr: Optional[str] = Field(None, description='Standard error output')
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class HealthCheckResult(BaseModel):
|
|
51
|
+
"""Health check result."""
|
|
52
|
+
|
|
53
|
+
healthy: bool = Field(..., description='Health status')
|
|
54
|
+
version: str = Field(..., description='System version')
|
|
55
|
+
uptime: float = Field(..., description='System uptime in seconds')
|
|
56
|
+
active_sandboxes: int = Field(..., description='Number of active sandboxes')
|
|
57
|
+
system_info: Dict[str, Any] = Field(default_factory=dict, description='System information')
|
|
File without changes
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""FastAPI server for sandbox system with optional API key authentication."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from contextlib import asynccontextmanager
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
from fastapi import BackgroundTasks, Depends, FastAPI, HTTPException, Request
|
|
8
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
9
|
+
from fastapi.responses import JSONResponse
|
|
10
|
+
|
|
11
|
+
from ..manager import LocalSandboxManager
|
|
12
|
+
from ..model import (
|
|
13
|
+
DockerSandboxConfig,
|
|
14
|
+
HealthCheckResult,
|
|
15
|
+
SandboxConfig,
|
|
16
|
+
SandboxInfo,
|
|
17
|
+
SandboxStatus,
|
|
18
|
+
SandboxType,
|
|
19
|
+
ToolExecutionRequest,
|
|
20
|
+
ToolResult,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SandboxServer:
|
|
25
|
+
"""FastAPI-based sandbox server.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, cleanup_interval: int = 300, api_key: Optional[str] = None):
|
|
29
|
+
"""Initialize sandbox server.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
cleanup_interval: Cleanup interval in seconds
|
|
33
|
+
api_key: Optional API key to protect endpoints. If None, auth is disabled.
|
|
34
|
+
"""
|
|
35
|
+
self.manager = LocalSandboxManager(cleanup_interval)
|
|
36
|
+
self.api_key: Optional[str] = api_key
|
|
37
|
+
self.app = FastAPI(
|
|
38
|
+
title='Sandbox API',
|
|
39
|
+
description='Agent sandbox execution environment',
|
|
40
|
+
version='1.0.0',
|
|
41
|
+
lifespan=self.lifespan
|
|
42
|
+
)
|
|
43
|
+
self._setup_middleware()
|
|
44
|
+
self._setup_auth_middleware()
|
|
45
|
+
self._setup_routes()
|
|
46
|
+
self.start_time = time.time()
|
|
47
|
+
|
|
48
|
+
@asynccontextmanager
|
|
49
|
+
async def lifespan(self, app: FastAPI):
|
|
50
|
+
"""Application lifespan management."""
|
|
51
|
+
# Startup
|
|
52
|
+
await self.manager.start()
|
|
53
|
+
yield
|
|
54
|
+
# Shutdown
|
|
55
|
+
await self.manager.stop()
|
|
56
|
+
|
|
57
|
+
def _setup_middleware(self):
|
|
58
|
+
"""Setup middleware."""
|
|
59
|
+
self.app.add_middleware(
|
|
60
|
+
CORSMiddleware,
|
|
61
|
+
allow_origins=['*'],
|
|
62
|
+
allow_credentials=True,
|
|
63
|
+
allow_methods=['*'],
|
|
64
|
+
allow_headers=['*'],
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def _setup_auth_middleware(self) -> None:
|
|
68
|
+
"""Setup optional API key authentication middleware.
|
|
69
|
+
|
|
70
|
+
When ``self.api_key`` is None, this middleware is a no-op.
|
|
71
|
+
Otherwise, it enforces that every request includes the correct API key
|
|
72
|
+
either via ``X-API-Key`` header or ``api_key`` query parameter.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
@self.app.middleware('http')
|
|
76
|
+
async def auth_middleware(request: Request, call_next): # type: ignore[unused-ignore]
|
|
77
|
+
# Fast path when auth is disabled
|
|
78
|
+
if not self.api_key:
|
|
79
|
+
return await call_next(request)
|
|
80
|
+
|
|
81
|
+
provided = request.headers.get('x-api-key') or request.query_params.get('api_key')
|
|
82
|
+
if provided == self.api_key:
|
|
83
|
+
return await call_next(request)
|
|
84
|
+
|
|
85
|
+
return JSONResponse(status_code=401, content={'detail': 'Unauthorized'})
|
|
86
|
+
|
|
87
|
+
def _setup_routes(self):
|
|
88
|
+
"""Setup API routes."""
|
|
89
|
+
|
|
90
|
+
# Health check
|
|
91
|
+
@self.app.get('/health', response_model=HealthCheckResult)
|
|
92
|
+
async def health_check():
|
|
93
|
+
"""Health check endpoint."""
|
|
94
|
+
stats = await self.manager.get_stats()
|
|
95
|
+
return HealthCheckResult(
|
|
96
|
+
healthy=True,
|
|
97
|
+
version='1.0.0',
|
|
98
|
+
uptime=time.time() - self.start_time,
|
|
99
|
+
active_sandboxes=stats['total_sandboxes'],
|
|
100
|
+
system_info=stats
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Sandbox management
|
|
104
|
+
@self.app.post('/sandbox/create')
|
|
105
|
+
async def create_sandbox(sandbox_type: SandboxType, config: Optional[Dict] = None):
|
|
106
|
+
"""Create a new sandbox."""
|
|
107
|
+
try:
|
|
108
|
+
sandbox_id = await self.manager.create_sandbox(sandbox_type, config)
|
|
109
|
+
|
|
110
|
+
return {'sandbox_id': sandbox_id}
|
|
111
|
+
|
|
112
|
+
except Exception as e:
|
|
113
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
114
|
+
|
|
115
|
+
@self.app.get('/sandboxes')
|
|
116
|
+
async def list_sandboxes(status: Optional[SandboxStatus] = None):
|
|
117
|
+
"""List all sandboxes."""
|
|
118
|
+
sandboxes = await self.manager.list_sandboxes(status)
|
|
119
|
+
return sandboxes
|
|
120
|
+
|
|
121
|
+
@self.app.get('/sandbox/{sandbox_id}', response_model=SandboxInfo)
|
|
122
|
+
async def get_sandbox_info(sandbox_id: str):
|
|
123
|
+
"""Get sandbox information."""
|
|
124
|
+
info = await self.manager.get_sandbox_info(sandbox_id)
|
|
125
|
+
if not info:
|
|
126
|
+
raise HTTPException(status_code=404, detail='Sandbox not found')
|
|
127
|
+
return info
|
|
128
|
+
|
|
129
|
+
@self.app.post('/sandbox/{sandbox_id}/stop')
|
|
130
|
+
async def stop_sandbox(sandbox_id: str):
|
|
131
|
+
"""Stop a sandbox."""
|
|
132
|
+
success = await self.manager.stop_sandbox(sandbox_id)
|
|
133
|
+
if not success:
|
|
134
|
+
raise HTTPException(status_code=404, detail='Sandbox not found')
|
|
135
|
+
return {'message': 'Sandbox stopped successfully'}
|
|
136
|
+
|
|
137
|
+
@self.app.delete('/sandbox/{sandbox_id}')
|
|
138
|
+
async def delete_sandbox(sandbox_id: str):
|
|
139
|
+
"""Delete a sandbox."""
|
|
140
|
+
success = await self.manager.delete_sandbox(sandbox_id)
|
|
141
|
+
if not success:
|
|
142
|
+
raise HTTPException(status_code=404, detail='Sandbox not found')
|
|
143
|
+
return {'message': 'Sandbox deleted successfully'}
|
|
144
|
+
|
|
145
|
+
# Tool execution
|
|
146
|
+
@self.app.post('/sandbox/tool/execute', response_model=ToolResult)
|
|
147
|
+
async def execute_tool(request: ToolExecutionRequest):
|
|
148
|
+
"""Execute tool in sandbox."""
|
|
149
|
+
try:
|
|
150
|
+
result = await self.manager.execute_tool(request.sandbox_id, request.tool_name, request.parameters)
|
|
151
|
+
return result
|
|
152
|
+
except ValueError as e:
|
|
153
|
+
raise HTTPException(status_code=404, detail=str(e))
|
|
154
|
+
except Exception as e:
|
|
155
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
156
|
+
|
|
157
|
+
@self.app.get('/sandbox/{sandbox_id}/tools')
|
|
158
|
+
async def get_sandbox_tools(sandbox_id: str):
|
|
159
|
+
"""Get available tools for a sandbox."""
|
|
160
|
+
try:
|
|
161
|
+
tools = await self.manager.get_sandbox_tools(sandbox_id)
|
|
162
|
+
return tools
|
|
163
|
+
except ValueError as e:
|
|
164
|
+
raise HTTPException(status_code=404, detail=str(e))
|
|
165
|
+
|
|
166
|
+
# System info
|
|
167
|
+
@self.app.get('/stats')
|
|
168
|
+
async def get_stats():
|
|
169
|
+
"""Get system statistics."""
|
|
170
|
+
return await self.manager.get_stats()
|
|
171
|
+
|
|
172
|
+
def run(self, host: str = '0.0.0.0', port: int = 8000, **kwargs):
|
|
173
|
+
"""Run the server.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
host: Host to bind to
|
|
177
|
+
port: Port to bind to
|
|
178
|
+
**kwargs: Additional uvicorn arguments
|
|
179
|
+
"""
|
|
180
|
+
import uvicorn
|
|
181
|
+
uvicorn.run(self.app, host=host, port=port, **kwargs)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# Create a default server instance
|
|
185
|
+
def create_server(cleanup_interval: int = 300, api_key: Optional[str] = None) -> SandboxServer:
|
|
186
|
+
"""Create a sandbox server instance.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
cleanup_interval: Cleanup interval in seconds
|
|
190
|
+
api_key: Optional API key to protect endpoints. If None, auth is disabled.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
Sandbox server instance
|
|
194
|
+
"""
|
|
195
|
+
return SandboxServer(cleanup_interval, api_key)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Base tool interface and factory."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Dict, Optional, Type
|
|
5
|
+
|
|
6
|
+
from ..model import SandboxType, ToolResult
|
|
7
|
+
from .tool_info import ToolParams
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from ms_enclave.sandbox.boxes import Sandbox
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Tool(ABC):
|
|
14
|
+
"""Abstract base class for all tools."""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
*,
|
|
19
|
+
name: Optional[str] = None,
|
|
20
|
+
description: Optional[str] = None,
|
|
21
|
+
parameters: Optional[ToolParams] = None,
|
|
22
|
+
enabled: bool = True,
|
|
23
|
+
timeout: Optional[int] = None,
|
|
24
|
+
**kwargs,
|
|
25
|
+
):
|
|
26
|
+
self._name = name or self.__class__.__name__
|
|
27
|
+
self._description = description
|
|
28
|
+
self._parameters = parameters
|
|
29
|
+
self.enabled = enabled
|
|
30
|
+
self.timeout = timeout
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def name(self) -> str:
|
|
34
|
+
return self._name
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def description(self) -> Optional[str]:
|
|
38
|
+
return self._description
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def parameters(self) -> Optional[ToolParams]:
|
|
42
|
+
return self._parameters
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def schema(self) -> Dict:
|
|
46
|
+
return {
|
|
47
|
+
'type': 'function',
|
|
48
|
+
'function': {
|
|
49
|
+
'name': self.name,
|
|
50
|
+
'description': self._description,
|
|
51
|
+
'parameters': self._parameters.model_dump(exclude_none=True) if self._parameters else {},
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
@abstractmethod
|
|
57
|
+
def required_sandbox_type(self) -> Optional[SandboxType]:
|
|
58
|
+
"""Get the required sandbox type for this tool."""
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
@abstractmethod
|
|
62
|
+
async def execute(self, sandbox_context: 'Sandbox', **kwargs) -> ToolResult:
|
|
63
|
+
"""Execute the tool with given sandbox context and parameters."""
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class ToolFactory:
|
|
68
|
+
"""Factory for creating tool instances."""
|
|
69
|
+
|
|
70
|
+
_tools: Dict[str, Type[Tool]] = {}
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def register_tool(cls, tool_name: str, tool_class: Type[Tool]):
|
|
74
|
+
cls._tools[tool_name] = tool_class
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def create_tool(cls, tool_name: str, **kwargs) -> Tool:
|
|
78
|
+
if tool_name not in cls._tools:
|
|
79
|
+
raise ValueError(f'Tool name {tool_name} is not registered')
|
|
80
|
+
|
|
81
|
+
tool_class = cls._tools[tool_name]
|
|
82
|
+
return tool_class(**kwargs)
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def get_available_tools(cls) -> list[str]:
|
|
86
|
+
return list(cls._tools.keys())
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def register_tool(tool_name: str):
|
|
90
|
+
|
|
91
|
+
def decorator(tool_class: Type[Tool]):
|
|
92
|
+
ToolFactory.register_tool(tool_name, tool_class)
|
|
93
|
+
return tool_class
|
|
94
|
+
|
|
95
|
+
return decorator
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from typing import Any, Dict, Optional
|
|
2
|
+
|
|
3
|
+
from ..model import SandboxType
|
|
4
|
+
from .base import Tool
|
|
5
|
+
from .tool_info import ToolParams
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SandboxTool(Tool):
|
|
9
|
+
"""Built-in tool class"""
|
|
10
|
+
|
|
11
|
+
_name: Optional[str] = None
|
|
12
|
+
_description: Optional[str] = None
|
|
13
|
+
_parameters: Optional[ToolParams] = None
|
|
14
|
+
_sandbox_type: Optional[SandboxType] = None
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
*,
|
|
19
|
+
name: Optional[str] = None,
|
|
20
|
+
description: Optional[str] = None,
|
|
21
|
+
parameters: Optional[ToolParams] = None,
|
|
22
|
+
sandbox: Optional[Any] = None,
|
|
23
|
+
sandbox_type: Optional[SandboxType] = None,
|
|
24
|
+
**kwargs,
|
|
25
|
+
):
|
|
26
|
+
"""
|
|
27
|
+
Initialize the tool.
|
|
28
|
+
|
|
29
|
+
Note: Once the sandbox is set, it does not change.
|
|
30
|
+
"""
|
|
31
|
+
super().__init__(
|
|
32
|
+
name=name,
|
|
33
|
+
description=description,
|
|
34
|
+
parameters=parameters,
|
|
35
|
+
**kwargs,
|
|
36
|
+
)
|
|
37
|
+
self._sandbox = sandbox
|
|
38
|
+
self._name = name or self.__class__._name
|
|
39
|
+
self._description = description or self.__class__._description
|
|
40
|
+
self._parameters = parameters or self.__class__._parameters
|
|
41
|
+
self._sandbox_type = sandbox_type or self.__class__._sandbox_type
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def required_sandbox_type(self) -> Optional[SandboxType]:
|
|
45
|
+
"""Get the required sandbox type for this tool."""
|
|
46
|
+
return self._sandbox_type
|