ms-enclave 0.0.3__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.
- ms_enclave/__init__.py +3 -0
- 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 +274 -0
- ms_enclave/sandbox/boxes/docker_notebook.py +224 -0
- ms_enclave/sandbox/boxes/docker_sandbox.py +317 -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 +75 -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 +119 -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 +331 -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 +72 -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 +170 -0
- ms_enclave/version.py +2 -0
- ms_enclave-0.0.3.dist-info/METADATA +370 -0
- ms_enclave-0.0.3.dist-info/RECORD +40 -0
- ms_enclave-0.0.3.dist-info/WHEEL +5 -0
- ms_enclave-0.0.3.dist-info/entry_points.txt +2 -0
- ms_enclave-0.0.3.dist-info/licenses/LICENSE +201 -0
- ms_enclave-0.0.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"""Docker-based sandbox implementation."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import time
|
|
5
|
+
from typing import Any, Dict, List, Optional, Union
|
|
6
|
+
|
|
7
|
+
import docker
|
|
8
|
+
from docker import DockerClient
|
|
9
|
+
from docker.errors import APIError, ContainerError, ImageNotFound, NotFound
|
|
10
|
+
from docker.models.containers import Container
|
|
11
|
+
|
|
12
|
+
from ms_enclave.utils import get_logger
|
|
13
|
+
|
|
14
|
+
from ..model import CommandResult, DockerSandboxConfig, ExecutionStatus, SandboxStatus, SandboxType
|
|
15
|
+
from .base import Sandbox, register_sandbox
|
|
16
|
+
|
|
17
|
+
logger = get_logger()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@register_sandbox(SandboxType.DOCKER)
|
|
21
|
+
class DockerSandbox(Sandbox):
|
|
22
|
+
"""Docker-based sandbox implementation."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, config: DockerSandboxConfig, sandbox_id: Optional[str] = None):
|
|
25
|
+
"""Initialize Docker sandbox.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
config: Docker sandbox configuration
|
|
29
|
+
sandbox_id: Optional sandbox ID
|
|
30
|
+
"""
|
|
31
|
+
super().__init__(config, sandbox_id)
|
|
32
|
+
self.config: DockerSandboxConfig = config
|
|
33
|
+
self.client: Optional[DockerClient] = None
|
|
34
|
+
self.container: Optional[Container] = None
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def sandbox_type(self) -> SandboxType:
|
|
38
|
+
"""Return sandbox type."""
|
|
39
|
+
return SandboxType.DOCKER
|
|
40
|
+
|
|
41
|
+
async def start(self) -> None:
|
|
42
|
+
"""Start the Docker container."""
|
|
43
|
+
try:
|
|
44
|
+
self.update_status(SandboxStatus.INITIALIZING)
|
|
45
|
+
|
|
46
|
+
# Initialize Docker client
|
|
47
|
+
self.client = docker.from_env()
|
|
48
|
+
|
|
49
|
+
# Ensure image exists
|
|
50
|
+
await self._ensure_image_exists()
|
|
51
|
+
|
|
52
|
+
# Create and start container
|
|
53
|
+
await self._create_container()
|
|
54
|
+
await self._start_container()
|
|
55
|
+
|
|
56
|
+
# Initialize tools
|
|
57
|
+
await self.initialize_tools()
|
|
58
|
+
|
|
59
|
+
self.update_status(SandboxStatus.RUNNING)
|
|
60
|
+
|
|
61
|
+
except Exception as e:
|
|
62
|
+
self.update_status(SandboxStatus.ERROR)
|
|
63
|
+
self.metadata['error'] = str(e)
|
|
64
|
+
logger.error(f'Failed to start Docker sandbox: {e}')
|
|
65
|
+
raise RuntimeError(f'Failed to start Docker sandbox: {e}')
|
|
66
|
+
|
|
67
|
+
async def stop(self) -> None:
|
|
68
|
+
"""Stop the Docker container without removing it unless configured.
|
|
69
|
+
|
|
70
|
+
When remove_on_exit is False, this method stops the container but keeps
|
|
71
|
+
the container reference so get_execution_context() can return it.
|
|
72
|
+
"""
|
|
73
|
+
if not self.container:
|
|
74
|
+
self.update_status(SandboxStatus.STOPPED)
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
self.update_status(SandboxStatus.STOPPING)
|
|
79
|
+
await self.stop_container()
|
|
80
|
+
|
|
81
|
+
# If configured to remove on exit, perform full cleanup (removes container and closes client)
|
|
82
|
+
if self.config.remove_on_exit:
|
|
83
|
+
await self.cleanup()
|
|
84
|
+
|
|
85
|
+
self.update_status(SandboxStatus.STOPPED)
|
|
86
|
+
except Exception as e:
|
|
87
|
+
logger.error(f'Error stopping container: {e}')
|
|
88
|
+
self.update_status(SandboxStatus.ERROR)
|
|
89
|
+
raise
|
|
90
|
+
|
|
91
|
+
async def cleanup(self) -> None:
|
|
92
|
+
"""Clean up Docker resources.
|
|
93
|
+
|
|
94
|
+
- Always stops the container if it is running.
|
|
95
|
+
- Removes the container only when remove_on_exit is True.
|
|
96
|
+
- Preserves container reference and client when remove_on_exit is False.
|
|
97
|
+
"""
|
|
98
|
+
if self.container:
|
|
99
|
+
try:
|
|
100
|
+
self.container.remove(force=True)
|
|
101
|
+
logger.debug(f'Container {self.container.id} removed')
|
|
102
|
+
except Exception as e:
|
|
103
|
+
logger.error(f'Error cleaning up container: {e}')
|
|
104
|
+
finally:
|
|
105
|
+
# Only drop the reference when we actually removed it
|
|
106
|
+
self.container = None
|
|
107
|
+
|
|
108
|
+
# Close Docker client only if we dropped the container reference
|
|
109
|
+
if self.client:
|
|
110
|
+
try:
|
|
111
|
+
self.client.close()
|
|
112
|
+
except Exception as e:
|
|
113
|
+
logger.warning(f'Error closing Docker client: {e}')
|
|
114
|
+
finally:
|
|
115
|
+
self.client = None
|
|
116
|
+
|
|
117
|
+
async def stop_container(self) -> None:
|
|
118
|
+
"""Stop the container if it is running."""
|
|
119
|
+
if not self.container:
|
|
120
|
+
return
|
|
121
|
+
try:
|
|
122
|
+
self.container.reload()
|
|
123
|
+
if self.container.status == SandboxStatus.RUNNING:
|
|
124
|
+
self.container.stop(timeout=10)
|
|
125
|
+
except NotFound:
|
|
126
|
+
logger.warning('Container not found while stopping')
|
|
127
|
+
except Exception as e:
|
|
128
|
+
logger.error(f'Error stopping container: {e}')
|
|
129
|
+
raise
|
|
130
|
+
|
|
131
|
+
async def get_execution_context(self) -> Any:
|
|
132
|
+
"""Return the container for tool execution."""
|
|
133
|
+
return self.container
|
|
134
|
+
|
|
135
|
+
def _run_streaming(self, command: Union[str, List[str]]) -> tuple[int, str, str]:
|
|
136
|
+
"""Execute command with streaming logs using low-level API.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
A tuple of (exit_code, stdout, stderr)
|
|
140
|
+
"""
|
|
141
|
+
if not self.client or not self.container:
|
|
142
|
+
raise RuntimeError('Container is not running')
|
|
143
|
+
|
|
144
|
+
# Use low-level API for precise control over streaming and exit code.
|
|
145
|
+
exec_id = self.client.api.exec_create(
|
|
146
|
+
container=self.container.id,
|
|
147
|
+
cmd=command,
|
|
148
|
+
tty=False,
|
|
149
|
+
)['Id']
|
|
150
|
+
|
|
151
|
+
stdout_parts: List[str] = []
|
|
152
|
+
stderr_parts: List[str] = []
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
for chunk in self.client.api.exec_start(exec_id, stream=True, demux=True):
|
|
156
|
+
if not chunk:
|
|
157
|
+
continue
|
|
158
|
+
out, err = chunk # each is Optional[bytes]
|
|
159
|
+
if out:
|
|
160
|
+
text = out.decode('utf-8', errors='replace')
|
|
161
|
+
stdout_parts.append(text)
|
|
162
|
+
for line in text.splitlines():
|
|
163
|
+
logger.info(f'[📦 {self.id}] {line}')
|
|
164
|
+
if err:
|
|
165
|
+
text = err.decode('utf-8', errors='replace')
|
|
166
|
+
stderr_parts.append(text)
|
|
167
|
+
for line in text.splitlines():
|
|
168
|
+
logger.error(f'[📦 {self.id}] {line}')
|
|
169
|
+
finally:
|
|
170
|
+
inspect = self.client.api.exec_inspect(exec_id)
|
|
171
|
+
exit_code = inspect.get('ExitCode')
|
|
172
|
+
if exit_code is None:
|
|
173
|
+
exit_code = -1
|
|
174
|
+
|
|
175
|
+
return exit_code, ''.join(stdout_parts), ''.join(stderr_parts)
|
|
176
|
+
|
|
177
|
+
def _run_buffered(self, command: Union[str, List[str]]) -> tuple[int, str, str]:
|
|
178
|
+
"""Execute command and return buffered output using high-level API.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
A tuple of (exit_code, stdout, stderr)
|
|
182
|
+
"""
|
|
183
|
+
if not self.container:
|
|
184
|
+
raise RuntimeError('Container is not running')
|
|
185
|
+
|
|
186
|
+
res = self.container.exec_run(command, tty=False, stream=False, demux=True)
|
|
187
|
+
out_tuple = res.output
|
|
188
|
+
if isinstance(out_tuple, tuple):
|
|
189
|
+
out_bytes, err_bytes = out_tuple
|
|
190
|
+
else:
|
|
191
|
+
# Fallback: when demux was not honored, treat all as stdout
|
|
192
|
+
out_bytes, err_bytes = out_tuple, b''
|
|
193
|
+
|
|
194
|
+
stdout = out_bytes.decode('utf-8', errors='replace') if out_bytes else ''
|
|
195
|
+
stderr = err_bytes.decode('utf-8', errors='replace') if err_bytes else ''
|
|
196
|
+
return res.exit_code, stdout, stderr
|
|
197
|
+
|
|
198
|
+
async def execute_command(
|
|
199
|
+
self, command: Union[str, List[str]], timeout: Optional[int] = None, stream: bool = True
|
|
200
|
+
) -> CommandResult:
|
|
201
|
+
"""Execute a command in the container.
|
|
202
|
+
|
|
203
|
+
When stream=True (default), logs are printed in real-time through the logger,
|
|
204
|
+
while stdout/stderr are still accumulated and returned in the result.
|
|
205
|
+
When stream=False, the command is executed and buffered, returning the full output at once.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
command: Command to run (str or list)
|
|
209
|
+
timeout: Optional timeout in seconds
|
|
210
|
+
stream: Whether to stream logs in real time
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
CommandResult with status, exit_code, stdout and stderr
|
|
214
|
+
"""
|
|
215
|
+
if not self.container or not self.client:
|
|
216
|
+
raise RuntimeError('Container is not running')
|
|
217
|
+
|
|
218
|
+
loop = asyncio.get_running_loop()
|
|
219
|
+
|
|
220
|
+
run_func = self._run_streaming if stream else self._run_buffered
|
|
221
|
+
try:
|
|
222
|
+
exit_code, stdout, stderr = await asyncio.wait_for(
|
|
223
|
+
loop.run_in_executor(None, lambda: run_func(command)), timeout=timeout
|
|
224
|
+
)
|
|
225
|
+
status = ExecutionStatus.SUCCESS if exit_code == 0 else ExecutionStatus.ERROR
|
|
226
|
+
return CommandResult(command=command, status=status, exit_code=exit_code, stdout=stdout, stderr=stderr)
|
|
227
|
+
except asyncio.TimeoutError:
|
|
228
|
+
return CommandResult(
|
|
229
|
+
command=command,
|
|
230
|
+
status=ExecutionStatus.TIMEOUT,
|
|
231
|
+
exit_code=-1,
|
|
232
|
+
stdout='',
|
|
233
|
+
stderr=f'Command timed out after {timeout} seconds'
|
|
234
|
+
)
|
|
235
|
+
except Exception as e:
|
|
236
|
+
return CommandResult(command=command, status=ExecutionStatus.ERROR, exit_code=-1, stdout='', stderr=str(e))
|
|
237
|
+
|
|
238
|
+
async def _ensure_image_exists(self) -> None:
|
|
239
|
+
"""Ensure Docker image exists."""
|
|
240
|
+
try:
|
|
241
|
+
self.client.images.get(self.config.image)
|
|
242
|
+
except ImageNotFound:
|
|
243
|
+
# Try to pull the image
|
|
244
|
+
try:
|
|
245
|
+
self.client.images.pull(self.config.image)
|
|
246
|
+
except Exception as e:
|
|
247
|
+
raise RuntimeError(f'Failed to pull image {self.config.image}: {e}')
|
|
248
|
+
|
|
249
|
+
async def _create_container(self) -> None:
|
|
250
|
+
"""Create Docker container."""
|
|
251
|
+
try:
|
|
252
|
+
# Prepare container configuration
|
|
253
|
+
container_config = {
|
|
254
|
+
'image': self.config.image,
|
|
255
|
+
'name': f'sandbox-{self.id}',
|
|
256
|
+
'working_dir': self.config.working_dir,
|
|
257
|
+
'environment': self.config.env_vars,
|
|
258
|
+
'detach': True,
|
|
259
|
+
'tty': True,
|
|
260
|
+
'stdin_open': True,
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
# Add command if specified
|
|
264
|
+
if self.config.command:
|
|
265
|
+
container_config['command'] = self.config.command
|
|
266
|
+
|
|
267
|
+
# Add resource limits
|
|
268
|
+
if self.config.memory_limit:
|
|
269
|
+
container_config['mem_limit'] = self.config.memory_limit
|
|
270
|
+
|
|
271
|
+
if self.config.cpu_limit:
|
|
272
|
+
container_config['cpu_quota'] = int(self.config.cpu_limit * 100000)
|
|
273
|
+
container_config['cpu_period'] = 100000
|
|
274
|
+
|
|
275
|
+
# Add volumes
|
|
276
|
+
if self.config.volumes:
|
|
277
|
+
container_config['volumes'] = self.config.volumes
|
|
278
|
+
|
|
279
|
+
# Add ports
|
|
280
|
+
if self.config.ports:
|
|
281
|
+
container_config['ports'] = self.config.ports
|
|
282
|
+
|
|
283
|
+
# Network configuration
|
|
284
|
+
if not self.config.network_enabled:
|
|
285
|
+
container_config['network_mode'] = 'none'
|
|
286
|
+
elif self.config.network:
|
|
287
|
+
container_config['network'] = self.config.network
|
|
288
|
+
|
|
289
|
+
# Privileged mode
|
|
290
|
+
container_config['privileged'] = self.config.privileged
|
|
291
|
+
|
|
292
|
+
# Create container
|
|
293
|
+
self.container = self.client.containers.create(**container_config)
|
|
294
|
+
self.metadata['container_id'] = self.container.id
|
|
295
|
+
|
|
296
|
+
except Exception as e:
|
|
297
|
+
raise RuntimeError(f'Failed to create container: {e}')
|
|
298
|
+
|
|
299
|
+
async def _start_container(self) -> None:
|
|
300
|
+
"""Start Docker container."""
|
|
301
|
+
try:
|
|
302
|
+
self.container.start()
|
|
303
|
+
|
|
304
|
+
# Wait for container to be ready
|
|
305
|
+
timeout = 30
|
|
306
|
+
start_time = time.time()
|
|
307
|
+
|
|
308
|
+
while time.time() - start_time < timeout:
|
|
309
|
+
self.container.reload()
|
|
310
|
+
if self.container.status == 'running':
|
|
311
|
+
break
|
|
312
|
+
await asyncio.sleep(0.5)
|
|
313
|
+
else:
|
|
314
|
+
raise RuntimeError('Container failed to start within timeout')
|
|
315
|
+
|
|
316
|
+
except Exception as e:
|
|
317
|
+
raise RuntimeError(f'Failed to start container: {e}')
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Sandbox manager implementations."""
|
|
2
|
+
|
|
3
|
+
from .base import SandboxManager
|
|
4
|
+
from .http_manager import HttpSandboxManager
|
|
5
|
+
from .local_manager import LocalSandboxManager
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
'SandboxManager',
|
|
9
|
+
'LocalSandboxManager',
|
|
10
|
+
'HttpSandboxManager',
|
|
11
|
+
]
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Base sandbox manager interface."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
|
|
5
|
+
|
|
6
|
+
from ..model import SandboxConfig, SandboxInfo, SandboxStatus, SandboxType, ToolResult
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from ..boxes import Sandbox
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SandboxManager(ABC):
|
|
13
|
+
"""Abstract base class for sandbox managers."""
|
|
14
|
+
|
|
15
|
+
def __init__(self):
|
|
16
|
+
"""Initialize the sandbox manager."""
|
|
17
|
+
self._running = False
|
|
18
|
+
self._sandboxes: Dict[str, 'Sandbox'] = {}
|
|
19
|
+
|
|
20
|
+
@abstractmethod
|
|
21
|
+
async def start(self) -> None:
|
|
22
|
+
"""Start the sandbox manager."""
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
async def stop(self) -> None:
|
|
27
|
+
"""Stop the sandbox manager."""
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
@abstractmethod
|
|
31
|
+
async def create_sandbox(
|
|
32
|
+
self,
|
|
33
|
+
sandbox_type: SandboxType,
|
|
34
|
+
config: Optional[Union[SandboxConfig, Dict]] = None,
|
|
35
|
+
sandbox_id: Optional[str] = None
|
|
36
|
+
) -> str:
|
|
37
|
+
"""Create a new sandbox.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
sandbox_type: Type of sandbox to create
|
|
41
|
+
config: Sandbox configuration
|
|
42
|
+
sandbox_id: Optional sandbox ID
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Sandbox ID
|
|
46
|
+
|
|
47
|
+
Raises:
|
|
48
|
+
ValueError: If sandbox type is not supported
|
|
49
|
+
RuntimeError: If sandbox creation fails
|
|
50
|
+
"""
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
@abstractmethod
|
|
54
|
+
async def get_sandbox_info(self, sandbox_id: str) -> Optional[SandboxInfo]:
|
|
55
|
+
"""Get sandbox information.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
sandbox_id: Sandbox ID
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Sandbox information or None if not found
|
|
62
|
+
"""
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
@abstractmethod
|
|
66
|
+
async def list_sandboxes(self, status_filter: Optional[SandboxStatus] = None) -> List[SandboxInfo]:
|
|
67
|
+
"""List all sandboxes.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
status_filter: Optional status filter
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
List of sandbox information
|
|
74
|
+
"""
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
@abstractmethod
|
|
78
|
+
async def stop_sandbox(self, sandbox_id: str) -> bool:
|
|
79
|
+
"""Stop a sandbox.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
sandbox_id: Sandbox ID
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
True if stopped successfully, False if not found
|
|
86
|
+
"""
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
@abstractmethod
|
|
90
|
+
async def delete_sandbox(self, sandbox_id: str) -> bool:
|
|
91
|
+
"""Delete a sandbox.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
sandbox_id: Sandbox ID
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
True if deleted successfully, False if not found
|
|
98
|
+
"""
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
@abstractmethod
|
|
102
|
+
async def execute_tool(self, sandbox_id: str, tool_name: str, parameters: Dict[str, Any]) -> ToolResult:
|
|
103
|
+
"""Execute tool in sandbox.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
sandbox_id: Sandbox ID
|
|
107
|
+
tool_name: Tool name to execute
|
|
108
|
+
parameters: Tool parameters
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Tool execution result
|
|
112
|
+
|
|
113
|
+
Raises:
|
|
114
|
+
ValueError: If sandbox or tool not found
|
|
115
|
+
"""
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
@abstractmethod
|
|
119
|
+
async def get_sandbox_tools(self, sandbox_id: str) -> Dict[str, Any]:
|
|
120
|
+
"""Get available tools for a sandbox.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
sandbox_id: Sandbox ID
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Dictionary of available tool types, e.g., {"tool_name": tool_schema}
|
|
127
|
+
|
|
128
|
+
Raises:
|
|
129
|
+
ValueError: If sandbox not found
|
|
130
|
+
"""
|
|
131
|
+
pass
|
|
132
|
+
|
|
133
|
+
@abstractmethod
|
|
134
|
+
async def get_stats(self) -> Dict[str, Any]:
|
|
135
|
+
"""Get manager statistics.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Statistics dictionary
|
|
139
|
+
"""
|
|
140
|
+
pass
|
|
141
|
+
|
|
142
|
+
@abstractmethod
|
|
143
|
+
async def cleanup_all_sandboxes(self) -> None:
|
|
144
|
+
"""Clean up all sandboxes."""
|
|
145
|
+
pass
|
|
146
|
+
|
|
147
|
+
# Context manager support
|
|
148
|
+
async def __aenter__(self):
|
|
149
|
+
"""Async context manager entry."""
|
|
150
|
+
await self.start()
|
|
151
|
+
return self
|
|
152
|
+
|
|
153
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
154
|
+
"""Async context manager exit."""
|
|
155
|
+
await self.stop()
|