mic3333 commited on
Commit
d1ae699
·
verified ·
1 Parent(s): eebd9e7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +483 -0
app.py CHANGED
@@ -1,3 +1,477 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  # Replace the gradio_execute_code function with this improved version
2
  def gradio_execute_code(code: str, language: str, session_id: str = ""):
3
  """Gradio interface for code execution with proper image handling"""
@@ -128,4 +602,13 @@ console.log(JSON.stringify(result, null, 2));
128
  fn=gradio_execute_code,
129
  inputs=[code_input, language_dropdown, session_input],
130
  outputs=[output_display, plot_display] # Now returns both text and image
 
 
 
 
 
 
 
 
 
131
  )
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ LibreChat Custom Code Interpreter - HuggingFace Space
4
+ A secure code execution server using Gradio + MCP with SSE support
5
+ """
6
+
7
+ import json
8
+ import subprocess
9
+ import tempfile
10
+ import os
11
+ import sys
12
+ import uuid
13
+ import time
14
+ import base64
15
+ import io
16
+ import re
17
+ from pathlib import Path
18
+ from typing import Dict, Any, Optional, List
19
+ import gradio as gr
20
+ import matplotlib
21
+ matplotlib.use('Agg') # Use non-interactive backend
22
+ import matplotlib.pyplot as plt
23
+ import numpy as np
24
+ import pandas as pd
25
+ import base64
26
+ from PIL import Image
27
+ import io
28
+
29
+ class SecureCodeExecutor:
30
+ def __init__(self):
31
+ self.sessions = {}
32
+ self.max_execution_time = 30
33
+ self.max_output_length = 10000
34
+ self.allowed_languages = ["python", "javascript", "bash"]
35
+
36
+ # Security: List of blocked commands/imports
37
+ self.blocked_imports = [
38
+ 'subprocess', 'os', 'sys', 'shutil', 'glob', 'pickle',
39
+ 'marshal', 'imp', 'importlib', '__import__'
40
+ ]
41
+ self.blocked_bash_commands = [
42
+ 'rm', 'sudo', 'chmod', 'chown', 'dd', 'mkfs', 'fdisk',
43
+ 'curl', 'wget', 'ssh', 'scp', 'nc', 'netcat'
44
+ ]
45
+
46
+ def create_session(self) -> str:
47
+ """Create a new execution session"""
48
+ session_id = str(uuid.uuid4())[:8] # Shorter ID for HF
49
+ self.sessions[session_id] = {
50
+ 'created_at': time.time(),
51
+ 'variables': {},
52
+ 'history': [],
53
+ 'files': {}
54
+ }
55
+ return session_id
56
+
57
+ def cleanup_old_sessions(self):
58
+ """Remove sessions older than 1 hour"""
59
+ current_time = time.time()
60
+ old_sessions = [
61
+ sid for sid, session in self.sessions.items()
62
+ if current_time - session['created_at'] > 3600
63
+ ]
64
+ for sid in old_sessions:
65
+ del self.sessions[sid]
66
+
67
+ def is_code_safe(self, code: str, language: str) -> tuple[bool, str]:
68
+ """Check if code is safe to execute using whole-word matching."""
69
+ if language == "python":
70
+ # Use regex to find whole-word matches for blocked imports
71
+ for blocked in self.blocked_imports:
72
+ # \b matches a word boundary. This prevents 'imp' from matching 'import'
73
+ pattern = r'\b' + re.escape(blocked) + r'\b'
74
+ if re.search(pattern, code):
75
+ return False, f"Blocked import/function: {blocked}"
76
+
77
+ # Check for dangerous patterns that are not whole words
78
+ dangerous_patterns = ['exec(', 'eval(', 'open(', 'file(', '__']
79
+ for pattern in dangerous_patterns:
80
+ if pattern in code:
81
+ return False, f"Dangerous pattern detected: {pattern}"
82
+
83
+ elif language == "bash":
84
+ # Use regex to find whole-word matches for blocked commands
85
+ for blocked in self.blocked_bash_commands:
86
+ pattern = r'\b' + re.escape(blocked) + r'\b'
87
+ if re.search(pattern, code.lower()):
88
+ return False, f"Blocked command: {blocked}"
89
+
90
+ return True, ""
91
+
92
+ def execute_python_code(self, code: str, session_id: Optional[str] = None) -> Dict[str, Any]:
93
+ """Execute Python code with visualization support"""
94
+ # Security check
95
+ is_safe, reason = self.is_code_safe(code, "python")
96
+ if not is_safe:
97
+ return {
98
+ "success": False,
99
+ "stdout": "",
100
+ "stderr": f"Security violation: {reason}",
101
+ "execution_time": time.time()
102
+ }
103
+
104
+ # Prepare execution environment
105
+ setup_code = '''
106
+ import matplotlib
107
+ matplotlib.use('Agg')
108
+ import matplotlib.pyplot as plt
109
+ import numpy as np
110
+ import pandas as pd
111
+ import json
112
+ import math
113
+ import random
114
+ import base64
115
+ import io
116
+ from datetime import datetime, timedelta
117
+
118
+ # Custom print function to capture output
119
+ _output_buffer = []
120
+ _original_print = print
121
+ def print(*args, **kwargs):
122
+ _output_buffer.append(' '.join(str(arg) for arg in args))
123
+
124
+ # Function to save plots as base64
125
+ def save_current_plot():
126
+ if plt.get_fignums(): # Check if there are any figures
127
+ buffer = io.BytesIO()
128
+ plt.savefig(buffer, format='png', bbox_inches='tight', dpi=100)
129
+ buffer.seek(0)
130
+ plot_data = buffer.getvalue()
131
+ buffer.close()
132
+ return base64.b64encode(plot_data).decode()
133
+ return None
134
+ '''
135
+
136
+ # Combine setup and user code
137
+ full_code = setup_code + "\n" + code
138
+
139
+ # Finalization code to capture plot and print the buffer to real stdout
140
+ finalization_code = '''
141
+ # Capture the plot if one exists and print it directly to stdout for the parent process
142
+ _plot_data = save_current_plot()
143
+ if _plot_data:
144
+ _original_print('PLOT_DATA:' + _plot_data)
145
+
146
+ # Print everything from the custom print buffer to stdout
147
+ if _output_buffer:
148
+ _original_print('\\n'.join(_output_buffer))
149
+ '''
150
+ full_code += "\n" + finalization_code
151
+
152
+ try:
153
+ # Create temporary file
154
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
155
+ f.write(full_code)
156
+ temp_file = f.name
157
+
158
+ # Execute with timeout
159
+ result = subprocess.run(
160
+ [sys.executable, temp_file],
161
+ capture_output=True,
162
+ text=True,
163
+ timeout=self.max_execution_time,
164
+ cwd=tempfile.gettempdir()
165
+ )
166
+
167
+ # Process output
168
+ stdout = result.stdout
169
+ stderr = result.stderr
170
+ plot_data = None
171
+
172
+ # Extract plot data if present
173
+ if 'PLOT_DATA:' in stdout:
174
+ lines = stdout.split('\n')
175
+ clean_lines = []
176
+ for line in lines:
177
+ if line.startswith('PLOT_DATA:'):
178
+ plot_data = line.replace('PLOT_DATA:', '')
179
+ else:
180
+ clean_lines.append(line)
181
+ stdout = '\n'.join(clean_lines)
182
+
183
+ # Limit output length
184
+ if len(stdout) > self.max_output_length:
185
+ stdout = stdout[:self.max_output_length] + "\n... (output truncated)"
186
+
187
+ execution_result = {
188
+ "success": result.returncode == 0,
189
+ "stdout": stdout.strip(),
190
+ "stderr": stderr.strip() if stderr else "",
191
+ "execution_time": time.time(),
192
+ "return_code": result.returncode
193
+ }
194
+
195
+ if plot_data:
196
+ execution_result["plot"] = plot_data
197
+
198
+ return execution_result
199
+
200
+ except subprocess.TimeoutExpired:
201
+ return {
202
+ "success": False,
203
+ "stdout": "",
204
+ "stderr": "Execution timed out (30s limit)",
205
+ "execution_time": time.time()
206
+ }
207
+ except Exception as e:
208
+ return {
209
+ "success": False,
210
+ "stdout": "",
211
+ "stderr": str(e),
212
+ "execution_time": time.time()
213
+ }
214
+ finally:
215
+ if 'temp_file' in locals():
216
+ try:
217
+ os.unlink(temp_file)
218
+ except:
219
+ pass
220
+
221
+ def execute_javascript_code(self, code: str, session_id: Optional[str] = None) -> Dict[str, Any]:
222
+ """Execute JavaScript code using Node.js"""
223
+ # Security check
224
+ is_safe, reason = self.is_code_safe(code, "javascript")
225
+ if not is_safe:
226
+ return {
227
+ "success": False,
228
+ "stdout": "",
229
+ "stderr": f"Security violation: {reason}",
230
+ "execution_time": time.time()
231
+ }
232
+
233
+ try:
234
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.js', delete=False) as f:
235
+ f.write(code)
236
+ temp_file = f.name
237
+
238
+ result = subprocess.run(
239
+ ['node', temp_file],
240
+ capture_output=True,
241
+ text=True,
242
+ timeout=self.max_execution_time
243
+ )
244
+
245
+ stdout = result.stdout
246
+ if len(stdout) > self.max_output_length:
247
+ stdout = stdout[:self.max_output_length] + "\n... (output truncated)"
248
+
249
+ return {
250
+ "success": result.returncode == 0,
251
+ "stdout": stdout.strip(),
252
+ "stderr": result.stderr.strip() if result.stderr else "",
253
+ "execution_time": time.time(),
254
+ "return_code": result.returncode
255
+ }
256
+
257
+ except subprocess.TimeoutExpired:
258
+ return {
259
+ "success": False,
260
+ "stdout": "",
261
+ "stderr": "Execution timed out (30s limit)",
262
+ "execution_time": time.time()
263
+ }
264
+ except Exception as e:
265
+ return {
266
+ "success": False,
267
+ "stdout": "",
268
+ "stderr": str(e),
269
+ "execution_time": time.time()
270
+ }
271
+ finally:
272
+ if 'temp_file' in locals():
273
+ try:
274
+ os.unlink(temp_file)
275
+ except:
276
+ pass
277
+
278
+ def execute_bash_command(self, command: str, session_id: Optional[str] = None) -> Dict[str, Any]:
279
+ """Execute bash commands with security restrictions"""
280
+ # Security check
281
+ is_safe, reason = self.is_code_safe(command, "bash")
282
+ if not is_safe:
283
+ return {
284
+ "success": False,
285
+ "stdout": "",
286
+ "stderr": f"Security violation: {reason}",
287
+ "execution_time": time.time()
288
+ }
289
+
290
+ try:
291
+ result = subprocess.run(
292
+ command,
293
+ shell=True,
294
+ capture_output=True,
295
+ text=True,
296
+ timeout=self.max_execution_time,
297
+ cwd=tempfile.gettempdir()
298
+ )
299
+
300
+ stdout = result.stdout
301
+ if len(stdout) > self.max_output_length:
302
+ stdout = stdout[:self.max_output_length] + "\n... (output truncated)"
303
+
304
+ return {
305
+ "success": result.returncode == 0,
306
+ "stdout": stdout.strip(),
307
+ "stderr": result.stderr.strip() if result.stderr else "",
308
+ "execution_time": time.time(),
309
+ "return_code": result.returncode
310
+ }
311
+
312
+ except subprocess.TimeoutExpired:
313
+ return {
314
+ "success": False,
315
+ "stdout": "",
316
+ "stderr": "Command timed out (30s limit)",
317
+ "execution_time": time.time()
318
+ }
319
+ except Exception as e:
320
+ return {
321
+ "success": False,
322
+ "stdout": "",
323
+ "stderr": str(e),
324
+ "execution_time": time.time()
325
+ }
326
+
327
+ def execute_code(self, code: str, language: str = "python", session_id: Optional[str] = None) -> str:
328
+ """Main execution function - returns JSON for MCP compatibility"""
329
+ # Cleanup old sessions periodically
330
+ if len(self.sessions) > 10:
331
+ self.cleanup_old_sessions()
332
+
333
+ if language not in self.allowed_languages:
334
+ return json.dumps({
335
+ "success": False,
336
+ "error": f"Language '{language}' not supported. Allowed: {', '.join(self.allowed_languages)}"
337
+ })
338
+
339
+ # Create session if needed
340
+ if session_id and session_id not in self.sessions:
341
+ session_id = self.create_session()
342
+ elif not session_id:
343
+ session_id = self.create_session()
344
+
345
+ # Execute based on language
346
+ if language == "python":
347
+ result = self.execute_python_code(code, session_id)
348
+ elif language == "javascript":
349
+ result = self.execute_javascript_code(code, session_id)
350
+ elif language == "bash":
351
+ result = self.execute_bash_command(code, session_id)
352
+ else:
353
+ result = {
354
+ "success": False,
355
+ "error": f"Execution handler for {language} not implemented"
356
+ }
357
+
358
+ # Store in session history
359
+ if session_id in self.sessions:
360
+ self.sessions[session_id]['history'].append({
361
+ 'code': code,
362
+ 'language': language,
363
+ 'result': result,
364
+ 'timestamp': time.time()
365
+ })
366
+
367
+ result['session_id'] = session_id
368
+ return json.dumps(result, indent=2)
369
+
370
+ # Global executor instance
371
+ executor = SecureCodeExecutor()
372
+
373
+ # MCP Functions
374
+ def execute_python_code(code: str, session_id: str = None) -> str:
375
+ """
376
+ Execute Python code safely with visualization support.
377
+
378
+ Args:
379
+ code (str): Python code to execute
380
+ session_id (str, optional): Session ID for persistent context
381
+
382
+ Returns:
383
+ str: JSON string with execution results
384
+ """
385
+ return executor.execute_code(code, "python", session_id)
386
+
387
+ def execute_javascript_code(code: str, session_id: str = None) -> str:
388
+ """
389
+ Execute JavaScript code using Node.js.
390
+
391
+ Args:
392
+ code (str): JavaScript code to execute
393
+ session_id (str, optional): Session ID for persistent context
394
+
395
+ Returns:
396
+ str: JSON string with execution results
397
+ """
398
+ return executor.execute_code(code, "javascript", session_id)
399
+
400
+ def execute_bash_command(command: str, session_id: str = None) -> str:
401
+ """
402
+ Execute bash commands with security restrictions.
403
+
404
+ Args:
405
+ command (str): Bash command to execute
406
+ session_id (str, optional): Session ID for persistent context
407
+
408
+ Returns:
409
+ str: JSON string with execution results
410
+ """
411
+ return executor.execute_code(command, "bash", session_id)
412
+
413
+ def create_execution_session() -> str:
414
+ """
415
+ Create a new execution session for maintaining state.
416
+
417
+ Returns:
418
+ str: JSON string containing new session ID
419
+ """
420
+ session_id = executor.create_session()
421
+ return json.dumps({"session_id": session_id, "created_at": time.time()})
422
+
423
+ def list_execution_sessions() -> str:
424
+ """
425
+ List all active execution sessions.
426
+
427
+ Returns:
428
+ str: JSON string containing session information
429
+ """
430
+ return json.dumps({
431
+ "sessions": list(executor.sessions.keys()),
432
+ "count": len(executor.sessions),
433
+ "timestamp": time.time()
434
+ })
435
+
436
+ def get_execution_history(session_id: str) -> str:
437
+ """
438
+ Get execution history for a specific session.
439
+
440
+ Args:
441
+ session_id (str): Session ID to get history for
442
+
443
+ Returns:
444
+ str: JSON string containing execution history
445
+ """
446
+ if session_id not in executor.sessions:
447
+ return json.dumps({"error": "Session not found"})
448
+
449
+ return json.dumps({
450
+ "session_id": session_id,
451
+ "history": executor.sessions[session_id]['history'],
452
+ "created_at": executor.sessions[session_id]['created_at']
453
+ })
454
+
455
+ def get_system_info() -> str:
456
+ """
457
+ Get system information and available packages.
458
+
459
+ Returns:
460
+ str: JSON string containing system information
461
+ """
462
+ return json.dumps({
463
+ "python_version": sys.version,
464
+ "available_packages": [
465
+ "numpy", "pandas", "matplotlib", "json", "math",
466
+ "random", "datetime", "base64", "io"
467
+ ],
468
+ "execution_limits": {
469
+ "max_time": executor.max_execution_time,
470
+ "max_output": executor.max_output_length
471
+ },
472
+ "supported_languages": executor.allowed_languages
473
+ })
474
+
475
  # Replace the gradio_execute_code function with this improved version
476
  def gradio_execute_code(code: str, language: str, session_id: str = ""):
477
  """Gradio interface for code execution with proper image handling"""
 
602
  fn=gradio_execute_code,
603
  inputs=[code_input, language_dropdown, session_input],
604
  outputs=[output_display, plot_display] # Now returns both text and image
605
+ )
606
+
607
+ if __name__ == "__main__":
608
+ # Launch with MCP server enabled
609
+ demo.launch(
610
+ mcp_server=True,
611
+ share=False,
612
+ server_name="0.0.0.0",
613
+ server_port=7860
614
  )