atlas-chat 0.1.0__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 (250) hide show
  1. atlas/__init__.py +40 -0
  2. atlas/application/__init__.py +7 -0
  3. atlas/application/chat/__init__.py +7 -0
  4. atlas/application/chat/agent/__init__.py +10 -0
  5. atlas/application/chat/agent/act_loop.py +179 -0
  6. atlas/application/chat/agent/factory.py +142 -0
  7. atlas/application/chat/agent/protocols.py +46 -0
  8. atlas/application/chat/agent/react_loop.py +338 -0
  9. atlas/application/chat/agent/think_act_loop.py +171 -0
  10. atlas/application/chat/approval_manager.py +151 -0
  11. atlas/application/chat/elicitation_manager.py +191 -0
  12. atlas/application/chat/events/__init__.py +1 -0
  13. atlas/application/chat/events/agent_event_relay.py +112 -0
  14. atlas/application/chat/modes/__init__.py +1 -0
  15. atlas/application/chat/modes/agent.py +125 -0
  16. atlas/application/chat/modes/plain.py +74 -0
  17. atlas/application/chat/modes/rag.py +81 -0
  18. atlas/application/chat/modes/tools.py +179 -0
  19. atlas/application/chat/orchestrator.py +213 -0
  20. atlas/application/chat/policies/__init__.py +1 -0
  21. atlas/application/chat/policies/tool_authorization.py +99 -0
  22. atlas/application/chat/preprocessors/__init__.py +1 -0
  23. atlas/application/chat/preprocessors/message_builder.py +92 -0
  24. atlas/application/chat/preprocessors/prompt_override_service.py +104 -0
  25. atlas/application/chat/service.py +454 -0
  26. atlas/application/chat/utilities/__init__.py +6 -0
  27. atlas/application/chat/utilities/error_handler.py +367 -0
  28. atlas/application/chat/utilities/event_notifier.py +546 -0
  29. atlas/application/chat/utilities/file_processor.py +613 -0
  30. atlas/application/chat/utilities/tool_executor.py +789 -0
  31. atlas/atlas_chat_cli.py +347 -0
  32. atlas/atlas_client.py +238 -0
  33. atlas/core/__init__.py +0 -0
  34. atlas/core/auth.py +205 -0
  35. atlas/core/authorization_manager.py +27 -0
  36. atlas/core/capabilities.py +123 -0
  37. atlas/core/compliance.py +215 -0
  38. atlas/core/domain_whitelist.py +147 -0
  39. atlas/core/domain_whitelist_middleware.py +82 -0
  40. atlas/core/http_client.py +28 -0
  41. atlas/core/log_sanitizer.py +102 -0
  42. atlas/core/metrics_logger.py +59 -0
  43. atlas/core/middleware.py +131 -0
  44. atlas/core/otel_config.py +242 -0
  45. atlas/core/prompt_risk.py +200 -0
  46. atlas/core/rate_limit.py +0 -0
  47. atlas/core/rate_limit_middleware.py +64 -0
  48. atlas/core/security_headers_middleware.py +51 -0
  49. atlas/domain/__init__.py +37 -0
  50. atlas/domain/chat/__init__.py +1 -0
  51. atlas/domain/chat/dtos.py +85 -0
  52. atlas/domain/errors.py +96 -0
  53. atlas/domain/messages/__init__.py +12 -0
  54. atlas/domain/messages/models.py +160 -0
  55. atlas/domain/rag_mcp_service.py +664 -0
  56. atlas/domain/sessions/__init__.py +7 -0
  57. atlas/domain/sessions/models.py +36 -0
  58. atlas/domain/unified_rag_service.py +371 -0
  59. atlas/infrastructure/__init__.py +10 -0
  60. atlas/infrastructure/app_factory.py +135 -0
  61. atlas/infrastructure/events/__init__.py +1 -0
  62. atlas/infrastructure/events/cli_event_publisher.py +140 -0
  63. atlas/infrastructure/events/websocket_publisher.py +140 -0
  64. atlas/infrastructure/sessions/in_memory_repository.py +56 -0
  65. atlas/infrastructure/transport/__init__.py +7 -0
  66. atlas/infrastructure/transport/websocket_connection_adapter.py +33 -0
  67. atlas/init_cli.py +226 -0
  68. atlas/interfaces/__init__.py +15 -0
  69. atlas/interfaces/events.py +134 -0
  70. atlas/interfaces/llm.py +54 -0
  71. atlas/interfaces/rag.py +40 -0
  72. atlas/interfaces/sessions.py +75 -0
  73. atlas/interfaces/tools.py +57 -0
  74. atlas/interfaces/transport.py +24 -0
  75. atlas/main.py +564 -0
  76. atlas/mcp/api_key_demo/README.md +76 -0
  77. atlas/mcp/api_key_demo/main.py +172 -0
  78. atlas/mcp/api_key_demo/run.sh +56 -0
  79. atlas/mcp/basictable/main.py +147 -0
  80. atlas/mcp/calculator/main.py +149 -0
  81. atlas/mcp/code-executor/execution_engine.py +98 -0
  82. atlas/mcp/code-executor/execution_environment.py +95 -0
  83. atlas/mcp/code-executor/main.py +528 -0
  84. atlas/mcp/code-executor/result_processing.py +276 -0
  85. atlas/mcp/code-executor/script_generation.py +195 -0
  86. atlas/mcp/code-executor/security_checker.py +140 -0
  87. atlas/mcp/corporate_cars/main.py +437 -0
  88. atlas/mcp/csv_reporter/main.py +545 -0
  89. atlas/mcp/duckduckgo/main.py +182 -0
  90. atlas/mcp/elicitation_demo/README.md +171 -0
  91. atlas/mcp/elicitation_demo/main.py +262 -0
  92. atlas/mcp/env-demo/README.md +158 -0
  93. atlas/mcp/env-demo/main.py +199 -0
  94. atlas/mcp/file_size_test/main.py +284 -0
  95. atlas/mcp/filesystem/main.py +348 -0
  96. atlas/mcp/image_demo/main.py +113 -0
  97. atlas/mcp/image_demo/requirements.txt +4 -0
  98. atlas/mcp/logging_demo/README.md +72 -0
  99. atlas/mcp/logging_demo/main.py +103 -0
  100. atlas/mcp/many_tools_demo/main.py +50 -0
  101. atlas/mcp/order_database/__init__.py +0 -0
  102. atlas/mcp/order_database/main.py +369 -0
  103. atlas/mcp/order_database/signal_data.csv +1001 -0
  104. atlas/mcp/pdfbasic/main.py +394 -0
  105. atlas/mcp/pptx_generator/main.py +760 -0
  106. atlas/mcp/pptx_generator/requirements.txt +13 -0
  107. atlas/mcp/pptx_generator/run_test.sh +1 -0
  108. atlas/mcp/pptx_generator/test_pptx_generator_security.py +169 -0
  109. atlas/mcp/progress_demo/main.py +167 -0
  110. atlas/mcp/progress_updates_demo/QUICKSTART.md +273 -0
  111. atlas/mcp/progress_updates_demo/README.md +120 -0
  112. atlas/mcp/progress_updates_demo/main.py +497 -0
  113. atlas/mcp/prompts/main.py +222 -0
  114. atlas/mcp/public_demo/main.py +189 -0
  115. atlas/mcp/sampling_demo/README.md +169 -0
  116. atlas/mcp/sampling_demo/main.py +234 -0
  117. atlas/mcp/thinking/main.py +77 -0
  118. atlas/mcp/tool_planner/main.py +240 -0
  119. atlas/mcp/ui-demo/badmesh.png +0 -0
  120. atlas/mcp/ui-demo/main.py +383 -0
  121. atlas/mcp/ui-demo/templates/button_demo.html +32 -0
  122. atlas/mcp/ui-demo/templates/data_visualization.html +32 -0
  123. atlas/mcp/ui-demo/templates/form_demo.html +28 -0
  124. atlas/mcp/username-override-demo/README.md +320 -0
  125. atlas/mcp/username-override-demo/main.py +308 -0
  126. atlas/modules/__init__.py +0 -0
  127. atlas/modules/config/__init__.py +34 -0
  128. atlas/modules/config/cli.py +231 -0
  129. atlas/modules/config/config_manager.py +1096 -0
  130. atlas/modules/file_storage/__init__.py +22 -0
  131. atlas/modules/file_storage/cli.py +330 -0
  132. atlas/modules/file_storage/content_extractor.py +290 -0
  133. atlas/modules/file_storage/manager.py +295 -0
  134. atlas/modules/file_storage/mock_s3_client.py +402 -0
  135. atlas/modules/file_storage/s3_client.py +417 -0
  136. atlas/modules/llm/__init__.py +19 -0
  137. atlas/modules/llm/caller.py +287 -0
  138. atlas/modules/llm/litellm_caller.py +675 -0
  139. atlas/modules/llm/models.py +19 -0
  140. atlas/modules/mcp_tools/__init__.py +17 -0
  141. atlas/modules/mcp_tools/client.py +2123 -0
  142. atlas/modules/mcp_tools/token_storage.py +556 -0
  143. atlas/modules/prompts/prompt_provider.py +130 -0
  144. atlas/modules/rag/__init__.py +24 -0
  145. atlas/modules/rag/atlas_rag_client.py +336 -0
  146. atlas/modules/rag/client.py +129 -0
  147. atlas/routes/admin_routes.py +865 -0
  148. atlas/routes/config_routes.py +484 -0
  149. atlas/routes/feedback_routes.py +361 -0
  150. atlas/routes/files_routes.py +274 -0
  151. atlas/routes/health_routes.py +40 -0
  152. atlas/routes/mcp_auth_routes.py +223 -0
  153. atlas/server_cli.py +164 -0
  154. atlas/tests/conftest.py +20 -0
  155. atlas/tests/integration/test_mcp_auth_integration.py +152 -0
  156. atlas/tests/manual_test_sampling.py +87 -0
  157. atlas/tests/modules/mcp_tools/test_client_auth.py +226 -0
  158. atlas/tests/modules/mcp_tools/test_client_env.py +191 -0
  159. atlas/tests/test_admin_mcp_server_management_routes.py +141 -0
  160. atlas/tests/test_agent_roa.py +135 -0
  161. atlas/tests/test_app_factory_smoke.py +47 -0
  162. atlas/tests/test_approval_manager.py +439 -0
  163. atlas/tests/test_atlas_client.py +188 -0
  164. atlas/tests/test_atlas_rag_client.py +447 -0
  165. atlas/tests/test_atlas_rag_integration.py +224 -0
  166. atlas/tests/test_attach_file_flow.py +287 -0
  167. atlas/tests/test_auth_utils.py +165 -0
  168. atlas/tests/test_backend_public_url.py +185 -0
  169. atlas/tests/test_banner_logging.py +287 -0
  170. atlas/tests/test_capability_tokens_and_injection.py +203 -0
  171. atlas/tests/test_compliance_level.py +54 -0
  172. atlas/tests/test_compliance_manager.py +253 -0
  173. atlas/tests/test_config_manager.py +617 -0
  174. atlas/tests/test_config_manager_paths.py +12 -0
  175. atlas/tests/test_core_auth.py +18 -0
  176. atlas/tests/test_core_utils.py +190 -0
  177. atlas/tests/test_docker_env_sync.py +202 -0
  178. atlas/tests/test_domain_errors.py +329 -0
  179. atlas/tests/test_domain_whitelist.py +359 -0
  180. atlas/tests/test_elicitation_manager.py +408 -0
  181. atlas/tests/test_elicitation_routing.py +296 -0
  182. atlas/tests/test_env_demo_server.py +88 -0
  183. atlas/tests/test_error_classification.py +113 -0
  184. atlas/tests/test_error_flow_integration.py +116 -0
  185. atlas/tests/test_feedback_routes.py +333 -0
  186. atlas/tests/test_file_content_extraction.py +1134 -0
  187. atlas/tests/test_file_extraction_routes.py +158 -0
  188. atlas/tests/test_file_library.py +107 -0
  189. atlas/tests/test_file_manager_unit.py +18 -0
  190. atlas/tests/test_health_route.py +49 -0
  191. atlas/tests/test_http_client_stub.py +8 -0
  192. atlas/tests/test_imports_smoke.py +30 -0
  193. atlas/tests/test_interfaces_llm_response.py +9 -0
  194. atlas/tests/test_issue_access_denied_fix.py +136 -0
  195. atlas/tests/test_llm_env_expansion.py +836 -0
  196. atlas/tests/test_log_level_sensitive_data.py +285 -0
  197. atlas/tests/test_mcp_auth_routes.py +341 -0
  198. atlas/tests/test_mcp_client_auth.py +331 -0
  199. atlas/tests/test_mcp_data_injection.py +270 -0
  200. atlas/tests/test_mcp_get_authorized_servers.py +95 -0
  201. atlas/tests/test_mcp_hot_reload.py +512 -0
  202. atlas/tests/test_mcp_image_content.py +424 -0
  203. atlas/tests/test_mcp_logging.py +172 -0
  204. atlas/tests/test_mcp_progress_updates.py +313 -0
  205. atlas/tests/test_mcp_prompt_override_system_prompt.py +102 -0
  206. atlas/tests/test_mcp_prompts_server.py +39 -0
  207. atlas/tests/test_mcp_tool_result_parsing.py +296 -0
  208. atlas/tests/test_metrics_logger.py +56 -0
  209. atlas/tests/test_middleware_auth.py +379 -0
  210. atlas/tests/test_prompt_risk_and_acl.py +141 -0
  211. atlas/tests/test_rag_mcp_aggregator.py +204 -0
  212. atlas/tests/test_rag_mcp_service.py +224 -0
  213. atlas/tests/test_rate_limit_middleware.py +45 -0
  214. atlas/tests/test_routes_config_smoke.py +60 -0
  215. atlas/tests/test_routes_files_download_token.py +41 -0
  216. atlas/tests/test_routes_files_health.py +18 -0
  217. atlas/tests/test_runtime_imports.py +53 -0
  218. atlas/tests/test_sampling_integration.py +482 -0
  219. atlas/tests/test_security_admin_routes.py +61 -0
  220. atlas/tests/test_security_capability_tokens.py +65 -0
  221. atlas/tests/test_security_file_stats_scope.py +21 -0
  222. atlas/tests/test_security_header_injection.py +191 -0
  223. atlas/tests/test_security_headers_and_filename.py +63 -0
  224. atlas/tests/test_shared_session_repository.py +101 -0
  225. atlas/tests/test_system_prompt_loading.py +181 -0
  226. atlas/tests/test_token_storage.py +505 -0
  227. atlas/tests/test_tool_approval_config.py +93 -0
  228. atlas/tests/test_tool_approval_utils.py +356 -0
  229. atlas/tests/test_tool_authorization_group_filtering.py +223 -0
  230. atlas/tests/test_tool_details_in_config.py +108 -0
  231. atlas/tests/test_tool_planner.py +300 -0
  232. atlas/tests/test_unified_rag_service.py +398 -0
  233. atlas/tests/test_username_override_in_approval.py +258 -0
  234. atlas/tests/test_websocket_auth_header.py +168 -0
  235. atlas/version.py +6 -0
  236. atlas_chat-0.1.0.data/data/.env.example +253 -0
  237. atlas_chat-0.1.0.data/data/config/defaults/compliance-levels.json +44 -0
  238. atlas_chat-0.1.0.data/data/config/defaults/domain-whitelist.json +123 -0
  239. atlas_chat-0.1.0.data/data/config/defaults/file-extractors.json +74 -0
  240. atlas_chat-0.1.0.data/data/config/defaults/help-config.json +198 -0
  241. atlas_chat-0.1.0.data/data/config/defaults/llmconfig-buggy.yml +11 -0
  242. atlas_chat-0.1.0.data/data/config/defaults/llmconfig.yml +19 -0
  243. atlas_chat-0.1.0.data/data/config/defaults/mcp.json +138 -0
  244. atlas_chat-0.1.0.data/data/config/defaults/rag-sources.json +17 -0
  245. atlas_chat-0.1.0.data/data/config/defaults/splash-config.json +16 -0
  246. atlas_chat-0.1.0.dist-info/METADATA +236 -0
  247. atlas_chat-0.1.0.dist-info/RECORD +250 -0
  248. atlas_chat-0.1.0.dist-info/WHEEL +5 -0
  249. atlas_chat-0.1.0.dist-info/entry_points.txt +4 -0
  250. atlas_chat-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,276 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Result processing module for code executor.
4
+ Handles output processing, visualization, and file encoding.
5
+ """
6
+
7
+ import base64
8
+ import logging
9
+ import traceback
10
+ from pathlib import Path
11
+ from typing import Dict, List
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ def detect_matplotlib_plots(exec_dir: Path) -> List[str]:
17
+ """
18
+ Detect if matplotlib plots were created and convert to base64.
19
+
20
+ Args:
21
+ exec_dir: Execution directory to scan for plot files
22
+
23
+ Returns:
24
+ List of base64-encoded plot images
25
+ """
26
+ try:
27
+ plot_files = []
28
+ for ext in ['*.png', '*.jpg', '*.jpeg', '*.svg']:
29
+ plot_files.extend(exec_dir.glob(ext))
30
+
31
+ base64_plots = []
32
+ for plot_file in plot_files:
33
+ try:
34
+ with open(plot_file, 'rb') as f:
35
+ image_data = base64.b64encode(f.read()).decode('utf-8')
36
+ file_ext = plot_file.suffix.lower()
37
+ mime_type = {
38
+ '.png': 'image/png',
39
+ '.jpg': 'image/jpeg',
40
+ '.jpeg': 'image/jpeg',
41
+ '.svg': 'image/svg+xml'
42
+ }.get(file_ext, 'image/png')
43
+
44
+ base64_plots.append(f"data:{mime_type};base64,{image_data}")
45
+ logger.info(f"Successfully encoded plot: {plot_file.name}")
46
+ except Exception as e:
47
+ logger.warning(f"Failed to encode plot {plot_file}: {str(e)}")
48
+ logger.warning(f"Traceback: {traceback.format_exc()}")
49
+ continue
50
+
51
+ logger.info(f"Detected {len(base64_plots)} plots in {exec_dir}")
52
+ return base64_plots
53
+
54
+ except Exception as e:
55
+ logger.error(f"Error detecting matplotlib plots: {str(e)}")
56
+ logger.error(f"Traceback: {traceback.format_exc()}")
57
+ return []
58
+
59
+
60
+ def create_visualization_html(plots: List[str], output_text: str) -> str:
61
+ """
62
+ Create HTML for displaying plots and output matching the frontend dark theme.
63
+
64
+ Args:
65
+ plots: List of base64-encoded plot images
66
+ output_text: Text output from code execution
67
+
68
+ Returns:
69
+ HTML string for display
70
+ """
71
+ html_content = """
72
+ <div style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
73
+ max-width: 100%;
74
+ padding: 20px;
75
+ background-color: #111827;
76
+ color: #e5e7eb;
77
+ line-height: 1.6;">
78
+ <h3 style="color: #e5e7eb; margin: 0 0 16px 0; font-weight: 600;">Code Execution Results</h3>
79
+ """
80
+
81
+ if output_text.strip():
82
+ html_content += f"""
83
+ <div style="background-color: #1f2937;
84
+ border: 1px solid #374151;
85
+ padding: 16px;
86
+ border-radius: 8px;
87
+ margin-bottom: 20px;">
88
+ <h4 style="color: #e5e7eb; margin: 0 0 12px 0; font-weight: 500; font-size: 14px;">Output:</h4>
89
+ <pre style="white-space: pre-wrap;
90
+ margin: 0;
91
+ font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
92
+ color: #d1d5db;
93
+ background-color: #111827;
94
+ padding: 12px;
95
+ border-radius: 6px;
96
+ border: 1px solid #4b5563;
97
+ overflow-x: auto;">{output_text}</pre>
98
+ </div>
99
+ """
100
+
101
+ if plots:
102
+ html_content += '<h4 style="color: #e5e7eb; margin: 20px 0 16px 0; font-weight: 500;">Generated Visualizations:</h4>'
103
+ for i, plot in enumerate(plots):
104
+ html_content += f"""
105
+ <div style="margin-bottom: 20px;
106
+ text-align: center;
107
+ background-color: #1f2937;
108
+ border: 1px solid #374151;
109
+ border-radius: 8px;
110
+ padding: 16px;">
111
+ <img src="{plot}"
112
+ alt="Plot {i+1}"
113
+ style="max-width: 100%;
114
+ height: auto;
115
+ border: 1px solid #4b5563;
116
+ border-radius: 6px;
117
+ background-color: white;">
118
+ </div>
119
+ """
120
+
121
+ html_content += "</div>"
122
+ return html_content
123
+
124
+
125
+ def list_generated_files(exec_dir: Path) -> List[str]:
126
+ """
127
+ List files generated during code execution.
128
+
129
+ Args:
130
+ exec_dir: Execution directory
131
+
132
+ Returns:
133
+ List of generated file names
134
+ """
135
+ try:
136
+ generated_files = []
137
+ for file_path in exec_dir.iterdir():
138
+ if file_path.is_file() and file_path.name != "exec_script.py":
139
+ generated_files.append(file_path.name)
140
+
141
+ logger.info(f"Generated files: {generated_files}")
142
+ return generated_files
143
+
144
+ except Exception as e:
145
+ logger.error(f"Error listing generated files: {str(e)}")
146
+ logger.error(f"Traceback: {traceback.format_exc()}")
147
+ return []
148
+
149
+
150
+ def encode_generated_files(exec_dir: Path) -> List[Dict[str, str]]:
151
+ """
152
+ Encode generated files to base64 for download.
153
+
154
+ Args:
155
+ exec_dir: Execution directory
156
+
157
+ Returns:
158
+ List of dictionaries with 'filename' and 'content_base64' keys
159
+ """
160
+ try:
161
+ encoded_files = []
162
+ for file_path in exec_dir.iterdir():
163
+ if file_path.is_file() and file_path.name != "exec_script.py":
164
+ try:
165
+ with open(file_path, 'rb') as f:
166
+ file_content = f.read()
167
+
168
+ content_base64 = base64.b64encode(file_content).decode('utf-8')
169
+ encoded_files.append({
170
+ 'filename': file_path.name,
171
+ 'content_base64': content_base64
172
+ })
173
+ logger.info(f"Encoded file: {file_path.name} ({len(file_content)} bytes)")
174
+ except Exception as e:
175
+ logger.warning(f"Failed to encode file {file_path.name}: {str(e)}")
176
+ continue
177
+
178
+ logger.info(f"Encoded {len(encoded_files)} generated files")
179
+ return encoded_files
180
+
181
+ except Exception as e:
182
+ logger.error(f"Error encoding generated files: {str(e)}")
183
+ logger.error(f"Traceback: {traceback.format_exc()}")
184
+ return []
185
+
186
+
187
+ def truncate_output_for_llm(output: str, max_chars: int = 2000) -> tuple[str, bool]:
188
+ """
189
+ Smart truncation that preserves important context around key terms.
190
+
191
+ Args:
192
+ output: The output text to potentially truncate
193
+ max_chars: Maximum characters to allow (default: 2000)
194
+
195
+ Returns:
196
+ Tuple of (truncated_output, was_truncated)
197
+ """
198
+ if len(output) <= max_chars:
199
+ return output, False
200
+
201
+ # Key terms that indicate important context
202
+ key_terms = [
203
+ 'error', 'Error', 'ERROR', 'exception', 'Exception', 'EXCEPTION',
204
+ 'traceback', 'Traceback', 'TRACEBACK', 'failed', 'Failed', 'FAILED',
205
+ 'warning', 'Warning', 'WARNING', 'success', 'Success', 'SUCCESS',
206
+ 'completed', 'Completed', 'COMPLETED', 'result', 'Result', 'RESULT',
207
+ 'summary', 'Summary', 'SUMMARY', 'total', 'Total', 'TOTAL',
208
+ 'shape:', 'dtype:', 'columns:', 'index:', 'memory usage:', 'non-null'
209
+ ]
210
+
211
+ # Find all important sections
212
+ important_sections = []
213
+ context_chars = 150 # Characters around each key term
214
+
215
+ for term in key_terms:
216
+ start_pos = 0
217
+ while True:
218
+ pos = output.find(term, start_pos)
219
+ if pos == -1:
220
+ break
221
+
222
+ # Extract context around the term
223
+ section_start = max(0, pos - context_chars)
224
+ section_end = min(len(output), pos + len(term) + context_chars)
225
+
226
+ # Expand to word boundaries if possible
227
+ while section_start > 0 and not output[section_start].isspace():
228
+ section_start -= 1
229
+ while section_end < len(output) and not output[section_end].isspace():
230
+ section_end += 1
231
+
232
+ important_sections.append((section_start, section_end, term))
233
+ start_pos = pos + 1
234
+
235
+ if important_sections:
236
+ # Sort sections by position and merge overlapping ones
237
+ important_sections.sort(key=lambda x: x[0])
238
+ merged_sections = []
239
+
240
+ for start, end, term in important_sections:
241
+ if merged_sections and start <= merged_sections[-1][1] + 50: # Merge if close
242
+ merged_sections[-1] = (merged_sections[-1][0], max(end, merged_sections[-1][1]), merged_sections[-1][2] + f", {term}")
243
+ else:
244
+ merged_sections.append((start, end, term))
245
+
246
+ # Build truncated output with important sections
247
+ result_parts = []
248
+ total_chars = 0
249
+
250
+ # Always include the beginning (first 300 chars)
251
+ beginning = output[:300]
252
+ result_parts.append(beginning)
253
+ total_chars += len(beginning)
254
+
255
+ # Add important sections
256
+ for start, end, terms in merged_sections:
257
+ section = output[start:end]
258
+ if total_chars + len(section) + 100 > max_chars: # Reserve space for truncation message
259
+ break
260
+
261
+ if start > 300: # Don't duplicate beginning
262
+ result_parts.append(f"\n\n[... content around: {terms} ...]\n")
263
+ result_parts.append(section)
264
+ total_chars += len(section) + 50
265
+
266
+ truncated = ''.join(result_parts)
267
+ else:
268
+ # No key terms found, use simple truncation
269
+ truncated = output[:max_chars - 200] # Reserve space for message
270
+ # Try to break at a reasonable point (newline) near the limit
271
+ last_newline = truncated.rfind('\n')
272
+ if last_newline > len(truncated) * 0.8: # If newline is in last 20%, use it
273
+ truncated = truncated[:last_newline]
274
+
275
+ truncation_msg = f"\n\n[OUTPUT TRUNCATED - Original length: {len(output)} characters. Full output preserved in downloaded files and visualizations.]"
276
+ return truncated + truncation_msg, True
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Script generation module for code executor.
4
+ Handles creation of safe execution scripts with security overrides.
5
+ """
6
+
7
+ import logging
8
+ import traceback
9
+ from pathlib import Path
10
+
11
+
12
+ # Import CodeExecutionError class definition locally to avoid circular imports
13
+ class CodeExecutionError(Exception):
14
+ """Raised when code execution fails."""
15
+ pass
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ def create_safe_execution_script(code: str, exec_dir: Path) -> Path:
21
+ """
22
+ Create a Python script with the user code wrapped in safety measures.
23
+
24
+ Args:
25
+ code: User's Python code
26
+ exec_dir: Execution directory
27
+ Returns:
28
+ Path to the created script
29
+ """
30
+ try:
31
+ # Indent each line of user code to fit inside the try block
32
+ indented_code = '\n'.join(' ' + line for line in code.split('\n'))
33
+
34
+ script_content = f'''#!/usr/bin/env python3
35
+ import sys
36
+ import os
37
+ import json
38
+ import traceback
39
+ from pathlib import Path
40
+
41
+ # Change to execution directory
42
+ os.chdir(r"{exec_dir}")
43
+
44
+ # Configure matplotlib for safe plotting
45
+ import matplotlib
46
+ matplotlib.use('Agg') # Use non-interactive backend that saves to files
47
+ matplotlib.rcParams['savefig.directory'] = r"{exec_dir}" # Default save directory
48
+
49
+ # Restrict file operations to current directory only
50
+ original_open = open
51
+
52
+ def safe_open(file, mode='r', **kwargs):
53
+ """Override open to restrict file access to execution directory, with exceptions for safe plotting libraries."""
54
+ file_path = Path(file).resolve()
55
+ exec_path = Path(r"{exec_dir}").resolve()
56
+
57
+ try:
58
+ file_path.relative_to(exec_path)
59
+ # File is in execution directory - always allow
60
+ return original_open(file, mode, **kwargs)
61
+ except ValueError:
62
+ # File is outside execution directory - check if it's an allowed library file
63
+ file_str = str(file_path)
64
+
65
+ # Allow matplotlib and seaborn configuration and data files (read-only)
66
+ allowed_paths = [
67
+ '/matplotlib/',
68
+ '/seaborn/',
69
+ '/site-packages/matplotlib/',
70
+ '/site-packages/seaborn/',
71
+ 'matplotlib/mpl-data/',
72
+ 'matplotlib/backends/',
73
+ 'matplotlib/font_manager.py',
74
+ 'seaborn/data/',
75
+ 'seaborn/_core/',
76
+ 'numpy/core/',
77
+ 'pandas/io/',
78
+ '/usr/share/fonts/',
79
+ '/usr/local/share/fonts/',
80
+ 'fontconfig/',
81
+ '.cache/matplotlib/',
82
+ '/tmp/matplotlib-',
83
+ '/home/.matplotlib/',
84
+ '/.matplotlib/',
85
+ ]
86
+
87
+ # Check if file path contains any allowed library paths
88
+ is_allowed_path = any(allowed_path in file_str for allowed_path in allowed_paths)
89
+
90
+ if not is_allowed_path:
91
+ raise PermissionError(f"File access outside execution directory not allowed: {{file}}")
92
+
93
+ # Allow read access to all allowed paths
94
+ if 'r' in mode and 'w' not in mode and 'a' not in mode and '+' not in mode:
95
+ return original_open(file, mode, **kwargs)
96
+
97
+ # Allow write access only to matplotlib cache directories
98
+ if ('.cache/matplotlib/' in file_str or
99
+ 'matplotlib/fontList.cache' in file_str or
100
+ 'matplotlib/tex.cache' in file_str or
101
+ '/tmp/matplotlib-' in file_str or
102
+ '/.matplotlib/' in file_str):
103
+ return original_open(file, mode, **kwargs)
104
+
105
+ # Deny write access to other external files
106
+ if 'w' in mode or 'a' in mode or '+' in mode:
107
+ raise PermissionError(f"Write access outside execution directory not allowed: {{file}}")
108
+
109
+ return original_open(file, mode, **kwargs)
110
+
111
+ # Override built-in open
112
+ if isinstance(__builtins__, dict):
113
+ __builtins__['open'] = safe_open
114
+ else:
115
+ __builtins__.open = safe_open
116
+
117
+ # Capture output
118
+ import io
119
+ import sys
120
+
121
+ stdout_buffer = io.StringIO()
122
+ stderr_buffer = io.StringIO()
123
+
124
+ # Redirect stdout and stderr
125
+ old_stdout = sys.stdout
126
+ old_stderr = sys.stderr
127
+ sys.stdout = stdout_buffer
128
+ sys.stderr = stderr_buffer
129
+
130
+ execution_error = None
131
+ error_traceback = None
132
+
133
+ try:
134
+ # User code starts here (matplotlib/seaborn should now work with plotting)
135
+ {indented_code}
136
+ # User code ends here
137
+
138
+ # Auto-save any open matplotlib figures so they surface in UI even if user didn't call plt.savefig()
139
+ try:
140
+ # Only run if matplotlib/pyplot is available
141
+ if 'matplotlib' in sys.modules:
142
+ import matplotlib.pyplot as plt # type: ignore
143
+ fig_nums = list(plt.get_fignums())
144
+ if fig_nums:
145
+ for _fig_num in fig_nums:
146
+ try:
147
+ fig = plt.figure(_fig_num)
148
+ out_name = "plot_" + str(_fig_num) + ".png"
149
+ fig.savefig(out_name)
150
+ except Exception:
151
+ # Don't fail user code on save issues
152
+ pass
153
+ try:
154
+ plt.close('all')
155
+ except Exception:
156
+ pass
157
+ except Exception:
158
+ # Silent best-effort; plotting is optional
159
+ pass
160
+
161
+ except Exception as e:
162
+ execution_error = e
163
+ error_traceback = traceback.format_exc()
164
+ print(f"Execution error: {{type(e).__name__}}: {{str(e)}}", file=sys.stderr)
165
+ print(f"Traceback:\\n{{error_traceback}}", file=sys.stderr)
166
+
167
+ finally:
168
+ # Restore stdout and stderr
169
+ sys.stdout = old_stdout
170
+ sys.stderr = old_stderr
171
+
172
+ # Output results
173
+ result = {{
174
+ "stdout": stdout_buffer.getvalue(),
175
+ "stderr": stderr_buffer.getvalue(),
176
+ "success": execution_error is None,
177
+ "error_type": type(execution_error).__name__ if execution_error else None,
178
+ "error_traceback": error_traceback
179
+ }}
180
+
181
+ print(json.dumps(result))
182
+ '''
183
+
184
+ script_path = exec_dir / "exec_script.py"
185
+ with open(script_path, 'w') as f:
186
+ f.write(script_content)
187
+
188
+ logger.info(f"Created execution script: {script_path}")
189
+ return script_path
190
+
191
+ except Exception as e:
192
+ error_msg = f"Failed to create execution script: {str(e)}"
193
+ logger.error(error_msg)
194
+ logger.error(f"Traceback: {traceback.format_exc()}")
195
+ raise CodeExecutionError(error_msg)
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Security checking module for code executor.
4
+ Contains AST-based security analysis for Python code.
5
+ """
6
+
7
+ import ast
8
+ import logging
9
+ import traceback
10
+ from typing import List
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class CodeSecurityError(Exception):
16
+ """Raised when code fails security checks."""
17
+ pass
18
+
19
+
20
+ class SecurityChecker(ast.NodeVisitor):
21
+ """AST visitor to check for dangerous code patterns."""
22
+
23
+ def __init__(self):
24
+ self.violations = []
25
+ self.imported_modules = set()
26
+
27
+ # Dangerous modules that should never be imported
28
+ self.forbidden_modules = {
29
+ 'os', 'sys', 'subprocess', 'socket', 'urllib', 'urllib2', 'urllib3',
30
+ 'requests', 'http', 'ftplib', 'smtplib', 'telnetlib', 'webbrowser',
31
+ 'ctypes', 'multiprocessing', 'threading', 'asyncio', 'concurrent',
32
+ 'pickle', 'dill', 'shelve', 'dbm', 'sqlite3', 'pymongo',
33
+ 'paramiko', 'fabric', 'pexpect', 'pty', 'tty',
34
+ 'importlib', '__builtin__', 'builtins', 'imp'
35
+ }
36
+
37
+ # Allowed safe modules for data analysis
38
+ self.allowed_modules = {
39
+ 'numpy', 'np', 'pandas', 'pd', 'matplotlib', 'plt', 'seaborn', 'sns',
40
+ 'scipy', 'sklearn', 'PIL', 'pillow', 'openpyxl',
41
+ 'json', 'csv', 'datetime', 'math', 'statistics', 'random', 're',
42
+ 'collections', 'itertools', 'functools', 'operator', 'copy',
43
+ 'decimal', 'fractions', 'pathlib', 'typing'
44
+ }
45
+
46
+ # Dangerous function names
47
+ self.forbidden_functions = {
48
+ 'eval', 'exec', 'compile', '__import__', 'getattr', 'setattr', 'delattr',
49
+ 'hasattr', 'callable', 'isinstance', 'issubclass', 'super', 'globals',
50
+ 'locals', 'vars', 'dir', 'help', 'input', 'raw_input', 'exit', 'quit'
51
+ }
52
+
53
+ def visit_Import(self, node):
54
+ """Check import statements."""
55
+ for alias in node.names:
56
+ module_name = alias.name.split('.')[0]
57
+ self.imported_modules.add(module_name)
58
+
59
+ if module_name in self.forbidden_modules:
60
+ self.violations.append(f"Forbidden module import: {module_name}")
61
+ elif module_name not in self.allowed_modules:
62
+ self.violations.append(f"Unauthorized module import: {module_name}")
63
+
64
+ self.generic_visit(node)
65
+
66
+ def visit_ImportFrom(self, node):
67
+ """Check from...import statements."""
68
+ if node.module:
69
+ module_name = node.module.split('.')[0]
70
+ self.imported_modules.add(module_name)
71
+
72
+ if module_name in self.forbidden_modules:
73
+ self.violations.append(f"Forbidden module import: {module_name}")
74
+ elif module_name not in self.allowed_modules:
75
+ self.violations.append(f"Unauthorized module import: {module_name}")
76
+
77
+ self.generic_visit(node)
78
+
79
+ def visit_Call(self, node):
80
+ """Check function calls."""
81
+ # Check for dangerous built-in functions
82
+ if isinstance(node.func, ast.Name):
83
+ if node.func.id in self.forbidden_functions:
84
+ self.violations.append(f"Forbidden function call: {node.func.id}")
85
+
86
+ # Check for file operations outside working directory
87
+ elif isinstance(node.func, ast.Attribute):
88
+ if (isinstance(node.func.value, ast.Name) and
89
+ node.func.value.id == 'open' or node.func.attr == 'open'):
90
+ # Allow open() but we'll validate paths at runtime
91
+ pass
92
+
93
+ self.generic_visit(node)
94
+
95
+ def visit_With(self, node):
96
+ """Check with statements (often used for file operations)."""
97
+ for item in node.items:
98
+ if isinstance(item.context_expr, ast.Call):
99
+ if (isinstance(item.context_expr.func, ast.Name) and
100
+ item.context_expr.func.id == 'open'):
101
+ # Allow open() but validate paths at runtime
102
+ pass
103
+
104
+ self.generic_visit(node)
105
+
106
+ def visit_Attribute(self, node):
107
+ """Check attribute access."""
108
+ # Check for dangerous attribute access patterns
109
+ if isinstance(node.value, ast.Name):
110
+ if (node.value.id == '__builtins__' or
111
+ node.attr.startswith('__') and node.attr.endswith('__')):
112
+ self.violations.append(f"Forbidden attribute access: {node.value.id}.{node.attr}")
113
+
114
+ self.generic_visit(node)
115
+
116
+
117
+ def check_code_security(code: str) -> List[str]:
118
+ """
119
+ Check Python code for security violations using AST parsing.
120
+
121
+ Args:
122
+ code: Python code to check
123
+
124
+ Returns:
125
+ List of security violations (empty if safe)
126
+ """
127
+ try:
128
+ tree = ast.parse(code)
129
+ checker = SecurityChecker()
130
+ checker.visit(tree)
131
+ return checker.violations
132
+ except SyntaxError as e:
133
+ error_msg = f"Syntax error: {str(e)}"
134
+ logger.warning(f"Code syntax error: {error_msg}")
135
+ return [error_msg]
136
+ except Exception as e:
137
+ error_msg = f"Security check error: {str(e)}"
138
+ logger.error(f"Unexpected error during security check: {error_msg}")
139
+ logger.error(f"Traceback: {traceback.format_exc()}")
140
+ return [error_msg]