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,405 @@
|
|
|
1
|
+
"""HTTP-based sandbox manager for remote sandbox services."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List, Optional, Union
|
|
4
|
+
|
|
5
|
+
import aiohttp
|
|
6
|
+
|
|
7
|
+
from ms_enclave.utils import get_logger
|
|
8
|
+
|
|
9
|
+
from ..model import SandboxConfig, SandboxInfo, SandboxStatus, SandboxType, ToolExecutionRequest, ToolResult
|
|
10
|
+
from .base import SandboxManager
|
|
11
|
+
|
|
12
|
+
logger = get_logger()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class HttpSandboxManager(SandboxManager):
|
|
16
|
+
"""HTTP-based sandbox manager for remote services.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, base_url: str, timeout: int = 30, api_key: Optional[str] = None):
|
|
20
|
+
"""Initialize HTTP sandbox manager.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
base_url: Base URL of the sandbox service
|
|
24
|
+
timeout: Request timeout in seconds
|
|
25
|
+
api_key: Optional API key to include as ``X-API-Key`` header for all requests
|
|
26
|
+
"""
|
|
27
|
+
super().__init__()
|
|
28
|
+
self.base_url = base_url.rstrip('/')
|
|
29
|
+
self.timeout = aiohttp.ClientTimeout(total=timeout)
|
|
30
|
+
self._default_headers: Optional[Dict[str, str]] = {'X-API-Key': api_key} if api_key else None
|
|
31
|
+
self._session: Optional[aiohttp.ClientSession] = None
|
|
32
|
+
|
|
33
|
+
async def start(self) -> None:
|
|
34
|
+
"""Start the HTTP sandbox manager."""
|
|
35
|
+
if self._running:
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
self._connector = aiohttp.TCPConnector()
|
|
39
|
+
self._session = aiohttp.ClientSession(
|
|
40
|
+
connector=self._connector, timeout=self.timeout, headers=self._default_headers
|
|
41
|
+
)
|
|
42
|
+
self._running = True
|
|
43
|
+
logger.info(f'HTTP sandbox manager started, connected to {self.base_url}')
|
|
44
|
+
|
|
45
|
+
async def stop(self) -> None:
|
|
46
|
+
"""Stop the HTTP sandbox manager."""
|
|
47
|
+
if not self._running:
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
self._running = False
|
|
51
|
+
|
|
52
|
+
# Clean up all sandboxes created by this manager
|
|
53
|
+
if self._sandboxes:
|
|
54
|
+
logger.info(f'Cleaning up {len(self._sandboxes)} sandboxes created by this manager')
|
|
55
|
+
await self.cleanup_all_sandboxes()
|
|
56
|
+
|
|
57
|
+
if self._session:
|
|
58
|
+
await self._session.close()
|
|
59
|
+
self._session = None
|
|
60
|
+
logger.info('HTTP sandbox manager stopped')
|
|
61
|
+
|
|
62
|
+
async def create_sandbox(
|
|
63
|
+
self,
|
|
64
|
+
sandbox_type: SandboxType,
|
|
65
|
+
config: Optional[Union[SandboxConfig, Dict]] = None,
|
|
66
|
+
sandbox_id: Optional[str] = None
|
|
67
|
+
) -> str:
|
|
68
|
+
"""Create a new sandbox via HTTP API.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
sandbox_type: Type of sandbox to create
|
|
72
|
+
config: Sandbox configuration
|
|
73
|
+
sandbox_id: Optional sandbox ID
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Sandbox ID
|
|
77
|
+
|
|
78
|
+
Raises:
|
|
79
|
+
ValueError: If sandbox type is not supported
|
|
80
|
+
RuntimeError: If sandbox creation fails
|
|
81
|
+
"""
|
|
82
|
+
if not self._session:
|
|
83
|
+
raise RuntimeError('Manager not started')
|
|
84
|
+
|
|
85
|
+
# Match server's endpoint format: POST /sandbox/create
|
|
86
|
+
params = {'sandbox_type': sandbox_type.value if isinstance(sandbox_type, SandboxType) else sandbox_type}
|
|
87
|
+
if isinstance(config, SandboxConfig):
|
|
88
|
+
payload = config.model_dump(exclude_none=True)
|
|
89
|
+
elif isinstance(config, dict):
|
|
90
|
+
payload = config
|
|
91
|
+
else:
|
|
92
|
+
payload = {}
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
async with self._session.post(f'{self.base_url}/sandbox/create', params=params, json=payload) as response:
|
|
96
|
+
if response.status == 200:
|
|
97
|
+
data = await response.json()
|
|
98
|
+
sandbox_id = data['sandbox_id']
|
|
99
|
+
|
|
100
|
+
# Get sandbox info and store in _sandboxes
|
|
101
|
+
sandbox_info = await self.get_sandbox_info(sandbox_id)
|
|
102
|
+
if sandbox_info:
|
|
103
|
+
self._sandboxes[sandbox_id] = sandbox_info
|
|
104
|
+
|
|
105
|
+
logger.info(f'Created sandbox {sandbox_id} via HTTP API')
|
|
106
|
+
return sandbox_id
|
|
107
|
+
else:
|
|
108
|
+
error_data = await response.json()
|
|
109
|
+
raise RuntimeError(f"HTTP {response.status}: {error_data.get('detail', 'Unknown error')}")
|
|
110
|
+
|
|
111
|
+
except aiohttp.ClientError as e:
|
|
112
|
+
logger.error(f'HTTP client error creating sandbox: {e}')
|
|
113
|
+
raise RuntimeError(f'Failed to create sandbox: {e}')
|
|
114
|
+
|
|
115
|
+
async def get_sandbox_info(self, sandbox_id: str) -> Optional[SandboxInfo]:
|
|
116
|
+
"""Get sandbox information via HTTP API.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
sandbox_id: Sandbox ID
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Sandbox information or None if not found
|
|
123
|
+
"""
|
|
124
|
+
if not self._session:
|
|
125
|
+
raise RuntimeError('Manager not started')
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
# Match server's endpoint format: GET /sandbox/{sandbox_id}
|
|
129
|
+
async with self._session.get(f'{self.base_url}/sandbox/{sandbox_id}') as response:
|
|
130
|
+
if response.status == 200:
|
|
131
|
+
data = await response.json()
|
|
132
|
+
return SandboxInfo.model_validate(data)
|
|
133
|
+
elif response.status == 404:
|
|
134
|
+
return None
|
|
135
|
+
else:
|
|
136
|
+
error_data = await response.json()
|
|
137
|
+
logger.error(f'Error getting sandbox info: HTTP {response.status}: {error_data}')
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
except aiohttp.ClientError as e:
|
|
141
|
+
logger.error(f'HTTP client error getting sandbox info: {e}')
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
async def list_sandboxes(self, status_filter: Optional[SandboxStatus] = None) -> List[SandboxInfo]:
|
|
145
|
+
"""List all sandboxes via HTTP API.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
status_filter: Optional status filter
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
List of sandbox information
|
|
152
|
+
"""
|
|
153
|
+
if not self._session:
|
|
154
|
+
raise RuntimeError('Manager not started')
|
|
155
|
+
|
|
156
|
+
params = {}
|
|
157
|
+
if status_filter:
|
|
158
|
+
params['status'] = status_filter.value
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
# Match server's endpoint format: GET /sandboxes
|
|
162
|
+
async with self._session.get(f'{self.base_url}/sandboxes', params=params) as response:
|
|
163
|
+
if response.status == 200:
|
|
164
|
+
data = await response.json()
|
|
165
|
+
return [SandboxInfo.model_validate(item) for item in data]
|
|
166
|
+
else:
|
|
167
|
+
error_data = await response.json()
|
|
168
|
+
logger.error(f'Error listing sandboxes: HTTP {response.status}: {error_data}')
|
|
169
|
+
return []
|
|
170
|
+
|
|
171
|
+
except aiohttp.ClientError as e:
|
|
172
|
+
logger.error(f'HTTP client error listing sandboxes: {e}')
|
|
173
|
+
return []
|
|
174
|
+
|
|
175
|
+
async def stop_sandbox(self, sandbox_id: str) -> bool:
|
|
176
|
+
"""Stop a sandbox via HTTP API.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
sandbox_id: Sandbox ID
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
True if stopped successfully, False if not found
|
|
183
|
+
"""
|
|
184
|
+
if not self._session:
|
|
185
|
+
raise RuntimeError('Manager not started')
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
# Match server's endpoint format: POST /sandbox/{sandbox_id}/stop
|
|
189
|
+
async with self._session.post(f'{self.base_url}/sandbox/{sandbox_id}/stop') as response:
|
|
190
|
+
if response.status == 200:
|
|
191
|
+
logger.info(f'Stopped sandbox {sandbox_id} via HTTP API')
|
|
192
|
+
return True
|
|
193
|
+
elif response.status == 404:
|
|
194
|
+
logger.warning(f'Sandbox {sandbox_id} not found for stopping')
|
|
195
|
+
return False
|
|
196
|
+
else:
|
|
197
|
+
error_data = await response.json()
|
|
198
|
+
logger.error(f'Error stopping sandbox: HTTP {response.status}: {error_data}')
|
|
199
|
+
return False
|
|
200
|
+
|
|
201
|
+
except aiohttp.ClientError as e:
|
|
202
|
+
logger.error(f'HTTP client error stopping sandbox: {e}')
|
|
203
|
+
return False
|
|
204
|
+
|
|
205
|
+
async def delete_sandbox(self, sandbox_id: str) -> bool:
|
|
206
|
+
"""Delete a sandbox via HTTP API.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
sandbox_id: Sandbox ID
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
True if deleted successfully, False if not found
|
|
213
|
+
"""
|
|
214
|
+
if not self._session:
|
|
215
|
+
raise RuntimeError('Manager not started')
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
# Match server's endpoint format: DELETE /sandbox/{sandbox_id}
|
|
219
|
+
async with self._session.delete(f'{self.base_url}/sandbox/{sandbox_id}') as response:
|
|
220
|
+
if response.status == 200:
|
|
221
|
+
# Remove from tracking when successfully deleted
|
|
222
|
+
self._sandboxes.pop(sandbox_id, None)
|
|
223
|
+
logger.info(f'Deleted sandbox {sandbox_id} via HTTP API')
|
|
224
|
+
return True
|
|
225
|
+
elif response.status == 404:
|
|
226
|
+
# Also remove from tracking if not found (already deleted)
|
|
227
|
+
self._sandboxes.pop(sandbox_id, None)
|
|
228
|
+
logger.warning(f'Sandbox {sandbox_id} not found for deletion')
|
|
229
|
+
return False
|
|
230
|
+
else:
|
|
231
|
+
error_data = await response.json()
|
|
232
|
+
logger.error(f'Error deleting sandbox: HTTP {response.status}: {error_data}')
|
|
233
|
+
return False
|
|
234
|
+
|
|
235
|
+
except aiohttp.ClientError as e:
|
|
236
|
+
logger.error(f'HTTP client error deleting sandbox: {e}')
|
|
237
|
+
return False
|
|
238
|
+
|
|
239
|
+
async def execute_tool(self, sandbox_id: str, tool_name: str, parameters: Dict[str, Any]) -> ToolResult:
|
|
240
|
+
"""Execute tool in sandbox via HTTP API.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
sandbox_id: Sandbox ID
|
|
244
|
+
tool_name: Tool name to execute
|
|
245
|
+
parameters: Tool parameters
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
Tool execution result
|
|
249
|
+
|
|
250
|
+
Raises:
|
|
251
|
+
ValueError: If sandbox or tool not found
|
|
252
|
+
"""
|
|
253
|
+
if not self._session:
|
|
254
|
+
raise RuntimeError('Manager not started')
|
|
255
|
+
|
|
256
|
+
# Create proper request object to match server expectations
|
|
257
|
+
request = ToolExecutionRequest(sandbox_id=sandbox_id, tool_name=tool_name, parameters=parameters)
|
|
258
|
+
payload = request.model_dump()
|
|
259
|
+
|
|
260
|
+
try:
|
|
261
|
+
# Match server's endpoint format: POST /sandbox/tool/execute
|
|
262
|
+
async with self._session.post(f'{self.base_url}/sandbox/tool/execute', json=payload) as response:
|
|
263
|
+
if response.status == 200:
|
|
264
|
+
data = await response.json()
|
|
265
|
+
return ToolResult.model_validate(data)
|
|
266
|
+
elif response.status == 404:
|
|
267
|
+
error_data = await response.json()
|
|
268
|
+
raise ValueError(error_data.get('detail', f'Sandbox {sandbox_id} not found'))
|
|
269
|
+
elif response.status == 500:
|
|
270
|
+
error_data = await response.json()
|
|
271
|
+
raise RuntimeError(error_data.get('detail', 'Internal server error'))
|
|
272
|
+
else:
|
|
273
|
+
error_data = await response.json()
|
|
274
|
+
raise RuntimeError(f"HTTP {response.status}: {error_data.get('detail', 'Unknown error')}")
|
|
275
|
+
|
|
276
|
+
except aiohttp.ClientError as e:
|
|
277
|
+
logger.error(f'HTTP client error executing tool: {e}')
|
|
278
|
+
raise RuntimeError(f'Failed to execute tool: {e}')
|
|
279
|
+
|
|
280
|
+
async def get_sandbox_tools(self, sandbox_id: str) -> Dict[str, Any]:
|
|
281
|
+
"""Get available tools for a sandbox via HTTP API.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
sandbox_id: Sandbox ID
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
List of available tool types
|
|
288
|
+
|
|
289
|
+
Raises:
|
|
290
|
+
ValueError: If sandbox not found
|
|
291
|
+
"""
|
|
292
|
+
if not self._session:
|
|
293
|
+
raise RuntimeError('Manager not started')
|
|
294
|
+
|
|
295
|
+
try:
|
|
296
|
+
# Match server's endpoint format: GET /sandbox/{sandbox_id}/tools
|
|
297
|
+
async with self._session.get(f'{self.base_url}/sandbox/{sandbox_id}/tools') as response:
|
|
298
|
+
if response.status == 200:
|
|
299
|
+
data = await response.json()
|
|
300
|
+
return data
|
|
301
|
+
elif response.status == 404:
|
|
302
|
+
error_data = await response.json()
|
|
303
|
+
raise ValueError(error_data.get('detail', f'Sandbox {sandbox_id} not found'))
|
|
304
|
+
else:
|
|
305
|
+
error_data = await response.json()
|
|
306
|
+
raise RuntimeError(f"HTTP {response.status}: {error_data.get('detail', 'Unknown error')}")
|
|
307
|
+
|
|
308
|
+
except aiohttp.ClientError as e:
|
|
309
|
+
logger.error(f'HTTP client error getting sandbox tools: {e}')
|
|
310
|
+
raise RuntimeError(f'Failed to get sandbox tools: {e}')
|
|
311
|
+
|
|
312
|
+
async def cleanup_all_sandboxes(self) -> None:
|
|
313
|
+
"""Clean up all sandboxes created by this manager via HTTP API."""
|
|
314
|
+
if not self._session:
|
|
315
|
+
raise RuntimeError('Manager not started')
|
|
316
|
+
|
|
317
|
+
try:
|
|
318
|
+
# Clean up only the sandboxes created by this manager
|
|
319
|
+
sandbox_ids = list(self._sandboxes.keys())
|
|
320
|
+
deleted_count = 0
|
|
321
|
+
|
|
322
|
+
for sandbox_id in sandbox_ids:
|
|
323
|
+
try:
|
|
324
|
+
if await self.delete_sandbox(sandbox_id):
|
|
325
|
+
deleted_count += 1
|
|
326
|
+
except Exception as e:
|
|
327
|
+
logger.error(f'Error deleting sandbox {sandbox_id}: {e}')
|
|
328
|
+
|
|
329
|
+
logger.info(f'Cleaned up {deleted_count} sandboxes created by this manager')
|
|
330
|
+
|
|
331
|
+
except Exception as e:
|
|
332
|
+
logger.error(f'HTTP client error cleaning up sandboxes: {e}')
|
|
333
|
+
|
|
334
|
+
async def get_stats(self) -> Dict[str, Any]:
|
|
335
|
+
"""Get server statistics via HTTP API.
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
Server statistics dictionary
|
|
339
|
+
"""
|
|
340
|
+
if not self._session:
|
|
341
|
+
raise RuntimeError('Manager not started')
|
|
342
|
+
|
|
343
|
+
try:
|
|
344
|
+
# Get server stats
|
|
345
|
+
async with self._session.get(f'{self.base_url}/stats') as response:
|
|
346
|
+
if response.status == 200:
|
|
347
|
+
server_stats = await response.json()
|
|
348
|
+
else:
|
|
349
|
+
error_data = await response.json()
|
|
350
|
+
logger.error(f'Error getting server stats: HTTP {response.status}: {error_data}')
|
|
351
|
+
server_stats = {}
|
|
352
|
+
|
|
353
|
+
# Add local tracking stats
|
|
354
|
+
from collections import Counter
|
|
355
|
+
status_counter = Counter()
|
|
356
|
+
type_counter = Counter()
|
|
357
|
+
|
|
358
|
+
for sandbox_info in self._sandboxes.values():
|
|
359
|
+
status_counter[sandbox_info.status.value] += 1
|
|
360
|
+
type_counter[sandbox_info.sandbox_type.value] += 1
|
|
361
|
+
|
|
362
|
+
local_stats = {
|
|
363
|
+
'manager_type': 'http',
|
|
364
|
+
'base_url': self.base_url,
|
|
365
|
+
'tracked_sandboxes': len(self._sandboxes),
|
|
366
|
+
'tracked_status_counts': dict(status_counter),
|
|
367
|
+
'tracked_sandbox_types': dict(type_counter),
|
|
368
|
+
'running': self._running,
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
# Combine server and local stats
|
|
372
|
+
return {**server_stats, **local_stats}
|
|
373
|
+
|
|
374
|
+
except aiohttp.ClientError as e:
|
|
375
|
+
logger.error(f'HTTP client error getting server stats: {e}')
|
|
376
|
+
return {
|
|
377
|
+
'manager_type': 'http',
|
|
378
|
+
'base_url': self.base_url,
|
|
379
|
+
'tracked_sandboxes': len(self._sandboxes),
|
|
380
|
+
'running': self._running,
|
|
381
|
+
'error': str(e)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async def health_check(self) -> Dict[str, Any]:
|
|
385
|
+
"""Perform health check via HTTP API.
|
|
386
|
+
|
|
387
|
+
Returns:
|
|
388
|
+
Health check result
|
|
389
|
+
"""
|
|
390
|
+
if not self._session:
|
|
391
|
+
raise RuntimeError('Manager not started')
|
|
392
|
+
|
|
393
|
+
try:
|
|
394
|
+
# Match server's endpoint format: GET /health
|
|
395
|
+
async with self._session.get(f'{self.base_url}/health') as response:
|
|
396
|
+
if response.status == 200:
|
|
397
|
+
return await response.json()
|
|
398
|
+
else:
|
|
399
|
+
error_data = await response.json()
|
|
400
|
+
logger.error(f'Health check failed: HTTP {response.status}: {error_data}')
|
|
401
|
+
return {'healthy': False, 'error': f'HTTP {response.status}'}
|
|
402
|
+
|
|
403
|
+
except aiohttp.ClientError as e:
|
|
404
|
+
logger.error(f'HTTP client error during health check: {e}')
|
|
405
|
+
return {'healthy': False, 'error': str(e)}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""Sandbox environment manager."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import time
|
|
5
|
+
from collections import Counter
|
|
6
|
+
from datetime import datetime, timedelta
|
|
7
|
+
from typing import Any, Dict, List, Optional, Union
|
|
8
|
+
|
|
9
|
+
from ms_enclave.utils import get_logger
|
|
10
|
+
|
|
11
|
+
from ..boxes import Sandbox, SandboxFactory
|
|
12
|
+
from ..model import SandboxConfig, SandboxInfo, SandboxStatus, SandboxType, ToolResult
|
|
13
|
+
from .base import SandboxManager
|
|
14
|
+
|
|
15
|
+
logger = get_logger()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LocalSandboxManager(SandboxManager):
|
|
19
|
+
"""Manager for sandbox environments."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, cleanup_interval: int = 300): # 5 minutes
|
|
22
|
+
"""Initialize sandbox manager.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
cleanup_interval: Interval between cleanup runs in seconds
|
|
26
|
+
"""
|
|
27
|
+
super().__init__()
|
|
28
|
+
self._cleanup_interval = cleanup_interval
|
|
29
|
+
self._cleanup_task: Optional[asyncio.Task] = None
|
|
30
|
+
|
|
31
|
+
async def start(self) -> None:
|
|
32
|
+
"""Start the sandbox manager."""
|
|
33
|
+
if self._running:
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
self._running = True
|
|
37
|
+
self._cleanup_task = asyncio.create_task(self._cleanup_loop())
|
|
38
|
+
logger.info('Local sandbox manager started')
|
|
39
|
+
|
|
40
|
+
async def stop(self) -> None:
|
|
41
|
+
"""Stop the sandbox manager."""
|
|
42
|
+
if not self._running:
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
self._running = False
|
|
46
|
+
|
|
47
|
+
# Cancel cleanup task
|
|
48
|
+
if self._cleanup_task:
|
|
49
|
+
self._cleanup_task.cancel()
|
|
50
|
+
try:
|
|
51
|
+
await self._cleanup_task
|
|
52
|
+
except asyncio.CancelledError:
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
# Stop and cleanup all sandboxes
|
|
56
|
+
await self.cleanup_all_sandboxes()
|
|
57
|
+
logger.info('Local sandbox manager stopped')
|
|
58
|
+
|
|
59
|
+
async def create_sandbox(
|
|
60
|
+
self,
|
|
61
|
+
sandbox_type: SandboxType,
|
|
62
|
+
config: Optional[Union[SandboxConfig, Dict]] = None,
|
|
63
|
+
sandbox_id: Optional[str] = None
|
|
64
|
+
) -> str:
|
|
65
|
+
"""Create a new sandbox.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
sandbox_type: Type of sandbox to create
|
|
69
|
+
config: Sandbox configuration
|
|
70
|
+
sandbox_id: Optional sandbox ID
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Sandbox ID
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
ValueError: If sandbox type is not supported
|
|
77
|
+
RuntimeError: If sandbox creation fails
|
|
78
|
+
"""
|
|
79
|
+
try:
|
|
80
|
+
# Create sandbox instance
|
|
81
|
+
sandbox = SandboxFactory.create_sandbox(sandbox_type, config, sandbox_id)
|
|
82
|
+
|
|
83
|
+
# Start the sandbox
|
|
84
|
+
await sandbox.start()
|
|
85
|
+
|
|
86
|
+
# Store sandbox
|
|
87
|
+
self._sandboxes[sandbox.id] = sandbox
|
|
88
|
+
|
|
89
|
+
logger.info(f'Created and started sandbox {sandbox.id} of type {sandbox_type}')
|
|
90
|
+
return sandbox.id
|
|
91
|
+
|
|
92
|
+
except Exception as e:
|
|
93
|
+
logger.error(f'Failed to create sandbox of type {sandbox_type}: {e}')
|
|
94
|
+
raise RuntimeError(f'Failed to create sandbox: {e}')
|
|
95
|
+
|
|
96
|
+
async def get_sandbox(self, sandbox_id: str) -> Optional[Sandbox]:
|
|
97
|
+
"""Get sandbox by ID.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
sandbox_id: Sandbox ID
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Sandbox instance or None if not found
|
|
104
|
+
"""
|
|
105
|
+
return self._sandboxes.get(sandbox_id)
|
|
106
|
+
|
|
107
|
+
async def get_sandbox_info(self, sandbox_id: str) -> Optional[SandboxInfo]:
|
|
108
|
+
"""Get sandbox information.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
sandbox_id: Sandbox ID
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Sandbox information or None if not found
|
|
115
|
+
"""
|
|
116
|
+
sandbox = self._sandboxes.get(sandbox_id)
|
|
117
|
+
if sandbox:
|
|
118
|
+
return sandbox.get_info()
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
async def list_sandboxes(self, status_filter: Optional[SandboxStatus] = None) -> List[SandboxInfo]:
|
|
122
|
+
"""List all sandboxes.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
status_filter: Optional status filter
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
List of sandbox information
|
|
129
|
+
"""
|
|
130
|
+
result = []
|
|
131
|
+
for sandbox in self._sandboxes.values():
|
|
132
|
+
info = sandbox.get_info()
|
|
133
|
+
if status_filter is None or info.status == status_filter:
|
|
134
|
+
result.append(info)
|
|
135
|
+
return result
|
|
136
|
+
|
|
137
|
+
async def stop_sandbox(self, sandbox_id: str) -> bool:
|
|
138
|
+
"""Stop a sandbox.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
sandbox_id: Sandbox ID
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
True if stopped successfully, False if not found
|
|
145
|
+
"""
|
|
146
|
+
sandbox = self._sandboxes.get(sandbox_id)
|
|
147
|
+
if not sandbox:
|
|
148
|
+
logger.warning(f'Sandbox {sandbox_id} not found for stopping')
|
|
149
|
+
return False
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
await sandbox.stop()
|
|
153
|
+
logger.info(f'Stopped sandbox {sandbox_id}')
|
|
154
|
+
return True
|
|
155
|
+
except Exception as e:
|
|
156
|
+
logger.error(f'Error stopping sandbox {sandbox_id}: {e}')
|
|
157
|
+
return False
|
|
158
|
+
|
|
159
|
+
async def delete_sandbox(self, sandbox_id: str) -> bool:
|
|
160
|
+
"""Delete a sandbox.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
sandbox_id: Sandbox ID
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
True if deleted successfully, False if not found
|
|
167
|
+
"""
|
|
168
|
+
sandbox = self._sandboxes.get(sandbox_id)
|
|
169
|
+
if not sandbox:
|
|
170
|
+
logger.warning(f'Sandbox {sandbox_id} not found for deletion')
|
|
171
|
+
return False
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
await sandbox.stop()
|
|
175
|
+
del self._sandboxes[sandbox_id]
|
|
176
|
+
logger.info(f'Deleted sandbox {sandbox_id}')
|
|
177
|
+
return True
|
|
178
|
+
except Exception as e:
|
|
179
|
+
logger.error(f'Error deleting sandbox {sandbox_id}: {e}')
|
|
180
|
+
return False
|
|
181
|
+
|
|
182
|
+
async def execute_tool(self, sandbox_id: str, tool_name: str, parameters: Dict[str, Any]) -> ToolResult:
|
|
183
|
+
"""Execute tool in sandbox.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
sandbox_id: Sandbox ID
|
|
187
|
+
tool_name: Tool name to execute
|
|
188
|
+
parameters: Tool parameters
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
Tool execution result
|
|
192
|
+
|
|
193
|
+
Raises:
|
|
194
|
+
ValueError: If sandbox or tool not found
|
|
195
|
+
"""
|
|
196
|
+
sandbox = self._sandboxes.get(sandbox_id)
|
|
197
|
+
if not sandbox:
|
|
198
|
+
raise ValueError(f'Sandbox {sandbox_id} not found')
|
|
199
|
+
|
|
200
|
+
if sandbox.status != SandboxStatus.RUNNING:
|
|
201
|
+
raise ValueError(f'Sandbox {sandbox_id} is not running (status: {sandbox.status})')
|
|
202
|
+
|
|
203
|
+
result = await sandbox.execute_tool(tool_name, parameters)
|
|
204
|
+
return result
|
|
205
|
+
|
|
206
|
+
async def get_sandbox_tools(self, sandbox_id: str) -> Dict[str, Any]:
|
|
207
|
+
"""Get available tools for a sandbox.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
sandbox_id: Sandbox ID
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
Dictionary of available tool types
|
|
214
|
+
|
|
215
|
+
Raises:
|
|
216
|
+
ValueError: If sandbox not found
|
|
217
|
+
"""
|
|
218
|
+
sandbox = self._sandboxes.get(sandbox_id)
|
|
219
|
+
if not sandbox:
|
|
220
|
+
raise ValueError(f'Sandbox {sandbox_id} not found')
|
|
221
|
+
|
|
222
|
+
return sandbox.get_available_tools()
|
|
223
|
+
|
|
224
|
+
async def cleanup_all_sandboxes(self) -> None:
|
|
225
|
+
"""Clean up all sandboxes."""
|
|
226
|
+
sandbox_ids = list(self._sandboxes.keys())
|
|
227
|
+
logger.info(f'Cleaning up {len(sandbox_ids)} sandboxes')
|
|
228
|
+
|
|
229
|
+
for sandbox_id in sandbox_ids:
|
|
230
|
+
try:
|
|
231
|
+
await self.delete_sandbox(sandbox_id)
|
|
232
|
+
except Exception as e:
|
|
233
|
+
logger.error(f'Error cleaning up sandbox {sandbox_id}: {e}')
|
|
234
|
+
|
|
235
|
+
async def get_stats(self) -> Dict[str, Any]:
|
|
236
|
+
"""Get manager statistics.
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
Statistics dictionary
|
|
240
|
+
"""
|
|
241
|
+
status_counter = Counter()
|
|
242
|
+
type_counter = Counter()
|
|
243
|
+
|
|
244
|
+
for sandbox in self._sandboxes.values():
|
|
245
|
+
status_counter[sandbox.status.value] += 1
|
|
246
|
+
type_counter[sandbox.sandbox_type.value] += 1
|
|
247
|
+
|
|
248
|
+
stats = {
|
|
249
|
+
'manager_type': 'local',
|
|
250
|
+
'total_sandboxes': len(self._sandboxes),
|
|
251
|
+
'status_counts': dict(status_counter),
|
|
252
|
+
'sandbox_types': dict(type_counter),
|
|
253
|
+
'running': self._running,
|
|
254
|
+
'cleanup_interval': self._cleanup_interval,
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return stats
|
|
258
|
+
|
|
259
|
+
async def _cleanup_loop(self) -> None:
|
|
260
|
+
"""Background cleanup loop."""
|
|
261
|
+
while self._running:
|
|
262
|
+
try:
|
|
263
|
+
await self._cleanup_expired_sandboxes()
|
|
264
|
+
await asyncio.sleep(self._cleanup_interval)
|
|
265
|
+
except asyncio.CancelledError:
|
|
266
|
+
break
|
|
267
|
+
except Exception as e:
|
|
268
|
+
logger.error(f'Error in cleanup loop: {e}')
|
|
269
|
+
await asyncio.sleep(self._cleanup_interval)
|
|
270
|
+
|
|
271
|
+
async def _cleanup_expired_sandboxes(self) -> None:
|
|
272
|
+
"""Clean up expired sandboxes."""
|
|
273
|
+
current_time = datetime.now()
|
|
274
|
+
expired_sandboxes = []
|
|
275
|
+
|
|
276
|
+
for sandbox_id, sandbox in self._sandboxes.items():
|
|
277
|
+
# Check if sandbox is in error state or stopped for too long
|
|
278
|
+
if sandbox.status in [SandboxStatus.ERROR, SandboxStatus.STOPPED]:
|
|
279
|
+
# Clean up after 1 hour
|
|
280
|
+
if current_time - sandbox.updated_at > timedelta(hours=1):
|
|
281
|
+
expired_sandboxes.append(sandbox_id)
|
|
282
|
+
# Check for very old sandboxes (48 hours)
|
|
283
|
+
elif current_time - sandbox.created_at > timedelta(hours=48):
|
|
284
|
+
expired_sandboxes.append(sandbox_id)
|
|
285
|
+
|
|
286
|
+
# Clean up expired sandboxes
|
|
287
|
+
if expired_sandboxes:
|
|
288
|
+
logger.info(f'Found {len(expired_sandboxes)} expired sandboxes to clean up')
|
|
289
|
+
|
|
290
|
+
for sandbox_id in expired_sandboxes:
|
|
291
|
+
try:
|
|
292
|
+
logger.info(f'Cleaning up expired sandbox: {sandbox_id}')
|
|
293
|
+
await self.delete_sandbox(sandbox_id)
|
|
294
|
+
except Exception as e:
|
|
295
|
+
logger.error(f'Error cleaning up expired sandbox {sandbox_id}: {e}')
|