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,331 @@
|
|
|
1
|
+
"""File operation tool for reading and writing files."""
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
import os
|
|
5
|
+
import tarfile
|
|
6
|
+
import uuid
|
|
7
|
+
from typing import TYPE_CHECKING, Literal, Optional
|
|
8
|
+
|
|
9
|
+
from ms_enclave.sandbox.model import ExecutionStatus, SandboxType, ToolResult
|
|
10
|
+
from ms_enclave.sandbox.tools.base import register_tool
|
|
11
|
+
from ms_enclave.sandbox.tools.sandbox_tool import SandboxTool
|
|
12
|
+
from ms_enclave.sandbox.tools.tool_info import ToolParams
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from ms_enclave.sandbox.boxes import Sandbox
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@register_tool('file_operation')
|
|
19
|
+
class FileOperation(SandboxTool):
|
|
20
|
+
"""Tool for performing file operations within Docker containers.
|
|
21
|
+
|
|
22
|
+
Supports read, write, create, delete, list, and exists operations.
|
|
23
|
+
Uses temporary files and copy strategy to avoid permission issues
|
|
24
|
+
with mounted directories.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
_name = 'file_operation'
|
|
28
|
+
_sandbox_type = SandboxType.DOCKER
|
|
29
|
+
_description = 'Perform file operations like read, write, delete, and list files'
|
|
30
|
+
_parameters = ToolParams(
|
|
31
|
+
type='object',
|
|
32
|
+
properties={
|
|
33
|
+
'operation': {
|
|
34
|
+
'type': 'string',
|
|
35
|
+
'description': 'Type of file operation to perform',
|
|
36
|
+
'enum': ['create', 'read', 'write', 'delete', 'list', 'exists']
|
|
37
|
+
},
|
|
38
|
+
'file_path': {
|
|
39
|
+
'type': 'string',
|
|
40
|
+
'description': 'Path to the file or directory'
|
|
41
|
+
},
|
|
42
|
+
'content': {
|
|
43
|
+
'type': 'string',
|
|
44
|
+
'description': 'Content to write to file (only for write operation)'
|
|
45
|
+
},
|
|
46
|
+
'encoding': {
|
|
47
|
+
'type': 'string',
|
|
48
|
+
'description': 'File encoding',
|
|
49
|
+
'default': 'utf-8'
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
required=['operation', 'file_path']
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
async def execute(
|
|
56
|
+
self,
|
|
57
|
+
sandbox_context: 'Sandbox',
|
|
58
|
+
operation: str,
|
|
59
|
+
file_path: str,
|
|
60
|
+
content: Optional[str] = None,
|
|
61
|
+
encoding: str = 'utf-8'
|
|
62
|
+
) -> ToolResult:
|
|
63
|
+
"""Perform file operations in the Docker container.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
sandbox_context: The sandbox instance to execute operations in
|
|
67
|
+
operation: Type of operation (read, write, create, delete, list, exists)
|
|
68
|
+
file_path: Path to the file or directory
|
|
69
|
+
content: Content to write (required for write/create operations)
|
|
70
|
+
encoding: File encoding for read/write operations
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
ToolResult with operation status and output/error information
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
# Validate file path is provided and not empty
|
|
77
|
+
if not file_path.strip():
|
|
78
|
+
return ToolResult(
|
|
79
|
+
tool_name=self.name, status=ExecutionStatus.ERROR, output='', error='No file path provided'
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
# Route to appropriate operation handler
|
|
84
|
+
if operation == 'read':
|
|
85
|
+
return await self._read_file(sandbox_context, file_path, encoding)
|
|
86
|
+
elif operation == 'write':
|
|
87
|
+
# Write operation requires content parameter
|
|
88
|
+
if content is None:
|
|
89
|
+
return ToolResult(
|
|
90
|
+
tool_name=self.name,
|
|
91
|
+
status=ExecutionStatus.ERROR,
|
|
92
|
+
output='',
|
|
93
|
+
error='Content is required for write operation'
|
|
94
|
+
)
|
|
95
|
+
return await self._write_file(sandbox_context, file_path, content, encoding)
|
|
96
|
+
elif operation == 'delete':
|
|
97
|
+
return await self._delete_file(sandbox_context, file_path)
|
|
98
|
+
elif operation == 'list':
|
|
99
|
+
return await self._list_directory(sandbox_context, file_path)
|
|
100
|
+
elif operation == 'exists':
|
|
101
|
+
return await self._check_exists(sandbox_context, file_path)
|
|
102
|
+
elif operation == 'create':
|
|
103
|
+
# Create operation with empty content if none provided
|
|
104
|
+
if content is None:
|
|
105
|
+
content = ''
|
|
106
|
+
return await self._write_file(sandbox_context, file_path, content, encoding)
|
|
107
|
+
else:
|
|
108
|
+
return ToolResult(
|
|
109
|
+
tool_name=self.name,
|
|
110
|
+
status=ExecutionStatus.ERROR,
|
|
111
|
+
output='',
|
|
112
|
+
error=f'Unknown operation: {operation}'
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
except Exception as e:
|
|
116
|
+
# Catch-all error handler for unexpected exceptions
|
|
117
|
+
return ToolResult(
|
|
118
|
+
tool_name=self.name, status=ExecutionStatus.ERROR, output='', error=f'Operation failed: {str(e)}'
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
async def _read_file(self, sandbox_context: 'Sandbox', file_path: str, encoding: str) -> ToolResult:
|
|
122
|
+
"""Read file content from the container using cat command.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
sandbox_context: Sandbox instance
|
|
126
|
+
file_path: Path to file to read
|
|
127
|
+
encoding: File encoding (currently not used by cat command)
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
ToolResult with file content or error message
|
|
131
|
+
"""
|
|
132
|
+
try:
|
|
133
|
+
# Use cat command to read file content - handles most file types well
|
|
134
|
+
result = await sandbox_context.execute_command(f'cat "{file_path}"')
|
|
135
|
+
|
|
136
|
+
if result.exit_code == 0:
|
|
137
|
+
return ToolResult(tool_name=self.name, status=ExecutionStatus.SUCCESS, output=result.stdout, error=None)
|
|
138
|
+
else:
|
|
139
|
+
return ToolResult(
|
|
140
|
+
tool_name=self.name,
|
|
141
|
+
status=ExecutionStatus.ERROR,
|
|
142
|
+
output='',
|
|
143
|
+
error=result.stderr or f'Failed to read file: {file_path}'
|
|
144
|
+
)
|
|
145
|
+
except Exception as e:
|
|
146
|
+
return ToolResult(
|
|
147
|
+
tool_name=self.name, status=ExecutionStatus.ERROR, output='', error=f'Read failed: {str(e)}'
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
async def _write_file(self, sandbox_context: 'Sandbox', file_path: str, content: str, encoding: str) -> ToolResult:
|
|
151
|
+
"""Write content to a file in the container using temp file strategy.
|
|
152
|
+
|
|
153
|
+
This method uses a two-step process to avoid permission issues:
|
|
154
|
+
1. Write content to a temporary file in /tmp (always writable)
|
|
155
|
+
2. Copy the temp file to the target location using cp command
|
|
156
|
+
3. Clean up the temporary file
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
sandbox_context: Sandbox instance
|
|
160
|
+
file_path: Target file path to write to
|
|
161
|
+
content: Content to write to file
|
|
162
|
+
encoding: File encoding for content
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
ToolResult indicating success or failure
|
|
166
|
+
"""
|
|
167
|
+
try:
|
|
168
|
+
# Create target directory structure if it doesn't exist
|
|
169
|
+
dir_path = os.path.dirname(file_path)
|
|
170
|
+
if dir_path:
|
|
171
|
+
await sandbox_context.execute_command(f'mkdir -p "{dir_path}"')
|
|
172
|
+
|
|
173
|
+
# Generate unique temporary file name to avoid conflicts
|
|
174
|
+
temp_file = f'/tmp/file_op_{uuid.uuid4().hex}'
|
|
175
|
+
|
|
176
|
+
# Step 1: Write content to temporary location using tar archive
|
|
177
|
+
await self._write_file_to_container(sandbox_context, temp_file, content, encoding)
|
|
178
|
+
|
|
179
|
+
# Step 2: Copy from temp to target location using cp command
|
|
180
|
+
# This handles permission issues better than direct tar extraction
|
|
181
|
+
copy_result = await sandbox_context.execute_command(f'cp "{temp_file}" "{file_path}"')
|
|
182
|
+
|
|
183
|
+
# Step 3: Clean up temporary file regardless of copy result
|
|
184
|
+
await sandbox_context.execute_command(f'rm -f "{temp_file}"')
|
|
185
|
+
|
|
186
|
+
# Check if copy operation succeeded
|
|
187
|
+
if copy_result.exit_code != 0:
|
|
188
|
+
return ToolResult(
|
|
189
|
+
tool_name=self.name,
|
|
190
|
+
status=ExecutionStatus.ERROR,
|
|
191
|
+
output='',
|
|
192
|
+
error=f'Failed to copy file to target location: {copy_result.stderr or "Unknown error"}'
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
return ToolResult(
|
|
196
|
+
tool_name=self.name,
|
|
197
|
+
status=ExecutionStatus.SUCCESS,
|
|
198
|
+
output=f'File written successfully: {file_path}',
|
|
199
|
+
error=None
|
|
200
|
+
)
|
|
201
|
+
except Exception as e:
|
|
202
|
+
return ToolResult(
|
|
203
|
+
tool_name=self.name, status=ExecutionStatus.ERROR, output='', error=f'Write failed: {str(e)}'
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
async def _delete_file(self, sandbox_context: 'Sandbox', file_path: str) -> ToolResult:
|
|
207
|
+
"""Delete a file or directory from the container.
|
|
208
|
+
|
|
209
|
+
Uses 'rm -rf' to handle both files and directories recursively.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
sandbox_context: Sandbox instance
|
|
213
|
+
file_path: Path to file or directory to delete
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
ToolResult indicating success or failure
|
|
217
|
+
"""
|
|
218
|
+
try:
|
|
219
|
+
# Use rm -rf to handle both files and directories
|
|
220
|
+
result = await sandbox_context.execute_command(f'rm -rf "{file_path}"')
|
|
221
|
+
|
|
222
|
+
if result.exit_code == 0:
|
|
223
|
+
return ToolResult(
|
|
224
|
+
tool_name=self.name,
|
|
225
|
+
status=ExecutionStatus.SUCCESS,
|
|
226
|
+
output=f'Successfully deleted: {file_path}',
|
|
227
|
+
error=None
|
|
228
|
+
)
|
|
229
|
+
else:
|
|
230
|
+
return ToolResult(
|
|
231
|
+
tool_name=self.name,
|
|
232
|
+
status=ExecutionStatus.ERROR,
|
|
233
|
+
output='',
|
|
234
|
+
error=result.stderr or f'Failed to delete: {file_path}'
|
|
235
|
+
)
|
|
236
|
+
except Exception as e:
|
|
237
|
+
return ToolResult(
|
|
238
|
+
tool_name=self.name, status=ExecutionStatus.ERROR, output='', error=f'Delete failed: {str(e)}'
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
async def _list_directory(self, sandbox_context: 'Sandbox', dir_path: str) -> ToolResult:
|
|
242
|
+
"""List contents of a directory with detailed information.
|
|
243
|
+
|
|
244
|
+
Uses 'ls -la' to show permissions, ownership, size, and timestamps.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
sandbox_context: Sandbox instance
|
|
248
|
+
dir_path: Path to directory to list
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
ToolResult with directory listing or error message
|
|
252
|
+
"""
|
|
253
|
+
try:
|
|
254
|
+
# Use ls -la for detailed directory listing
|
|
255
|
+
result = await sandbox_context.execute_command(f'ls -la "{dir_path}"')
|
|
256
|
+
|
|
257
|
+
if result.exit_code == 0:
|
|
258
|
+
return ToolResult(tool_name=self.name, status=ExecutionStatus.SUCCESS, output=result.stdout, error=None)
|
|
259
|
+
else:
|
|
260
|
+
return ToolResult(
|
|
261
|
+
tool_name=self.name,
|
|
262
|
+
status=ExecutionStatus.ERROR,
|
|
263
|
+
output='',
|
|
264
|
+
error=result.stderr or f'Failed to list directory: {dir_path}'
|
|
265
|
+
)
|
|
266
|
+
except Exception as e:
|
|
267
|
+
return ToolResult(
|
|
268
|
+
tool_name=self.name, status=ExecutionStatus.ERROR, output='', error=f'List failed: {str(e)}'
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
async def _check_exists(self, sandbox_context: 'Sandbox', file_path: str) -> ToolResult:
|
|
272
|
+
"""Check if a file or directory exists.
|
|
273
|
+
|
|
274
|
+
Uses 'test -e' command which returns exit code 0 if path exists.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
sandbox_context: Sandbox instance
|
|
278
|
+
file_path: Path to check for existence
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
ToolResult with existence status message
|
|
282
|
+
"""
|
|
283
|
+
try:
|
|
284
|
+
# Use test -e to check existence (works for files and directories)
|
|
285
|
+
result = await sandbox_context.execute_command(f'test -e "{file_path}"')
|
|
286
|
+
|
|
287
|
+
# test command returns 0 if path exists, non-zero otherwise
|
|
288
|
+
exists = result.exit_code == 0
|
|
289
|
+
return ToolResult(
|
|
290
|
+
tool_name=self.name,
|
|
291
|
+
status=ExecutionStatus.SUCCESS,
|
|
292
|
+
output=f'{"exists" if exists else "does not exist"}',
|
|
293
|
+
error=None
|
|
294
|
+
)
|
|
295
|
+
except Exception as e:
|
|
296
|
+
return ToolResult(
|
|
297
|
+
tool_name=self.name, status=ExecutionStatus.ERROR, output='', error=f'Exists check failed: {str(e)}'
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
async def _write_file_to_container(
|
|
301
|
+
self, sandbox_context: 'Sandbox', file_path: str, content: str, encoding: str
|
|
302
|
+
) -> None:
|
|
303
|
+
"""Write content to a file in the container using tar archive method.
|
|
304
|
+
|
|
305
|
+
This is a low-level method that creates a tar archive containing the file
|
|
306
|
+
and extracts it to the container. Used internally by _write_file.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
sandbox_context: Sandbox instance with container access
|
|
310
|
+
file_path: Target file path in container
|
|
311
|
+
content: File content as string
|
|
312
|
+
encoding: Text encoding for content conversion
|
|
313
|
+
|
|
314
|
+
Raises:
|
|
315
|
+
Exception: If tar creation or container extraction fails
|
|
316
|
+
"""
|
|
317
|
+
# Create a tar archive in memory to transfer file content
|
|
318
|
+
tar_stream = io.BytesIO()
|
|
319
|
+
tar = tarfile.TarFile(fileobj=tar_stream, mode='w')
|
|
320
|
+
|
|
321
|
+
# Encode content using specified encoding and create tar entry
|
|
322
|
+
file_data = content.encode(encoding)
|
|
323
|
+
tarinfo = tarfile.TarInfo(name=os.path.basename(file_path))
|
|
324
|
+
tarinfo.size = len(file_data)
|
|
325
|
+
tar.addfile(tarinfo, io.BytesIO(file_data))
|
|
326
|
+
tar.close()
|
|
327
|
+
|
|
328
|
+
# Extract tar archive to container filesystem
|
|
329
|
+
# Note: This writes to the directory containing the target file
|
|
330
|
+
tar_stream.seek(0)
|
|
331
|
+
sandbox_context.container.put_archive(os.path.dirname(file_path) or '/', tar_stream.getvalue())
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Notebook code execution tool for Jupyter kernels."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
import uuid
|
|
6
|
+
from typing import TYPE_CHECKING, Optional
|
|
7
|
+
|
|
8
|
+
from ms_enclave.sandbox.model import CommandResult, ExecutionStatus, SandboxType, ToolResult
|
|
9
|
+
from ms_enclave.sandbox.tools.base import Tool, register_tool
|
|
10
|
+
from ms_enclave.sandbox.tools.sandbox_tool import SandboxTool
|
|
11
|
+
from ms_enclave.sandbox.tools.tool_info import ToolParams
|
|
12
|
+
from ms_enclave.utils import get_logger
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from ms_enclave.sandbox.boxes import Sandbox
|
|
16
|
+
|
|
17
|
+
logger = get_logger()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@register_tool('notebook_executor')
|
|
21
|
+
class NotebookExecutor(SandboxTool):
|
|
22
|
+
|
|
23
|
+
_name = 'notebook_executor'
|
|
24
|
+
_sandbox_type = SandboxType.DOCKER_NOTEBOOK
|
|
25
|
+
_description = 'Execute Python code in a Jupyter kernel environment'
|
|
26
|
+
_parameters = ToolParams(
|
|
27
|
+
type='object',
|
|
28
|
+
properties={
|
|
29
|
+
'code': {
|
|
30
|
+
'type': 'string',
|
|
31
|
+
'description': 'Python code to execute in the notebook kernel'
|
|
32
|
+
},
|
|
33
|
+
'timeout': {
|
|
34
|
+
'type': 'integer',
|
|
35
|
+
'description': 'Execution timeout in seconds',
|
|
36
|
+
'default': 30
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
required=['code']
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
async def execute(self, sandbox_context: 'Sandbox', code: str, timeout: Optional[int] = 30) -> ToolResult:
|
|
43
|
+
"""Execute Python code in the Jupyter kernel."""
|
|
44
|
+
|
|
45
|
+
if not code.strip():
|
|
46
|
+
return ToolResult(tool_name=self.name, status=ExecutionStatus.ERROR, output='', error='No code provided')
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
# Execute code using the sandbox's Jupyter kernel
|
|
50
|
+
result = await self._execute_in_kernel(sandbox_context, code, timeout)
|
|
51
|
+
|
|
52
|
+
if result.exit_code == 0:
|
|
53
|
+
status = ExecutionStatus.SUCCESS
|
|
54
|
+
else:
|
|
55
|
+
status = ExecutionStatus.ERROR
|
|
56
|
+
|
|
57
|
+
return ToolResult(
|
|
58
|
+
tool_name=self.name,
|
|
59
|
+
status=status,
|
|
60
|
+
output=result.stdout,
|
|
61
|
+
error=result.stderr if result.stderr else None
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
except Exception as e:
|
|
65
|
+
return ToolResult(
|
|
66
|
+
tool_name=self.name, status=ExecutionStatus.ERROR, output='', error=f'Execution failed: {str(e)}'
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
async def _execute_in_kernel(self, sandbox_context: 'Sandbox', code: str, timeout: Optional[int]):
|
|
70
|
+
"""Execute code in the Jupyter kernel via websocket."""
|
|
71
|
+
|
|
72
|
+
# Check if sandbox has the required Jupyter components
|
|
73
|
+
if not hasattr(sandbox_context, 'ws') or not hasattr(sandbox_context, 'kernel_id'):
|
|
74
|
+
raise RuntimeError('Sandbox does not have Jupyter kernel setup')
|
|
75
|
+
|
|
76
|
+
if not sandbox_context.ws or not sandbox_context.kernel_id:
|
|
77
|
+
raise RuntimeError('Jupyter kernel is not ready')
|
|
78
|
+
|
|
79
|
+
# Send execute request
|
|
80
|
+
msg_id = self._send_execute_request(sandbox_context, code)
|
|
81
|
+
|
|
82
|
+
outputs = []
|
|
83
|
+
result = None
|
|
84
|
+
error_occurred = False
|
|
85
|
+
error_msg = ''
|
|
86
|
+
actual_timeout = timeout or 30
|
|
87
|
+
start_time = time.time()
|
|
88
|
+
|
|
89
|
+
while True:
|
|
90
|
+
elapsed = time.time() - start_time
|
|
91
|
+
if elapsed >= actual_timeout:
|
|
92
|
+
error_occurred = True
|
|
93
|
+
error_msg = f'Execution timed out after {actual_timeout} seconds'
|
|
94
|
+
logger.error(error_msg)
|
|
95
|
+
break
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
# Set a short timeout for recv to allow periodic timeout check
|
|
99
|
+
sandbox_context.ws.settimeout(min(1, actual_timeout - elapsed))
|
|
100
|
+
msg = json.loads(sandbox_context.ws.recv())
|
|
101
|
+
parent_msg_id = msg.get('parent_header', {}).get('msg_id')
|
|
102
|
+
|
|
103
|
+
# Skip unrelated messages
|
|
104
|
+
if parent_msg_id != msg_id:
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
msg_type = msg.get('msg_type', '')
|
|
108
|
+
msg_content = msg.get('content', {})
|
|
109
|
+
|
|
110
|
+
if msg_type == 'stream':
|
|
111
|
+
outputs.append(msg_content['text'])
|
|
112
|
+
elif msg_type == 'execute_result':
|
|
113
|
+
result = msg_content['data'].get('text/plain', '')
|
|
114
|
+
elif msg_type == 'error':
|
|
115
|
+
error_occurred = True
|
|
116
|
+
error_msg = '\n'.join(msg_content.get('traceback', []))
|
|
117
|
+
outputs.append(error_msg)
|
|
118
|
+
elif msg_type == 'status' and msg_content.get('execution_state') == 'idle':
|
|
119
|
+
break
|
|
120
|
+
|
|
121
|
+
except Exception as e:
|
|
122
|
+
logger.error(f'Error receiving message: {e}')
|
|
123
|
+
error_occurred = True
|
|
124
|
+
error_msg = str(e)
|
|
125
|
+
break
|
|
126
|
+
|
|
127
|
+
output_text = ''.join(outputs)
|
|
128
|
+
if result:
|
|
129
|
+
output_text += f'\nResult: {result}'
|
|
130
|
+
if error_msg and error_msg not in output_text:
|
|
131
|
+
output_text += f'\n{error_msg}'
|
|
132
|
+
|
|
133
|
+
return CommandResult(
|
|
134
|
+
status=ExecutionStatus.SUCCESS if not error_occurred else ExecutionStatus.ERROR,
|
|
135
|
+
command=code,
|
|
136
|
+
exit_code=1 if error_occurred else 0,
|
|
137
|
+
stdout=output_text if not error_occurred else '',
|
|
138
|
+
stderr=output_text if error_occurred else ''
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def _send_execute_request(self, sandbox_context: 'Sandbox', code: str) -> str:
|
|
142
|
+
"""Send code execution request to kernel."""
|
|
143
|
+
# Generate a unique message ID
|
|
144
|
+
msg_id = str(uuid.uuid4())
|
|
145
|
+
|
|
146
|
+
# Create execute request
|
|
147
|
+
execute_request = {
|
|
148
|
+
'header': {
|
|
149
|
+
'msg_id': msg_id,
|
|
150
|
+
'username': 'anonymous',
|
|
151
|
+
'session': str(uuid.uuid4()),
|
|
152
|
+
'msg_type': 'execute_request',
|
|
153
|
+
'version': '5.0',
|
|
154
|
+
},
|
|
155
|
+
'parent_header': {},
|
|
156
|
+
'metadata': {},
|
|
157
|
+
'content': {
|
|
158
|
+
'code': code,
|
|
159
|
+
'silent': False,
|
|
160
|
+
'store_history': True,
|
|
161
|
+
'user_expressions': {},
|
|
162
|
+
'allow_stdin': False,
|
|
163
|
+
},
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
sandbox_context.ws.send(json.dumps(execute_request))
|
|
167
|
+
return msg_id
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Python code execution tool."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import uuid
|
|
5
|
+
from typing import TYPE_CHECKING, Optional
|
|
6
|
+
|
|
7
|
+
from ms_enclave.sandbox.model import ExecutionStatus, SandboxType, ToolResult
|
|
8
|
+
from ms_enclave.sandbox.tools.base import Tool, register_tool
|
|
9
|
+
from ms_enclave.sandbox.tools.sandbox_tool import SandboxTool
|
|
10
|
+
from ms_enclave.sandbox.tools.tool_info import ToolParams
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from ms_enclave.sandbox.boxes import DockerSandbox
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@register_tool('python_executor')
|
|
17
|
+
class PythonExecutor(SandboxTool):
|
|
18
|
+
|
|
19
|
+
_name = 'python_executor'
|
|
20
|
+
_sandbox_type = SandboxType.DOCKER
|
|
21
|
+
_description = 'Execute Python code in an isolated environment using IPython'
|
|
22
|
+
_parameters = ToolParams(
|
|
23
|
+
type='object',
|
|
24
|
+
properties={
|
|
25
|
+
'code': {
|
|
26
|
+
'type': 'string',
|
|
27
|
+
'description': 'Python code to execute'
|
|
28
|
+
},
|
|
29
|
+
'timeout': {
|
|
30
|
+
'type': 'integer',
|
|
31
|
+
'description': 'Execution timeout in seconds',
|
|
32
|
+
'default': 30
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
required=['code']
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
async def execute(self, sandbox_context: 'DockerSandbox', code: str, timeout: Optional[int] = 30) -> ToolResult:
|
|
39
|
+
"""Execute Python code by writing to a temporary file and executing it."""
|
|
40
|
+
|
|
41
|
+
script_basename = f'exec_script_{uuid.uuid4().hex}.py'
|
|
42
|
+
script_path = f'/tmp/{script_basename}'
|
|
43
|
+
|
|
44
|
+
if not code.strip():
|
|
45
|
+
return ToolResult(tool_name=self.name, status=ExecutionStatus.ERROR, output='', error='No code provided')
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
|
|
49
|
+
# Write script to container to avoid long code errors
|
|
50
|
+
await self._write_file_to_container(sandbox_context, script_path, code)
|
|
51
|
+
|
|
52
|
+
# Execute using python
|
|
53
|
+
command = f'python {script_path}'
|
|
54
|
+
result = await sandbox_context.execute_command(command, timeout=timeout)
|
|
55
|
+
|
|
56
|
+
if result.exit_code == 0:
|
|
57
|
+
status = ExecutionStatus.SUCCESS
|
|
58
|
+
else:
|
|
59
|
+
status = ExecutionStatus.ERROR
|
|
60
|
+
|
|
61
|
+
return ToolResult(
|
|
62
|
+
tool_name=self.name,
|
|
63
|
+
status=status,
|
|
64
|
+
output=result.stdout,
|
|
65
|
+
error=result.stderr if result.stderr else None
|
|
66
|
+
)
|
|
67
|
+
except Exception as e:
|
|
68
|
+
return ToolResult(
|
|
69
|
+
tool_name=self.name, status=ExecutionStatus.ERROR, output='', error=f'Execution failed: {str(e)}'
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
async def _write_file_to_container(self, sandbox_context: 'DockerSandbox', file_path: str, content: str) -> None:
|
|
73
|
+
"""Write content to a file in the container."""
|
|
74
|
+
import io
|
|
75
|
+
import tarfile
|
|
76
|
+
|
|
77
|
+
# Create a tar archive in memory using context managers
|
|
78
|
+
with io.BytesIO() as tar_stream:
|
|
79
|
+
with tarfile.TarFile(fileobj=tar_stream, mode='w') as tar:
|
|
80
|
+
file_data = content.encode('utf-8')
|
|
81
|
+
tarinfo = tarfile.TarInfo(name=os.path.basename(file_path))
|
|
82
|
+
tarinfo.size = len(file_data)
|
|
83
|
+
tar.addfile(tarinfo, io.BytesIO(file_data))
|
|
84
|
+
|
|
85
|
+
# Reset stream position and put archive into container
|
|
86
|
+
tar_stream.seek(0)
|
|
87
|
+
sandbox_context.container.put_archive(os.path.dirname(file_path), tar_stream.getvalue())
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Shell command execution tool."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, List, Optional, Union
|
|
4
|
+
|
|
5
|
+
from ms_enclave.sandbox.model import ExecutionStatus, SandboxType, ToolResult
|
|
6
|
+
from ms_enclave.sandbox.tools.base import register_tool
|
|
7
|
+
from ms_enclave.sandbox.tools.sandbox_tool import SandboxTool
|
|
8
|
+
from ms_enclave.sandbox.tools.tool_info import ToolParams
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from ms_enclave.sandbox.boxes import Sandbox
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@register_tool('shell_executor')
|
|
15
|
+
class ShellExecutor(SandboxTool):
|
|
16
|
+
|
|
17
|
+
_name = 'shell_executor'
|
|
18
|
+
_sandbox_type = SandboxType.DOCKER
|
|
19
|
+
_description = 'Execute shell commands in an isolated environment'
|
|
20
|
+
_parameters = ToolParams(
|
|
21
|
+
type='object',
|
|
22
|
+
properties={
|
|
23
|
+
'command': {
|
|
24
|
+
'anyOf': [{
|
|
25
|
+
'type': 'string',
|
|
26
|
+
'description': 'Shell command to execute'
|
|
27
|
+
}, {
|
|
28
|
+
'type': 'array',
|
|
29
|
+
'items': {
|
|
30
|
+
'type': 'string'
|
|
31
|
+
},
|
|
32
|
+
'description': 'List of shell command arguments to execute'
|
|
33
|
+
}]
|
|
34
|
+
},
|
|
35
|
+
'timeout': {
|
|
36
|
+
'type': 'integer',
|
|
37
|
+
'description': 'Execution timeout in seconds',
|
|
38
|
+
'default': 30
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
required=['command']
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
async def execute(
|
|
45
|
+
self, sandbox_context: 'Sandbox', command: Union[str, List[str]], timeout: Optional[int] = 30
|
|
46
|
+
) -> ToolResult:
|
|
47
|
+
"""Execute shell command in the Docker container."""
|
|
48
|
+
|
|
49
|
+
if not command or (isinstance(command, str) and not command.strip()):
|
|
50
|
+
return ToolResult(tool_name=self.name, status=ExecutionStatus.ERROR, output='', error='No command provided')
|
|
51
|
+
try:
|
|
52
|
+
result = await sandbox_context.execute_command(command, timeout=timeout)
|
|
53
|
+
|
|
54
|
+
if result.exit_code == 0:
|
|
55
|
+
return ToolResult(
|
|
56
|
+
tool_name=self.name,
|
|
57
|
+
status=ExecutionStatus.SUCCESS,
|
|
58
|
+
output=result.stdout,
|
|
59
|
+
error=result.stderr if result.stderr else None
|
|
60
|
+
)
|
|
61
|
+
else:
|
|
62
|
+
return ToolResult(
|
|
63
|
+
tool_name=self.name,
|
|
64
|
+
status=ExecutionStatus.ERROR,
|
|
65
|
+
output=result.stdout,
|
|
66
|
+
error=result.stderr if result.stderr else f'Command failed with exit code {result.exit_code}'
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
except Exception as e:
|
|
70
|
+
return ToolResult(
|
|
71
|
+
tool_name=self.name, status=ExecutionStatus.ERROR, output='', error=f'Execution failed: {str(e)}'
|
|
72
|
+
)
|