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,204 @@
1
+ import os
2
+ import sys
3
+ import types
4
+
5
+ import pytest
6
+
7
+ # Ensure backend root is on path (same approach used in other tests)
8
+ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
9
+
10
+ # Patch fastmcp Client usage by MCPToolManager with a fake client/manager
11
+ @pytest.fixture(autouse=True)
12
+ def patch_mcp(monkeypatch):
13
+ from atlas.modules.mcp_tools.client import MCPToolManager
14
+
15
+ class FakeTool:
16
+ def __init__(self, name, description="", inputSchema=None):
17
+ self.name = name
18
+ self.description = description
19
+ self.inputSchema = inputSchema or {"type": "object", "properties": {"username": {"type": "string"}}}
20
+
21
+ class FakeClient:
22
+ def __init__(self, server_name):
23
+ self.server_name = server_name
24
+ async def __aenter__(self):
25
+ return self
26
+ async def __aexit__(self, exc_type, exc, tb):
27
+ return False
28
+ async def call_tool(self, tool_name, arguments, **kwargs):
29
+ # Provide deterministic results for test
30
+ if tool_name == "rag_discover_resources":
31
+ if self.server_name == "docsRag":
32
+ return types.SimpleNamespace(
33
+ structured_content={
34
+ "results": {
35
+ "resources": [
36
+ {"id": "handbook", "name": "Employee Handbook", "authRequired": True, "groups": ["hr"], "defaultSelected": True},
37
+ {"id": "legal", "name": "Legal Docs", "authRequired": True, "groups": ["legal"]},
38
+ ]
39
+ }
40
+ }
41
+ )
42
+ elif self.server_name == "notionRag":
43
+ return types.SimpleNamespace(
44
+ structured_content={
45
+ "results": {
46
+ "resources": [
47
+ {"id": "notion-space", "name": "Notion Space", "authRequired": True, "groups": ["notion"]}
48
+ ]
49
+ }
50
+ }
51
+ )
52
+ if tool_name == "rag_get_raw_results":
53
+ # Return hits with scores
54
+ return types.SimpleNamespace(
55
+ structured_content={
56
+ "results": {
57
+ "hits": [
58
+ {"id": f"{self.server_name}-1", "score": 0.9, "resourceId": arguments.get("sources", [""])[0]},
59
+ {"id": f"{self.server_name}-2", "score": 0.5, "resourceId": arguments.get("sources", [""])[-1]},
60
+ ],
61
+ "stats": {"top_k": arguments.get("top_k", 8)},
62
+ }
63
+ }
64
+ )
65
+ if tool_name == "rag_get_synthesized_results":
66
+ return types.SimpleNamespace(
67
+ structured_content={
68
+ "results": {
69
+ "answer": f"Answer from {self.server_name}",
70
+ "citations": [{"resourceId": r} for r in arguments.get("sources", [])],
71
+ }
72
+ }
73
+ )
74
+ return types.SimpleNamespace(structured_content={})
75
+
76
+ async def fake_initialize_clients(self):
77
+ # Pretend both servers are configured and online
78
+ self.clients = {"docsRag": FakeClient("docsRag"), "notionRag": FakeClient("notionRag")}
79
+
80
+ async def fake_discover_tools(self):
81
+ # Expose RAG tools on docsRag; notionRag only discovery/raw
82
+ self.available_tools = {
83
+ "docsRag": {
84
+ "tools": [FakeTool("rag_discover_resources"), FakeTool("rag_get_raw_results"), FakeTool("rag_get_synthesized_results")],
85
+ "config": {"description": "Docs RAG"},
86
+ },
87
+ "notionRag": {
88
+ "tools": [FakeTool("rag_discover_resources"), FakeTool("rag_get_raw_results")],
89
+ "config": {"description": "Notion RAG"},
90
+ },
91
+ }
92
+ # Also set servers config for UI fields
93
+ self.servers_config = {
94
+ "docsRag": {"description": "Docs RAG", "ui": {"icon": "book"}},
95
+ "notionRag": {"description": "Notion", "ui": {"icon": "notion"}},
96
+ }
97
+
98
+ async def fake_discover_prompts(self):
99
+ self.available_prompts = {}
100
+
101
+ async def fake_get_authorized_servers(self, user_email, auth_check_func):
102
+ # Simple ACL: allow both for @company.com; only docsRag otherwise
103
+ return ["docsRag", "notionRag"] if user_email.endswith("@company.com") else ["docsRag"]
104
+
105
+ async def fake_call_tool(self, server_name, tool_name, arguments, **kwargs):
106
+ return await self.clients[server_name].call_tool(tool_name, arguments)
107
+
108
+ monkeypatch.setattr(MCPToolManager, "initialize_clients", fake_initialize_clients, raising=False)
109
+ monkeypatch.setattr(MCPToolManager, "discover_tools", fake_discover_tools, raising=False)
110
+ monkeypatch.setattr(MCPToolManager, "discover_prompts", fake_discover_prompts, raising=False)
111
+ monkeypatch.setattr(MCPToolManager, "get_authorized_servers", fake_get_authorized_servers, raising=False)
112
+ monkeypatch.setattr(MCPToolManager, "call_tool", fake_call_tool, raising=False)
113
+
114
+ # Also patch rag_mcp_config to return the test RAG servers
115
+ # (RAGMCPService uses rag_mcp_config for authorization, not mcp_manager.servers_config)
116
+ from atlas.modules.config.config_manager import ConfigManager, MCPConfig, MCPServerConfig
117
+ fake_rag_servers = {
118
+ "docsRag": MCPServerConfig(
119
+ description="Docs RAG",
120
+ enabled=True,
121
+ groups=["users"], # Everyone is in users group
122
+ ),
123
+ "notionRag": MCPServerConfig(
124
+ description="Notion RAG",
125
+ enabled=True,
126
+ groups=["company"], # Only company users
127
+ ),
128
+ }
129
+ fake_rag_mcp_config = MCPConfig(servers=fake_rag_servers)
130
+ monkeypatch.setattr(ConfigManager, "rag_mcp_config", property(lambda self: fake_rag_mcp_config))
131
+
132
+ # Patch is_user_in_group to simulate domain-based access
133
+ # @company.com users are in "company" group, everyone is in "users" group
134
+ async def fake_is_user_in_group(user_id: str, group_id: str) -> bool:
135
+ if group_id == "users":
136
+ return True
137
+ if group_id == "company":
138
+ return user_id.endswith("@company.com")
139
+ return False
140
+
141
+ from atlas.core import auth as core_auth
142
+ monkeypatch.setattr(core_auth, "is_user_in_group", fake_is_user_in_group)
143
+
144
+
145
+ @pytest.mark.asyncio
146
+ async def test_discovery_across_multiple_servers():
147
+ from atlas.infrastructure.app_factory import app_factory
148
+ # Initialize MCP
149
+ mcp = app_factory.get_mcp_manager()
150
+ await mcp.initialize_clients()
151
+ await mcp.discover_tools()
152
+ await mcp.discover_prompts()
153
+
154
+ from atlas.core.auth import is_user_in_group
155
+ from atlas.domain.rag_mcp_service import RAGMCPService
156
+
157
+ svc = RAGMCPService(mcp, app_factory.get_config_manager(), is_user_in_group)
158
+ # user with @company.com gets both servers
159
+ sources = await svc.discover_data_sources("alice@company.com")
160
+ assert "docsRag:handbook" in sources
161
+ assert "docsRag:legal" in sources
162
+ assert "notionRag:notion-space" in sources
163
+
164
+ # richer servers
165
+ servers = await svc.discover_servers("alice@company.com")
166
+ assert any(s["server"] == "docsRag" and s["sources"] for s in servers)
167
+
168
+
169
+ @pytest.mark.asyncio
170
+ async def test_acl_filtering():
171
+ from atlas.core.auth import is_user_in_group
172
+ from atlas.domain.rag_mcp_service import RAGMCPService
173
+ from atlas.infrastructure.app_factory import app_factory
174
+
175
+ mcp = app_factory.get_mcp_manager()
176
+ await mcp.initialize_clients()
177
+ await mcp.discover_tools()
178
+ await mcp.discover_prompts()
179
+
180
+ svc = RAGMCPService(mcp, app_factory.get_config_manager(), is_user_in_group)
181
+ # Non-company user only sees docsRag
182
+ sources = await svc.discover_data_sources("bob@public.net")
183
+ assert all(s.startswith("docsRag:") for s in sources)
184
+
185
+
186
+ @pytest.mark.asyncio
187
+ async def test_search_and_synthesize_merge():
188
+ from atlas.core.auth import is_user_in_group
189
+ from atlas.domain.rag_mcp_service import RAGMCPService
190
+ from atlas.infrastructure.app_factory import app_factory
191
+
192
+ mcp = app_factory.get_mcp_manager()
193
+ await mcp.initialize_clients()
194
+ await mcp.discover_tools()
195
+ await mcp.discover_prompts()
196
+
197
+ svc = RAGMCPService(mcp, app_factory.get_config_manager(), is_user_in_group)
198
+ sources = ["docsRag:handbook", "notionRag:notion-space"]
199
+ res = await svc.search_raw("alice@company.com", "vacation policy", sources, top_k=1)
200
+ assert "results" in res and "hits" in res["results"]
201
+ assert len(res["results"]["hits"]) == 1 # limited by top_k
202
+
203
+ syn = await svc.synthesize("alice@company.com", "vacation policy", sources, top_k=2)
204
+ assert "results" in syn and "answer" in syn["results"]
@@ -0,0 +1,224 @@
1
+ import os
2
+ import sys
3
+ import types
4
+ from typing import Any, Dict, List
5
+
6
+ import pytest
7
+
8
+ # Ensure backend root is on path (same approach used in other tests)
9
+ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
10
+
11
+
12
+ class FakeTool:
13
+ def __init__(self, name: str):
14
+ self.name = name
15
+
16
+
17
+ class FakeMCP:
18
+ def __init__(self):
19
+ # Simulate available tools config per server
20
+ self.available_tools: Dict[str, Dict[str, Any]] = {
21
+ "docsRag": {"tools": [FakeTool("rag_discover_resources"), FakeTool("rag_get_raw_results")], "config": {"ui": {"icon": "book"}}},
22
+ "searchRag": {"tools": [FakeTool("rag_discover_resources"), FakeTool("rag_get_raw_results"), FakeTool("rag_get_synthesized_results")], "config": {}},
23
+ "misc": {"tools": [FakeTool("other")], "config": {}},
24
+ }
25
+
26
+ async def get_authorized_servers(self, user: str, _auth) -> List[str]:
27
+ # User bob can see all, alice cannot see misc
28
+ if user.startswith("alice"):
29
+ return ["docsRag", "searchRag"]
30
+ return list(self.available_tools.keys())
31
+
32
+ async def call_tool(self, server_name: str, tool_name: str, arguments: Dict[str, Any], *_, **__):
33
+ # Return minimal v2-structured payloads
34
+ if tool_name == "rag_discover_resources":
35
+ if server_name == "docsRag":
36
+ return types.SimpleNamespace(structured_content={
37
+ "results": {"resources": [
38
+ {"id": "handbook", "name": "Employee Handbook", "authRequired": True, "groups": ["hr"], "defaultSelected": True},
39
+ {"id": "legal", "name": "Legal Docs", "authRequired": True, "groups": ["legal"]},
40
+ ]}
41
+ })
42
+ if server_name == "searchRag":
43
+ return types.SimpleNamespace(structured_content={
44
+ "results": {"resources": [
45
+ {"id": "kb", "name": "KB", "authRequired": True, "groups": ["kb"]}
46
+ ]}
47
+ })
48
+ if tool_name == "rag_get_raw_results":
49
+ q = arguments.get("query")
50
+ srcs = arguments.get("sources", [])
51
+ hits = []
52
+ for i, s in enumerate(srcs):
53
+ hits.append({
54
+ "id": f"{server_name}-{s}-{i}",
55
+ "score": 1.0 - i * 0.01,
56
+ "resourceId": f"{server_name}:{s}",
57
+ "title": f"{q} in {s}",
58
+ })
59
+ return types.SimpleNamespace(structured_content={"results": {"hits": hits}})
60
+ if tool_name == "rag_get_synthesized_results":
61
+ return types.SimpleNamespace(structured_content={
62
+ "results": {"answer": f"Synth for {arguments.get('query')} by {server_name}"}
63
+ })
64
+ return types.SimpleNamespace(structured_content={})
65
+
66
+
67
+ class FakeMCPServerConfig:
68
+ """Minimal server config for testing."""
69
+ def __init__(self, enabled=True, groups=None):
70
+ self.enabled = enabled
71
+ self.groups = groups or []
72
+
73
+
74
+ class FakeMCPConfig:
75
+ """Minimal MCP config for testing."""
76
+ def __init__(self, servers=None):
77
+ self.servers = servers or {}
78
+
79
+
80
+ class FakeConfig:
81
+ """Fake config manager for testing RAGMCPService."""
82
+ def __init__(self, rag_servers=None):
83
+ # Default RAG servers matching FakeMCP.available_tools
84
+ if rag_servers is None:
85
+ rag_servers = {
86
+ "docsRag": FakeMCPServerConfig(enabled=True, groups=["users"]),
87
+ "searchRag": FakeMCPServerConfig(enabled=True, groups=["users"]),
88
+ "misc": FakeMCPServerConfig(enabled=True, groups=["users"]),
89
+ }
90
+ self._rag_mcp_config = FakeMCPConfig(servers=rag_servers)
91
+
92
+ @property
93
+ def rag_mcp_config(self):
94
+ return self._rag_mcp_config
95
+
96
+
97
+ async def fake_auth_check(user: str, group: str) -> bool:
98
+ """Default auth check - everyone is in 'users' group."""
99
+ return group == "users"
100
+
101
+
102
+ @pytest.mark.asyncio
103
+ async def test_discovery_flat_and_rich():
104
+ from atlas.domain.rag_mcp_service import RAGMCPService
105
+
106
+ svc = RAGMCPService(FakeMCP(), FakeConfig(), fake_auth_check)
107
+
108
+ flat = await svc.discover_data_sources("bob@example.com")
109
+ # misc has no rag_discover_resources, excluded
110
+ assert set(flat) == {"docsRag:handbook", "docsRag:legal", "searchRag:kb"}
111
+
112
+ rich = await svc.discover_servers("alice@example.com")
113
+ servers = {d["server"] for d in rich}
114
+ assert servers == {"docsRag", "searchRag"}
115
+ # docsRag must include two sources with defaultSelected on handbook
116
+ dr = next(s for s in rich if s["server"] == "docsRag")
117
+ assert any(x.get("selected") for x in dr["sources"]) # handbook default selected
118
+
119
+
120
+ @pytest.mark.asyncio
121
+ async def test_search_and_synthesize_merge():
122
+ from atlas.domain.rag_mcp_service import RAGMCPService
123
+
124
+ svc = RAGMCPService(FakeMCP(), FakeConfig(), fake_auth_check)
125
+ res = await svc.search_raw(
126
+ username="bob@example.com",
127
+ query="policy",
128
+ sources=["docsRag:handbook", "searchRag:kb"],
129
+ top_k=2,
130
+ )
131
+ hits = res.get("results", {}).get("hits", [])
132
+ assert len(hits) == 2
133
+ # resourceId should be qualified
134
+ assert all(":" in h.get("resourceId", "") for h in hits)
135
+
136
+ syn = await svc.synthesize(
137
+ username="alice@example.com",
138
+ query="benefits",
139
+ sources=["searchRag:kb"],
140
+ )
141
+ answer = syn.get("results", {}).get("answer")
142
+ assert isinstance(answer, str) and "Synth for" in answer
143
+
144
+
145
+ @pytest.mark.asyncio
146
+ async def test_rag_authorization_uses_rag_config_not_mcp_servers_config():
147
+ """
148
+ Regression test: RAG authorization must use rag_mcp_config, not mcp_manager.servers_config.
149
+
150
+ Bug context: RAGMCPService temporarily adds RAG servers to mcp_manager.servers_config
151
+ for initialization, then restores the original config. If authorization checks
152
+ use servers_config (which no longer has RAG servers), no RAG sources are returned.
153
+
154
+ The fix is to check authorization directly against rag_mcp_config.servers.
155
+ """
156
+ from atlas.domain.rag_mcp_service import RAGMCPService
157
+
158
+ class MCPWithEmptyServersConfig:
159
+ """MCP manager with empty servers_config (RAG servers only in rag_mcp_config)."""
160
+ def __init__(self):
161
+ # Simulate RAG servers being initialized but NOT in servers_config
162
+ self.servers_config = {} # Empty! RAG servers were temporarily added then removed
163
+ self.clients = {"ragServer": object()} # Client exists (was initialized)
164
+ self.available_tools = {
165
+ "ragServer": {
166
+ "tools": [FakeTool("rag_discover_resources")],
167
+ "config": {}
168
+ }
169
+ }
170
+
171
+ async def get_authorized_servers(self, user: str, _auth) -> List[str]:
172
+ # This would return [] because servers_config is empty
173
+ return list(self.servers_config.keys())
174
+
175
+ async def call_tool(self, server_name: str, tool_name: str, arguments: Dict[str, Any], *_, **__):
176
+ if tool_name == "rag_discover_resources":
177
+ return types.SimpleNamespace(structured_content={
178
+ "results": {"resources": [
179
+ {"id": "doc1", "name": "Document 1"}
180
+ ]}
181
+ })
182
+ return types.SimpleNamespace(structured_content={})
183
+
184
+ # RAG server configured in rag_mcp_config
185
+ rag_servers = {
186
+ "ragServer": FakeMCPServerConfig(enabled=True, groups=["users"])
187
+ }
188
+ fake_config = FakeConfig(rag_servers=rag_servers)
189
+
190
+ svc = RAGMCPService(MCPWithEmptyServersConfig(), fake_config, fake_auth_check)
191
+
192
+ # This should find ragServer even though mcp_manager.servers_config is empty
193
+ flat = await svc.discover_data_sources("user@example.com")
194
+
195
+ # Before the fix, this would return [] because authorization checked servers_config
196
+ # After the fix, this returns the RAG sources because authorization uses rag_mcp_config
197
+ assert "ragServer:doc1" in flat, \
198
+ "RAG authorization should use rag_mcp_config, not mcp_manager.servers_config"
199
+
200
+
201
+ @pytest.mark.asyncio
202
+ async def test_rag_group_filtering():
203
+ """Test that RAG sources are properly filtered by group membership."""
204
+ from atlas.domain.rag_mcp_service import RAGMCPService
205
+
206
+ # Server requires 'admin' group, not 'users'
207
+ rag_servers = {
208
+ "docsRag": FakeMCPServerConfig(enabled=True, groups=["admin"]),
209
+ "searchRag": FakeMCPServerConfig(enabled=True, groups=["users"]),
210
+ "misc": FakeMCPServerConfig(enabled=True, groups=["users"]),
211
+ }
212
+
213
+ async def restricted_auth_check(user: str, group: str) -> bool:
214
+ # User is only in 'users' group, not 'admin'
215
+ return group == "users"
216
+
217
+ svc = RAGMCPService(FakeMCP(), FakeConfig(rag_servers=rag_servers), restricted_auth_check)
218
+
219
+ flat = await svc.discover_data_sources("user@example.com")
220
+
221
+ # docsRag requires 'admin' group, user is not in admin
222
+ assert "docsRag:handbook" not in flat, "Admin-only RAG server should not be visible"
223
+ # searchRag is in 'users' group
224
+ assert "searchRag:kb" in flat, "User-accessible RAG server should be visible"
@@ -0,0 +1,45 @@
1
+ from fastapi import FastAPI
2
+ from starlette.testclient import TestClient
3
+
4
+ from atlas.core.rate_limit_middleware import RateLimitMiddleware
5
+ from atlas.modules.config import config_manager
6
+
7
+
8
+ def test_rate_limit_blocks_after_threshold():
9
+ # Configure very low limits via ConfigManager
10
+ settings = config_manager.app_settings
11
+ orig_rpm = settings.rate_limit_rpm
12
+ orig_window = settings.rate_limit_window_seconds
13
+ orig_per_path = settings.rate_limit_per_path
14
+
15
+ settings.rate_limit_rpm = 2
16
+ settings.rate_limit_window_seconds = 60
17
+ settings.rate_limit_per_path = False
18
+
19
+ try:
20
+ app = FastAPI()
21
+ app.add_middleware(RateLimitMiddleware)
22
+
23
+ @app.get("/ping")
24
+ def ping():
25
+ return {"ok": True}
26
+
27
+ client = TestClient(app)
28
+
29
+ # First two requests should pass
30
+ r1 = client.get("/ping")
31
+ assert r1.status_code == 200
32
+ r2 = client.get("/ping")
33
+ assert r2.status_code == 200
34
+
35
+ # Third request within the window should be rate-limited
36
+ r3 = client.get("/ping")
37
+ assert r3.status_code == 429
38
+ data = r3.json()
39
+ assert "detail" in data
40
+ assert "Retry-After" in r3.headers
41
+ finally:
42
+ # Restore original settings to avoid side effects on other tests
43
+ settings.rate_limit_rpm = orig_rpm
44
+ settings.rate_limit_window_seconds = orig_window
45
+ settings.rate_limit_per_path = orig_per_path
@@ -0,0 +1,60 @@
1
+ from unittest.mock import AsyncMock, MagicMock, patch
2
+
3
+ from main import app
4
+ from starlette.testclient import TestClient
5
+
6
+ from atlas.infrastructure.app_factory import app_factory
7
+
8
+
9
+ def test_config_endpoint_smoke(monkeypatch):
10
+ client = TestClient(app)
11
+ resp = client.get("/api/config", headers={"X-User-Email": "test@test.com"})
12
+ # Endpoint should not crash; tolerate 200 with minimal fields
13
+ assert resp.status_code == 200
14
+ data = resp.json()
15
+ assert "app_name" in data
16
+ assert "models" in data
17
+ assert "tools" in data
18
+ assert "prompts" in data
19
+ assert "data_sources" in data
20
+
21
+
22
+ def test_rag_discovery_skipped_when_feature_disabled(monkeypatch):
23
+ """Verify RAG discovery is not attempted when feature_rag_enabled is False."""
24
+ # Create mock unified_rag_service to track if discover_data_sources is called
25
+ mock_unified_rag = MagicMock()
26
+ mock_unified_rag.discover_data_sources = AsyncMock(return_value=[])
27
+
28
+ # Create mock rag_mcp_service
29
+ mock_rag_mcp = MagicMock()
30
+ mock_rag_mcp.discover_data_sources = AsyncMock(return_value=[])
31
+ mock_rag_mcp.discover_servers = AsyncMock(return_value=[])
32
+
33
+ # Patch the app_factory methods
34
+ with patch.object(app_factory, 'get_unified_rag_service', return_value=mock_unified_rag):
35
+ with patch.object(app_factory, 'get_rag_mcp_service', return_value=mock_rag_mcp):
36
+ # Ensure RAG feature is disabled
37
+ config_manager = app_factory.get_config_manager()
38
+ original_setting = config_manager.app_settings.feature_rag_enabled
39
+ # Use object.__setattr__ to bypass Pydantic frozen model protection
40
+ object.__setattr__(config_manager.app_settings, 'feature_rag_enabled', False)
41
+
42
+ try:
43
+ client = TestClient(app)
44
+ resp = client.get("/api/config", headers={"X-User-Email": "test@test.com"})
45
+ assert resp.status_code == 200
46
+
47
+ # Verify RAG discovery was NOT called when feature is disabled
48
+ mock_unified_rag.discover_data_sources.assert_not_called()
49
+ mock_rag_mcp.discover_data_sources.assert_not_called()
50
+ mock_rag_mcp.discover_servers.assert_not_called()
51
+
52
+ # Verify response still has data_sources field (just empty)
53
+ data = resp.json()
54
+ assert "data_sources" in data
55
+ assert data["data_sources"] == []
56
+ assert "rag_servers" in data
57
+ assert data["rag_servers"] == []
58
+ finally:
59
+ # Restore original setting
60
+ object.__setattr__(config_manager.app_settings, 'feature_rag_enabled', original_setting)
@@ -0,0 +1,41 @@
1
+ import base64
2
+
3
+ from main import app
4
+ from starlette.testclient import TestClient
5
+
6
+ from atlas.core.capabilities import generate_file_token
7
+
8
+
9
+ def test_files_download_with_token(monkeypatch):
10
+ client = TestClient(app)
11
+
12
+ # Prepare fake file in mock S3 by monkeypatching S3 client get_file
13
+ from atlas.infrastructure.app_factory import app_factory
14
+ s3 = app_factory.get_file_storage()
15
+
16
+ async def fake_get_file(user, key):
17
+ return {
18
+ "key": key,
19
+ "filename": "hello.txt",
20
+ "content_base64": base64.b64encode(b"hello").decode(),
21
+ "content_type": "text/plain",
22
+ "size": 5,
23
+ "last_modified": "",
24
+ "etag": "",
25
+ "tags": {},
26
+ "user_email": user,
27
+ }
28
+
29
+ monkeypatch.setattr(s3, "get_file", fake_get_file)
30
+
31
+ token = generate_file_token(user_email="test@test.com", file_key="k1", ttl_seconds=60)
32
+
33
+ resp = client.get(
34
+ "/api/files/download/k1",
35
+ params={"token": token},
36
+ headers={"X-User-Email": "ignored@example.com"}, # token overrides
37
+ )
38
+ assert resp.status_code == 200
39
+ assert resp.content == b"hello"
40
+ ct = resp.headers.get("content-type", "")
41
+ assert ct.startswith("text/plain")
@@ -0,0 +1,18 @@
1
+ from main import app
2
+ from starlette.testclient import TestClient
3
+
4
+
5
+ def test_files_health_endpoint(monkeypatch):
6
+ # Stub out S3 client call to avoid external dependency sensitivity
7
+ from atlas.infrastructure.app_factory import app_factory
8
+ s3 = app_factory.get_file_storage()
9
+ # No network call is made by files/health; but ensure attributes exist
10
+ assert hasattr(s3, "endpoint_url")
11
+
12
+ client = TestClient(app)
13
+ resp = client.get("/api/files/healthz", headers={"X-User-Email": "test@test.com"})
14
+ assert resp.status_code == 200
15
+ data = resp.json()
16
+ assert data["status"] == "healthy"
17
+ assert data["service"] == "files-api"
18
+ assert "s3_config" in data
@@ -0,0 +1,53 @@
1
+ import os
2
+ import subprocess
3
+ import sys
4
+ from pathlib import Path
5
+
6
+
7
+ def run_subprocess(code: str, cwd: Path, project_root: Path = None):
8
+ env = os.environ.copy()
9
+ # For the atlas package structure, ensure project root is in PYTHONPATH
10
+ if project_root:
11
+ env["PYTHONPATH"] = str(project_root)
12
+ else:
13
+ env.pop("PYTHONPATH", None)
14
+ proc = subprocess.run(
15
+ [sys.executable, "-c", code],
16
+ cwd=str(cwd),
17
+ env=env,
18
+ capture_output=True,
19
+ text=True,
20
+ timeout=30,
21
+ )
22
+ return proc
23
+
24
+
25
+ def test_backend_dir_imports_work_without_project_root_in_path():
26
+ """
27
+ Ensure imports work when running from the atlas directory (the supported run mode).
28
+ The atlas package requires the project root on PYTHONPATH for proper package imports.
29
+ This mirrors `bash agent_start.sh` which sets PYTHONPATH before running from ./atlas.
30
+ """
31
+ atlas_dir = Path(__file__).resolve().parents[1]
32
+ project_root = atlas_dir.parent
33
+
34
+ code = (
35
+ "import main; "
36
+ "from atlas.modules.config import ConfigManager; "
37
+ "cm=ConfigManager(); "
38
+ "_ = cm.llm_config; _ = cm.mcp_config; _ = cm.rag_mcp_config; "
39
+ "print('OK')"
40
+ )
41
+
42
+ proc = run_subprocess(code, atlas_dir, project_root=project_root)
43
+
44
+ # Helpful diagnostics on failure
45
+ if proc.returncode != 0:
46
+ print("STDOUT:\n" + proc.stdout)
47
+ print("STDERR:\n" + proc.stderr)
48
+
49
+ assert proc.returncode == 0, "Subprocess failed to import and initialize config from atlas dir"
50
+ # Guard against the specific regression seen in runtime warnings
51
+ assert "No module named 'atlas'" not in (proc.stdout + proc.stderr)
52
+ assert "Could not validate LLM compliance levels" not in (proc.stdout + proc.stderr)
53
+ assert "Could not validate MCP compliance levels" not in (proc.stdout + proc.stderr)