ms-enclave 0.0.0__py3-none-any.whl → 0.0.2__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.

Files changed (43) hide show
  1. ms_enclave/__init__.py +2 -2
  2. ms_enclave/cli/__init__.py +1 -0
  3. ms_enclave/cli/base.py +20 -0
  4. ms_enclave/cli/cli.py +27 -0
  5. ms_enclave/cli/start_server.py +84 -0
  6. ms_enclave/sandbox/__init__.py +27 -0
  7. ms_enclave/sandbox/boxes/__init__.py +16 -0
  8. ms_enclave/sandbox/boxes/base.py +270 -0
  9. ms_enclave/sandbox/boxes/docker_notebook.py +214 -0
  10. ms_enclave/sandbox/boxes/docker_sandbox.py +317 -0
  11. ms_enclave/sandbox/manager/__init__.py +11 -0
  12. ms_enclave/sandbox/manager/base.py +155 -0
  13. ms_enclave/sandbox/manager/http_manager.py +405 -0
  14. ms_enclave/sandbox/manager/local_manager.py +295 -0
  15. ms_enclave/sandbox/model/__init__.py +21 -0
  16. ms_enclave/sandbox/model/base.py +36 -0
  17. ms_enclave/sandbox/model/config.py +97 -0
  18. ms_enclave/sandbox/model/requests.py +57 -0
  19. ms_enclave/sandbox/model/responses.py +57 -0
  20. ms_enclave/sandbox/server/__init__.py +0 -0
  21. ms_enclave/sandbox/server/server.py +195 -0
  22. ms_enclave/sandbox/tools/__init__.py +4 -0
  23. ms_enclave/sandbox/tools/base.py +95 -0
  24. ms_enclave/sandbox/tools/sandbox_tool.py +46 -0
  25. ms_enclave/sandbox/tools/sandbox_tools/__init__.py +4 -0
  26. ms_enclave/sandbox/tools/sandbox_tools/file_operation.py +331 -0
  27. ms_enclave/sandbox/tools/sandbox_tools/notebook_executor.py +167 -0
  28. ms_enclave/sandbox/tools/sandbox_tools/python_executor.py +87 -0
  29. ms_enclave/sandbox/tools/sandbox_tools/shell_executor.py +63 -0
  30. ms_enclave/sandbox/tools/tool_info.py +141 -0
  31. ms_enclave/utils/__init__.py +1 -0
  32. ms_enclave/utils/json_schema.py +208 -0
  33. ms_enclave/utils/logger.py +170 -0
  34. ms_enclave/version.py +2 -2
  35. ms_enclave-0.0.2.dist-info/METADATA +366 -0
  36. ms_enclave-0.0.2.dist-info/RECORD +40 -0
  37. {ms_enclave-0.0.0.dist-info → ms_enclave-0.0.2.dist-info}/WHEEL +1 -1
  38. ms_enclave-0.0.2.dist-info/entry_points.txt +2 -0
  39. ms_enclave/run_server.py +0 -21
  40. ms_enclave-0.0.0.dist-info/METADATA +0 -329
  41. ms_enclave-0.0.0.dist-info/RECORD +0 -8
  42. {ms_enclave-0.0.0.dist-info → ms_enclave-0.0.2.dist-info}/licenses/LICENSE +0 -0
  43. {ms_enclave-0.0.0.dist-info → ms_enclave-0.0.2.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}')