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.
Files changed (40) hide show
  1. ms_enclave/__init__.py +3 -0
  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 +274 -0
  9. ms_enclave/sandbox/boxes/docker_notebook.py +224 -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 +75 -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 +119 -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 +72 -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 -0
  35. ms_enclave-0.0.3.dist-info/METADATA +370 -0
  36. ms_enclave-0.0.3.dist-info/RECORD +40 -0
  37. ms_enclave-0.0.3.dist-info/WHEEL +5 -0
  38. ms_enclave-0.0.3.dist-info/entry_points.txt +2 -0
  39. ms_enclave-0.0.3.dist-info/licenses/LICENSE +201 -0
  40. 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
+ )