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
atlas/main.py ADDED
@@ -0,0 +1,564 @@
1
+ """
2
+ Basic chat backend implementing the modular architecture.
3
+ Focuses on essential chat functionality only.
4
+ """
5
+
6
+ # Suppress LiteLLM verbose logging BEFORE any transitive import of litellm.
7
+ # litellm._logging reads LITELLM_LOG at import time and defaults to DEBUG.
8
+ # This must happen before any other imports that might load litellm.
9
+ import os
10
+ from pathlib import Path as _Path
11
+
12
+ from dotenv import dotenv_values as _dotenv_values
13
+
14
+ # Load .env values without setting them in os.environ yet (just to read feature flag)
15
+ _env_path = _Path(__file__).parent.parent / ".env"
16
+ _env_values = _dotenv_values(_env_path) if _env_path.exists() else {}
17
+
18
+ # Check feature flag: FEATURE_SUPPRESS_LITELLM_LOGGING (default: true)
19
+ _suppress_litellm = _env_values.get("FEATURE_SUPPRESS_LITELLM_LOGGING", "true").lower() in ("true", "1", "yes")
20
+
21
+ if _suppress_litellm and "LITELLM_LOG" not in os.environ:
22
+ os.environ["LITELLM_LOG"] = "ERROR"
23
+
24
+ # Clean up temporary imports
25
+ del _Path, _dotenv_values, _env_path, _env_values, _suppress_litellm
26
+
27
+ # Standard imports follow - must come after LiteLLM logging suppression above
28
+ # ruff: noqa: E402
29
+ import asyncio
30
+ import logging
31
+ from contextlib import asynccontextmanager
32
+ from pathlib import Path
33
+ from uuid import uuid4
34
+
35
+ from dotenv import load_dotenv
36
+ from fastapi import FastAPI, WebSocket, WebSocketDisconnect, WebSocketException
37
+ from fastapi.responses import FileResponse
38
+ from fastapi.staticfiles import StaticFiles
39
+
40
+ from atlas.core.auth import get_user_from_header
41
+ from atlas.core.domain_whitelist_middleware import DomainWhitelistMiddleware
42
+ from atlas.core.log_sanitizer import sanitize_for_logging, summarize_tool_approval_response_for_logging
43
+ from atlas.core.metrics_logger import log_metric
44
+
45
+ # Import from atlas.core (only essential middleware and config)
46
+ from atlas.core.middleware import AuthMiddleware
47
+ from atlas.core.otel_config import setup_opentelemetry
48
+ from atlas.core.rate_limit_middleware import RateLimitMiddleware
49
+ from atlas.core.security_headers_middleware import SecurityHeadersMiddleware
50
+
51
+ # Import domain errors
52
+ from atlas.domain.errors import DomainError, LLMAuthenticationError, LLMTimeoutError, RateLimitError, ValidationError
53
+
54
+ # Import from atlas.infrastructure
55
+ from atlas.infrastructure.app_factory import app_factory
56
+ from atlas.infrastructure.transport.websocket_connection_adapter import WebSocketConnectionAdapter
57
+ from atlas.routes.admin_routes import admin_router
58
+
59
+ # Import essential routes
60
+ from atlas.routes.config_routes import router as config_router
61
+ from atlas.routes.feedback_routes import feedback_router
62
+ from atlas.routes.files_routes import router as files_router
63
+ from atlas.routes.health_routes import router as health_router
64
+ from atlas.routes.mcp_auth_routes import router as mcp_auth_router
65
+ from atlas.version import VERSION
66
+
67
+ # Load environment variables from the parent directory
68
+ load_dotenv(dotenv_path="../.env")
69
+
70
+ # Setup OpenTelemetry logging
71
+ otel_config = setup_opentelemetry("atlas-ui-3-backend", "1.0.0")
72
+
73
+ logger = logging.getLogger(__name__)
74
+
75
+
76
+ async def websocket_update_callback(websocket: WebSocket, message: dict):
77
+ """
78
+ Callback function to handle websocket updates with logging.
79
+ """
80
+ try:
81
+ mtype = message.get("type")
82
+ if mtype == "intermediate_update":
83
+ utype = message.get("update_type") or message.get("data", {}).get("update_type")
84
+ # Handle specific update types (canvas_files, files_update)
85
+ # Logging disabled for these message types - see git history if needed
86
+ if utype in ("canvas_files", "files_update"):
87
+ pass
88
+ elif mtype == "canvas_content":
89
+ content = message.get("content")
90
+ clen = len(content) if isinstance(content, str) else "obj"
91
+ logger.debug("WS SEND: canvas_content length=%s", clen)
92
+ else:
93
+ logger.debug("WS SEND: %s", mtype)
94
+ except Exception:
95
+ # Non-fatal logging error; continue to send
96
+ pass
97
+ await websocket.send_json(message)
98
+
99
+
100
+ def _ensure_feedback_directory():
101
+ """Ensure feedback storage directory exists at startup."""
102
+ from pathlib import Path
103
+ config = app_factory.get_config_manager()
104
+ feedback_dir = Path(config.app_settings.runtime_feedback_dir)
105
+ try:
106
+ feedback_dir.mkdir(parents=True, exist_ok=True)
107
+ logger.info(f"Feedback directory ready: {feedback_dir}")
108
+ except Exception as e:
109
+ logger.warning(f"Could not create feedback directory {feedback_dir}: {e}")
110
+
111
+
112
+ @asynccontextmanager
113
+ async def lifespan(app: FastAPI):
114
+ """Application lifespan manager."""
115
+ logger.info("Starting Chat UI Backend with modular architecture")
116
+
117
+ # Initialize configuration
118
+ config = app_factory.get_config_manager()
119
+
120
+ # SECURITY WARNING: Check for missing proxy secret in production
121
+ if not config.app_settings.debug_mode:
122
+ if not config.app_settings.feature_proxy_secret_enabled:
123
+ logger.warning(
124
+ "SECURITY WARNING: Proxy secret validation is DISABLED in production. "
125
+ "Set FEATURE_PROXY_SECRET_ENABLED=true and PROXY_SECRET to enable."
126
+ )
127
+ elif not config.app_settings.proxy_secret:
128
+ logger.warning(
129
+ "SECURITY WARNING: Proxy secret is ENABLED but PROXY_SECRET is not set. "
130
+ "Authentication will fail for all requests."
131
+ )
132
+
133
+ logger.info(f"Backend initialized with {len(config.llm_config.models)} LLM models")
134
+ logger.info(f"MCP servers configured: {len(config.mcp_config.servers)}")
135
+
136
+ # Ensure feedback directory exists
137
+ _ensure_feedback_directory()
138
+
139
+ # Initialize MCP tools manager
140
+ logger.info("Initializing MCP tools manager...")
141
+ mcp_manager = app_factory.get_mcp_manager()
142
+
143
+ try:
144
+ logger.info("Step 1: Initializing MCP clients...")
145
+ await mcp_manager.initialize_clients()
146
+ logger.info("Step 1 complete: MCP clients initialized")
147
+
148
+ logger.info("Step 2: Discovering tools...")
149
+ await mcp_manager.discover_tools()
150
+ logger.info("Step 2 complete: Tool discovery finished")
151
+
152
+ logger.info("Step 3: Discovering prompts...")
153
+ await mcp_manager.discover_prompts()
154
+ logger.info("Step 3 complete: Prompt discovery finished")
155
+
156
+ logger.info("MCP tools manager initialization complete")
157
+
158
+ # Start auto-reconnect background task if enabled
159
+ logger.info("Step 4: Starting MCP auto-reconnect (if enabled)...")
160
+ await mcp_manager.start_auto_reconnect()
161
+ logger.info("Step 4 complete: Auto-reconnect task started (if enabled)")
162
+
163
+ except Exception as e:
164
+ logger.error(f"Error during MCP initialization: {e}", exc_info=True)
165
+ # Continue startup even if MCP fails
166
+ logger.warning("Continuing startup without MCP tools")
167
+
168
+ yield
169
+
170
+ logger.info("Shutting down Chat UI Backend")
171
+ # Stop auto-reconnect task
172
+ await mcp_manager.stop_auto_reconnect()
173
+ # Cleanup MCP clients
174
+ await mcp_manager.cleanup()
175
+
176
+
177
+ # Create FastAPI app with minimal setup
178
+ app = FastAPI(
179
+ title="Chat UI Backend",
180
+ description="Basic chat backend with modular architecture",
181
+ version=VERSION,
182
+ lifespan=lifespan,
183
+ )
184
+
185
+ # Get config for middleware
186
+ config = app_factory.get_config_manager()
187
+
188
+ """Security: enforce rate limiting and auth middleware.
189
+ RateLimit first to cheaply throttle abusive traffic before heavier logic.
190
+ """
191
+ app.add_middleware(SecurityHeadersMiddleware)
192
+ app.add_middleware(RateLimitMiddleware)
193
+ # Domain whitelist check (if enabled) - add before Auth so it runs after
194
+ if config.app_settings.feature_domain_whitelist_enabled:
195
+ app.add_middleware(
196
+ DomainWhitelistMiddleware,
197
+ auth_redirect_url=config.app_settings.auth_redirect_url
198
+ )
199
+ app.add_middleware(
200
+ AuthMiddleware,
201
+ debug_mode=config.app_settings.debug_mode,
202
+ auth_header_name=config.app_settings.auth_user_header,
203
+ auth_header_type=config.app_settings.auth_user_header_type,
204
+ auth_aws_expected_alb_arn=config.app_settings.auth_aws_expected_alb_arn,
205
+ auth_aws_region=config.app_settings.auth_aws_region,
206
+ proxy_secret_enabled=config.app_settings.feature_proxy_secret_enabled,
207
+ proxy_secret_header=config.app_settings.proxy_secret_header,
208
+ proxy_secret=config.app_settings.proxy_secret,
209
+ auth_redirect_url=config.app_settings.auth_redirect_url
210
+ )
211
+
212
+ # Include essential routes (add files API)
213
+ app.include_router(config_router)
214
+ app.include_router(admin_router)
215
+ app.include_router(files_router)
216
+ app.include_router(health_router)
217
+ app.include_router(feedback_router)
218
+ app.include_router(mcp_auth_router)
219
+
220
+ # Serve frontend build (Vite)
221
+ project_root = Path(__file__).resolve().parents[1]
222
+ static_dir = project_root / "frontend" / "dist"
223
+ if static_dir.exists():
224
+ # Serve the SPA entry
225
+ @app.get("/")
226
+ async def read_root():
227
+ return FileResponse(str(static_dir / "index.html"))
228
+
229
+ # Serve hashed asset files under /assets (CSS/JS/images from Vite build)
230
+ assets_dir = static_dir / "assets"
231
+ if assets_dir.exists():
232
+ app.mount("/assets", StaticFiles(directory=assets_dir), name="assets")
233
+
234
+ # Serve webfonts from Vite build (placed via frontend/public/fonts)
235
+ fonts_dir = static_dir / "fonts"
236
+ if fonts_dir.exists():
237
+ app.mount("/fonts", StaticFiles(directory=fonts_dir), name="fonts")
238
+ else:
239
+ # Fallback to unbuilt public fonts if dist/fonts is missing
240
+ public_fonts = project_root / "frontend" / "public" / "fonts"
241
+ if public_fonts.exists():
242
+ app.mount("/fonts", StaticFiles(directory=public_fonts), name="fonts")
243
+
244
+ # Common top-level static files in the Vite build
245
+ @app.get("/favicon.ico")
246
+ async def favicon():
247
+ path = static_dir / "favicon.ico"
248
+ return FileResponse(str(path))
249
+
250
+ @app.get("/vite.svg")
251
+ async def vite_svg():
252
+ path = static_dir / "vite.svg"
253
+ return FileResponse(str(path))
254
+
255
+ @app.get("/logo.png")
256
+ async def logo_png():
257
+ path = static_dir / "logo.png"
258
+ return FileResponse(str(path))
259
+
260
+ @app.get("/sandia-powered-by-atlas.png")
261
+ async def logo2_png():
262
+ path = static_dir / "sandia-powered-by-atlas.png"
263
+ return FileResponse(str(path))
264
+
265
+
266
+ # WebSocket endpoint for chat
267
+ @app.websocket("/ws")
268
+ async def websocket_endpoint(websocket: WebSocket):
269
+ """
270
+ Main chat WebSocket endpoint using new architecture.
271
+
272
+ SECURITY NOTE - Production Architecture:
273
+ ==========================================
274
+ This endpoint appears to lack authentication when viewed in isolation,
275
+ but in production it sits behind a reverse proxy with a separate
276
+ authentication service. The authentication flow is:
277
+
278
+ 1. Client connects to WebSocket endpoint
279
+ 2. Reverse proxy intercepts WebSocket handshake (HTTP Upgrade request)
280
+ 3. Reverse proxy delegates to authentication service
281
+ 4. Auth service validates JWT/session from cookies or headers
282
+ 5. If valid: Auth service returns authenticated user header
283
+ 6. Reverse proxy forwards connection to this app with authenticated user header
284
+ 7. This app trusts the header (already validated by auth service)
285
+
286
+ The header name is configurable via AUTH_USER_HEADER environment variable
287
+ (default: X-User-Email). This allows flexibility for different reverse proxy setups.
288
+
289
+ SECURITY REQUIREMENTS:
290
+ - This app MUST ONLY be accessible via reverse proxy
291
+ - Direct public access to this app bypasses authentication
292
+ - Use network isolation to prevent direct access
293
+ - The /login endpoint lives in the separate auth service
294
+ - Reverse proxy MUST strip client-provided X-User-Email headers before adding its own
295
+ (otherwise attackers can inject headers: X-User-Email: admin@company.com)
296
+
297
+ DEVELOPMENT vs PRODUCTION:
298
+ - Production: Extracts user from configured auth header (set by reverse proxy)
299
+ - Development: Falls back to 'user' query parameter (INSECURE, local only)
300
+
301
+ See docs/security_architecture.md for complete architecture details.
302
+ """
303
+ # Extract user email using the same authentication flow as HTTP requests
304
+ # Priority: 1) configured auth header (production), 2) query param (dev), 3) test user (dev fallback)
305
+ config_manager = app_factory.get_config_manager()
306
+
307
+ is_debug_mode = config_manager.app_settings.debug_mode
308
+
309
+ # WebSocket connections must present the shared proxy secret (same as AuthMiddleware)
310
+ if (
311
+ config_manager.app_settings.feature_proxy_secret_enabled
312
+ and config_manager.app_settings.proxy_secret
313
+ and not is_debug_mode
314
+ ):
315
+ proxy_secret_header = config_manager.app_settings.proxy_secret_header
316
+ proxy_secret_value = websocket.headers.get(proxy_secret_header)
317
+ if proxy_secret_value != config_manager.app_settings.proxy_secret:
318
+ logger.warning(
319
+ "WS proxy secret mismatch on %s",
320
+ sanitize_for_logging(websocket.client)
321
+ )
322
+ raise WebSocketException(code=1008, reason="Invalid proxy secret")
323
+
324
+ # Authenticate user BEFORE accepting the connection
325
+ user_email = None
326
+
327
+ # Check configured auth header first (consistent with AuthMiddleware)
328
+ auth_header_name = config_manager.app_settings.auth_user_header
329
+ x_email_header = websocket.headers.get(auth_header_name)
330
+ if x_email_header:
331
+ user_email = get_user_from_header(x_email_header)
332
+
333
+ # Fallback to query parameter (development/testing ONLY)
334
+ if not user_email and is_debug_mode:
335
+ user_email = websocket.query_params.get('user')
336
+ if user_email:
337
+ logger.info(
338
+ "WebSocket authenticated via query parameter (debug mode): %s",
339
+ sanitize_for_logging(user_email)
340
+ )
341
+
342
+ # Final fallback to test user (development mode ONLY)
343
+ if not user_email and is_debug_mode:
344
+ user_email = config_manager.app_settings.test_user or 'test@test.com'
345
+ logger.info(
346
+ "WebSocket using fallback test user (debug mode): %s",
347
+ sanitize_for_logging(user_email)
348
+ )
349
+
350
+ # PRODUCTION: Reject unauthenticated connections
351
+ if not user_email:
352
+ logger.warning(
353
+ "WebSocket authentication failed - no user found in %s header. Client: %s",
354
+ sanitize_for_logging(auth_header_name),
355
+ sanitize_for_logging(websocket.client)
356
+ )
357
+ raise WebSocketException(
358
+ code=1008,
359
+ reason="Authentication required. Please ensure you are accessing this application through the configured reverse proxy."
360
+ )
361
+
362
+ # Now accept the connection (user is authenticated)
363
+ await websocket.accept()
364
+ logger.info(
365
+ "WebSocket authenticated via %s header: %s",
366
+ sanitize_for_logging(auth_header_name),
367
+ sanitize_for_logging(user_email)
368
+ )
369
+
370
+ session_id = uuid4()
371
+
372
+ # Create connection adapter with authenticated user and chat service
373
+ connection_adapter = WebSocketConnectionAdapter(websocket, user_email)
374
+ chat_service = app_factory.create_chat_service(connection_adapter)
375
+
376
+ logger.info(f"WebSocket connection established for session {sanitize_for_logging(str(session_id))}")
377
+
378
+ try:
379
+ while True:
380
+ data = await websocket.receive_json()
381
+ message_type = data.get("type")
382
+
383
+ # Debug: Log ALL incoming messages
384
+ logger.debug(
385
+ "WS RECEIVED message_type=[%s], data keys=%s",
386
+ sanitize_for_logging(message_type),
387
+ [f"[{sanitize_for_logging(key)}]" for key in data.keys()]
388
+ )
389
+
390
+ if message_type == "chat":
391
+ # Handle chat message in background so we can still receive approval responses
392
+ async def handle_chat():
393
+ try:
394
+ await chat_service.handle_chat_message(
395
+ session_id=session_id,
396
+ content=data.get("content", ""),
397
+ model=data.get("model", ""),
398
+ selected_tools=data.get("selected_tools"),
399
+ selected_prompts=data.get("selected_prompts"),
400
+ selected_data_sources=data.get("selected_data_sources"),
401
+ only_rag=data.get("only_rag", False),
402
+ tool_choice_required=data.get("tool_choice_required", False),
403
+ user_email=user_email, # Use authenticated user from connection
404
+ agent_mode=data.get("agent_mode", False),
405
+ agent_max_steps=data.get("agent_max_steps", 10),
406
+ temperature=data.get("temperature", 0.7),
407
+ agent_loop_strategy=data.get("agent_loop_strategy"),
408
+ update_callback=lambda message: websocket_update_callback(websocket, message),
409
+ files=data.get("files")
410
+ )
411
+ except RateLimitError as e:
412
+ logger.warning(f"Rate limit error in chat handler: {e}")
413
+ log_metric("error", user_email, error_type="rate_limit")
414
+ await websocket.send_json({
415
+ "type": "error",
416
+ "message": str(e.message if hasattr(e, 'message') else e),
417
+ "error_type": "rate_limit"
418
+ })
419
+ except LLMTimeoutError as e:
420
+ logger.warning(f"Timeout error in chat handler: {e}")
421
+ log_metric("error", user_email, error_type="timeout")
422
+ await websocket.send_json({
423
+ "type": "error",
424
+ "message": str(e.message if hasattr(e, 'message') else e),
425
+ "error_type": "timeout"
426
+ })
427
+ except LLMAuthenticationError as e:
428
+ logger.error(f"Authentication error in chat handler: {e}")
429
+ log_metric("error", user_email, error_type="authentication")
430
+ await websocket.send_json({
431
+ "type": "error",
432
+ "message": str(e.message if hasattr(e, 'message') else e),
433
+ "error_type": "authentication"
434
+ })
435
+ except ValidationError as e:
436
+ logger.warning(f"Validation error in chat handler: {e}")
437
+ log_metric("error", user_email, error_type="validation")
438
+ await websocket.send_json({
439
+ "type": "error",
440
+ "message": str(e.message if hasattr(e, 'message') else e),
441
+ "error_type": "validation"
442
+ })
443
+ except DomainError as e:
444
+ logger.error(f"Domain error in chat handler: {e}", exc_info=True)
445
+ log_metric("error", user_email, error_type="domain")
446
+ await websocket.send_json({
447
+ "type": "error",
448
+ "message": str(e.message if hasattr(e, 'message') else e),
449
+ "error_type": "domain"
450
+ })
451
+ except Exception as e:
452
+ logger.error(f"Unexpected error in chat handler: {e}", exc_info=True)
453
+ log_metric("error", user_email, error_type="unexpected")
454
+ await websocket.send_json({
455
+ "type": "error",
456
+ "message": "An unexpected error occurred. Please try again or contact support if the issue persists.",
457
+ "error_type": "unexpected"
458
+ })
459
+
460
+ # Start chat handling in background
461
+ asyncio.create_task(handle_chat())
462
+
463
+ elif message_type == "download_file":
464
+ # Handle file download (use authenticated user from connection)
465
+ response = await chat_service.handle_download_file(
466
+ session_id=session_id,
467
+ filename=data.get("filename", ""),
468
+ user_email=user_email
469
+ )
470
+ await websocket.send_json(response)
471
+
472
+ elif message_type == "reset_session":
473
+ # Handle session reset (use authenticated user from connection)
474
+ response = await chat_service.handle_reset_session(
475
+ session_id=session_id,
476
+ user_email=user_email
477
+ )
478
+ await websocket.send_json(response)
479
+
480
+ elif message_type == "attach_file":
481
+ # Handle file attachment to session (use authenticated user, not client-sent)
482
+ response = await chat_service.handle_attach_file(
483
+ session_id=session_id,
484
+ s3_key=data.get("s3_key"),
485
+ user_email=user_email, # Use authenticated user from connection
486
+ update_callback=lambda message: websocket_update_callback(websocket, message)
487
+ )
488
+ await websocket.send_json(response)
489
+
490
+ elif message_type == "tool_approval_response":
491
+ # Handle tool approval response
492
+ from atlas.application.chat.approval_manager import get_approval_manager
493
+ approval_manager = get_approval_manager()
494
+
495
+ tool_call_id = data.get("tool_call_id")
496
+ approved = data.get("approved", False)
497
+ arguments = data.get("arguments")
498
+ reason = data.get("reason")
499
+
500
+ # SECURITY: Never log tool arguments at INFO level (they may include sensitive user data).
501
+ # Log a conservative summary instead.
502
+ logger.info(
503
+ "Received tool approval response: %s",
504
+ summarize_tool_approval_response_for_logging(data),
505
+ )
506
+
507
+ logger.info(f"Processing approval: tool_call_id={sanitize_for_logging(tool_call_id)}, approved={approved}")
508
+
509
+ result = approval_manager.handle_approval_response(
510
+ tool_call_id=tool_call_id,
511
+ approved=approved,
512
+ arguments=arguments,
513
+ reason=reason
514
+ )
515
+
516
+ logger.info(f"Approval response handled: result={sanitize_for_logging(result)}")
517
+ # No response needed - the approval will unblock the waiting tool execution
518
+
519
+ elif message_type == "elicitation_response":
520
+ # Handle elicitation response
521
+ from atlas.application.chat.elicitation_manager import get_elicitation_manager
522
+ elicitation_manager = get_elicitation_manager()
523
+
524
+ elicitation_id = data.get("elicitation_id")
525
+ action = data.get("action", "cancel")
526
+ response_data = data.get("data")
527
+
528
+ logger.info(
529
+ f"Received elicitation response: id={sanitize_for_logging(elicitation_id)}, "
530
+ f"action={action}"
531
+ )
532
+
533
+ result = elicitation_manager.handle_elicitation_response(
534
+ elicitation_id=elicitation_id,
535
+ action=action,
536
+ data=response_data
537
+ )
538
+
539
+ logger.info(f"Elicitation response handled: result={sanitize_for_logging(result)}")
540
+ # No response needed - the elicitation will unblock the waiting tool execution
541
+
542
+ else:
543
+ logger.warning(f"Unknown message type: {sanitize_for_logging(message_type)}")
544
+ await websocket.send_json({
545
+ "type": "error",
546
+ "message": f"Unknown message type: {sanitize_for_logging(message_type)}"
547
+ })
548
+
549
+ except WebSocketDisconnect:
550
+ chat_service.end_session(session_id)
551
+ logger.info(f"WebSocket connection closed for session {session_id}")
552
+
553
+
554
+ if __name__ == "__main__":
555
+ import os
556
+
557
+ import uvicorn
558
+
559
+ # Use environment variable for host binding, default to localhost for security
560
+ # Set ATLAS_HOST=0.0.0.0 in production environments where needed
561
+ host = os.getenv("ATLAS_HOST", "127.0.0.1")
562
+ port = int(os.getenv("PORT", 8000))
563
+
564
+ uvicorn.run(app, host=host, port=port)
@@ -0,0 +1,76 @@
1
+ # API Key Demo MCP Server
2
+
3
+ Last updated: 2026-01-25
4
+
5
+ This MCP server demonstrates per-user API key authentication. It validates the `X-API-Key` header on all tool calls using FastMCP middleware.
6
+
7
+ ## Running the Server
8
+
9
+ ```bash
10
+ # From this directory
11
+ ./run.sh
12
+
13
+ # Or with custom port
14
+ ./run.sh 9000
15
+
16
+ # Or directly with Python
17
+ python main.py
18
+ ```
19
+
20
+ ## Valid Test Keys
21
+
22
+ For demo purposes, these API keys are accepted:
23
+ - `test123` (developer)
24
+ - `admin123` (admin)
25
+ - `demo-api-key-12345` (viewer)
26
+
27
+ ## Configuration in Atlas
28
+
29
+ Add to `config/overrides/mcp.json`:
30
+
31
+ ```json
32
+ {
33
+ "api_key_demo": {
34
+ "url": "http://127.0.0.1:8006/mcp",
35
+ "transport": "http",
36
+ "groups": ["users"],
37
+ "description": "API key authentication demo",
38
+ "auth_type": "api_key",
39
+ "auth_header": "X-API-Key",
40
+ "auth_prompt": "Enter your API key for the demo server"
41
+ }
42
+ }
43
+ ```
44
+
45
+ ## How It Works
46
+
47
+ 1. **Server Side**: The `ApiKeyAuthMiddleware` class intercepts all tool calls and validates the `X-API-Key` header against a set of valid keys.
48
+
49
+ 2. **Client Side**: Atlas UI detects `auth_type: "api_key"` in the config and prompts users to enter their API key via the TokenInputModal.
50
+
51
+ 3. **Storage**: User API keys are stored encrypted in `config/secure/mcp_tokens.enc` per-user per-server.
52
+
53
+ 4. **Injection**: When calling tools, Atlas creates a per-user MCP client with the API key injected via `StreamableHttpTransport(headers={"X-API-Key": key})`.
54
+
55
+ ## Available Tools
56
+
57
+ - `echo(message)` - Echo back a message
58
+ - `add_numbers(a, b)` - Add two numbers
59
+ - `get_user_data()` - Get sample protected data
60
+ - `list_valid_keys()` - List valid demo keys (for testing)
61
+
62
+ ## Testing with FastMCP Client
63
+
64
+ ```python
65
+ from fastmcp import Client
66
+ from fastmcp.client.transports import StreamableHttpTransport
67
+
68
+ transport = StreamableHttpTransport(
69
+ "http://localhost:8006/mcp",
70
+ headers={"X-API-Key": "demo-api-key-12345"}
71
+ )
72
+
73
+ async with Client(transport=transport) as client:
74
+ result = await client.call_tool("echo", {"message": "Hello!"})
75
+ print(result)
76
+ ```