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,356 @@
1
+ """Tests for tool approval utilities in tool_executor.py"""
2
+
3
+ from unittest.mock import Mock
4
+
5
+ from atlas.application.chat.utilities.tool_executor import (
6
+ _filter_args_to_schema,
7
+ _sanitize_args_for_ui,
8
+ requires_approval,
9
+ tool_accepts_username,
10
+ )
11
+
12
+
13
+ class MockToolConfig:
14
+ """Mock tool configuration."""
15
+ def __init__(self, require_approval, allow_edit):
16
+ self.require_approval = require_approval
17
+ self.allow_edit = allow_edit
18
+
19
+
20
+ class MockApprovalsConfig:
21
+ """Mock approvals configuration."""
22
+ def __init__(self, require_by_default=True, tools=None):
23
+ self.require_approval_by_default = require_by_default
24
+ self.tools = tools or {}
25
+
26
+
27
+ class TestRequiresApproval:
28
+ """Test the requires_approval function."""
29
+
30
+ def test_requires_approval_no_config_manager(self):
31
+ """Test requires_approval with no config manager."""
32
+ needs_approval, allow_edit, admin_required = requires_approval("test_tool", None)
33
+
34
+ assert needs_approval is True
35
+ assert allow_edit is True
36
+ assert admin_required is False
37
+
38
+ def test_requires_approval_tool_specific_config(self):
39
+ """Test requires_approval with tool-specific configuration."""
40
+ config_manager = Mock()
41
+ config_manager.tool_approvals_config = MockApprovalsConfig(
42
+ require_by_default=False,
43
+ tools={
44
+ "dangerous_tool": MockToolConfig(require_approval=True, allow_edit=False)
45
+ }
46
+ )
47
+
48
+ needs_approval, allow_edit, admin_required = requires_approval("dangerous_tool", config_manager)
49
+
50
+ assert needs_approval is True
51
+ # UI is always editable when approval is required
52
+ assert allow_edit is True
53
+ assert admin_required is True
54
+
55
+ def test_requires_approval_default_true(self):
56
+ """Test requires_approval with default set to require approval.
57
+
58
+ REQUIRE_TOOL_APPROVAL_BY_DEFAULT=true should be user-level (admin_required=False)
59
+ so users can toggle auto-approve via the inline UI.
60
+ """
61
+ config_manager = Mock()
62
+ config_manager.tool_approvals_config = MockApprovalsConfig(
63
+ require_by_default=True,
64
+ tools={}
65
+ )
66
+
67
+ needs_approval, allow_edit, admin_required = requires_approval("any_tool", config_manager)
68
+
69
+ assert needs_approval is True
70
+ assert allow_edit is True
71
+ assert admin_required is False # User CAN toggle auto-approve
72
+
73
+ def test_requires_approval_default_false(self):
74
+ """Test requires_approval with default set to not require approval."""
75
+ config_manager = Mock()
76
+ config_manager.tool_approvals_config = MockApprovalsConfig(
77
+ require_by_default=False,
78
+ tools={}
79
+ )
80
+
81
+ needs_approval, allow_edit, admin_required = requires_approval("any_tool", config_manager)
82
+
83
+ # Default is False but function returns True with user-level approval
84
+ assert needs_approval is True
85
+ assert allow_edit is True
86
+ assert admin_required is False
87
+
88
+ def test_requires_approval_exception_handling(self):
89
+ """Test requires_approval handles exceptions gracefully."""
90
+ config_manager = Mock()
91
+ config_manager.tool_approvals_config = None
92
+
93
+ # Should not raise, should return default
94
+ needs_approval, allow_edit, admin_required = requires_approval("test_tool", config_manager)
95
+
96
+ assert needs_approval is True
97
+ assert allow_edit is True
98
+ assert admin_required is False
99
+
100
+ def test_requires_approval_multiple_tools(self):
101
+ """Test requires_approval with multiple tool configurations."""
102
+ config_manager = Mock()
103
+ config_manager.tool_approvals_config = MockApprovalsConfig(
104
+ require_by_default=False,
105
+ tools={
106
+ "tool_a": MockToolConfig(require_approval=True, allow_edit=True),
107
+ "tool_b": MockToolConfig(require_approval=True, allow_edit=False),
108
+ "tool_c": MockToolConfig(require_approval=False, allow_edit=True)
109
+ }
110
+ )
111
+
112
+ # Tool A
113
+ needs_approval, allow_edit, admin_required = requires_approval("tool_a", config_manager)
114
+ assert needs_approval is True
115
+ assert allow_edit is True
116
+ assert admin_required is True
117
+
118
+ # Tool B (allow_edit False in config is ignored for UI gating)
119
+ needs_approval, allow_edit, admin_required = requires_approval("tool_b", config_manager)
120
+ assert needs_approval is True
121
+ assert allow_edit is True
122
+ assert admin_required is True
123
+
124
+ # Tool C: with Option B, entries with require_approval=False are not
125
+ # considered explicit; fall back to default (which is False here),
126
+ # resulting in user-level approval required by design.
127
+ config_manager2 = Mock()
128
+ config_manager2.tool_approvals_config = MockApprovalsConfig(
129
+ require_by_default=False,
130
+ tools={
131
+ "tool_a": MockToolConfig(require_approval=True, allow_edit=True),
132
+ "tool_b": MockToolConfig(require_approval=True, allow_edit=False),
133
+ # tool_c omitted to simulate Option B config building
134
+ }
135
+ )
136
+ needs_approval, allow_edit, admin_required = requires_approval("tool_c", config_manager2)
137
+ assert needs_approval is True
138
+ assert allow_edit is True
139
+ assert admin_required is False
140
+
141
+
142
+ class TestToolAcceptsUsername:
143
+ """Test the tool_accepts_username function."""
144
+
145
+ def test_tool_accepts_username_true(self):
146
+ """Test tool that accepts username parameter."""
147
+ tool_manager = Mock()
148
+ tool_manager.get_tools_schema.return_value = [
149
+ {
150
+ "function": {
151
+ "name": "test_tool",
152
+ "parameters": {
153
+ "properties": {
154
+ "username": {"type": "string"},
155
+ "other_param": {"type": "string"}
156
+ }
157
+ }
158
+ }
159
+ }
160
+ ]
161
+
162
+ result = tool_accepts_username("test_tool", tool_manager)
163
+ assert result is True
164
+
165
+ def test_tool_accepts_username_false(self):
166
+ """Test tool that does not accept username parameter."""
167
+ tool_manager = Mock()
168
+ tool_manager.get_tools_schema.return_value = [
169
+ {
170
+ "function": {
171
+ "name": "test_tool",
172
+ "parameters": {
173
+ "properties": {
174
+ "other_param": {"type": "string"}
175
+ }
176
+ }
177
+ }
178
+ }
179
+ ]
180
+
181
+ result = tool_accepts_username("test_tool", tool_manager)
182
+ assert result is False
183
+
184
+ def test_tool_accepts_username_no_tool_manager(self):
185
+ """Test with no tool manager."""
186
+ result = tool_accepts_username("test_tool", None)
187
+ assert result is False
188
+
189
+ def test_tool_accepts_username_no_schema(self):
190
+ """Test when tool schema is not found."""
191
+ tool_manager = Mock()
192
+ tool_manager.get_tools_schema.return_value = []
193
+
194
+ result = tool_accepts_username("test_tool", tool_manager)
195
+ assert result is False
196
+
197
+ def test_tool_accepts_username_exception(self):
198
+ """Test exception handling."""
199
+ tool_manager = Mock()
200
+ tool_manager.get_tools_schema.side_effect = Exception("Schema error")
201
+
202
+ result = tool_accepts_username("test_tool", tool_manager)
203
+ assert result is False
204
+
205
+
206
+ class TestSanitizeArgsForUI:
207
+ """Test the _sanitize_args_for_ui function."""
208
+
209
+ def test_sanitize_simple_args(self):
210
+ """Test sanitizing simple arguments."""
211
+ args = {"param1": "value1", "param2": "value2"}
212
+ result = _sanitize_args_for_ui(args)
213
+
214
+ assert result == args
215
+
216
+ def test_sanitize_filename(self):
217
+ """Test sanitizing filename with URL."""
218
+ args = {"filename": "http://example.com/path/file.txt?token=secret"}
219
+ result = _sanitize_args_for_ui(args)
220
+
221
+ # Should extract just the filename
222
+ assert "token" not in result["filename"]
223
+ assert "file.txt" in result["filename"]
224
+
225
+ def test_sanitize_file_names_list(self):
226
+ """Test sanitizing list of filenames."""
227
+ args = {
228
+ "file_names": [
229
+ "http://example.com/file1.txt?token=abc",
230
+ "http://example.com/file2.txt?token=def"
231
+ ]
232
+ }
233
+ result = _sanitize_args_for_ui(args)
234
+
235
+ assert len(result["file_names"]) == 2
236
+ for filename in result["file_names"]:
237
+ assert "token" not in filename
238
+
239
+ def test_sanitize_file_url(self):
240
+ """Test sanitizing file_url field."""
241
+ args = {"file_url": "http://example.com/path/file.txt?token=secret"}
242
+ result = _sanitize_args_for_ui(args)
243
+
244
+ assert "token" not in result["file_url"]
245
+
246
+ def test_sanitize_file_urls_list(self):
247
+ """Test sanitizing file_urls list."""
248
+ args = {
249
+ "file_urls": [
250
+ "http://example.com/file1.txt?token=abc",
251
+ "http://example.com/file2.txt?token=def"
252
+ ]
253
+ }
254
+ result = _sanitize_args_for_ui(args)
255
+
256
+ assert len(result["file_urls"]) == 2
257
+ for url in result["file_urls"]:
258
+ assert "token" not in url
259
+
260
+ def test_sanitize_mixed_args(self):
261
+ """Test sanitizing mixed arguments."""
262
+ args = {
263
+ "filename": "http://example.com/file.txt?token=secret",
264
+ "other_param": "normal_value",
265
+ "file_names": ["file1.txt", "file2.txt"]
266
+ }
267
+ result = _sanitize_args_for_ui(args)
268
+
269
+ assert "token" not in result["filename"]
270
+ assert result["other_param"] == "normal_value"
271
+ assert len(result["file_names"]) == 2
272
+
273
+
274
+ class TestFilterArgsToSchema:
275
+ """Test the _filter_args_to_schema function."""
276
+
277
+ def test_filter_with_schema(self):
278
+ """Test filtering arguments with available schema."""
279
+ tool_manager = Mock()
280
+ tool_manager.get_tools_schema.return_value = [
281
+ {
282
+ "function": {
283
+ "name": "test_tool",
284
+ "parameters": {
285
+ "properties": {
286
+ "allowed_param": {"type": "string"},
287
+ "another_param": {"type": "number"}
288
+ }
289
+ }
290
+ }
291
+ }
292
+ ]
293
+
294
+ args = {
295
+ "allowed_param": "value",
296
+ "another_param": 42,
297
+ "original_filename": "old.txt",
298
+ "file_url": "http://example.com/file.txt",
299
+ "extra_param": "should_be_removed"
300
+ }
301
+
302
+ result = _filter_args_to_schema(args, "test_tool", tool_manager)
303
+
304
+ assert "allowed_param" in result
305
+ assert "another_param" in result
306
+ assert "original_filename" not in result
307
+ assert "file_url" not in result
308
+ assert "extra_param" not in result
309
+
310
+ def test_filter_without_schema(self):
311
+ """Test filtering when schema is unavailable."""
312
+ tool_manager = Mock()
313
+ tool_manager.get_tools_schema.return_value = []
314
+
315
+ args = {
316
+ "param": "value",
317
+ "original_filename": "old.txt",
318
+ "file_url": "http://example.com/file.txt"
319
+ }
320
+
321
+ result = _filter_args_to_schema(args, "test_tool", tool_manager)
322
+
323
+ # Should keep param but drop original_* and file_url(s)
324
+ assert "param" in result
325
+ assert "original_filename" not in result
326
+ assert "file_url" not in result
327
+
328
+ def test_filter_no_tool_manager(self):
329
+ """Test filtering with no tool manager."""
330
+ args = {
331
+ "param": "value",
332
+ "original_something": "should_be_removed",
333
+ "file_urls": ["url1", "url2"]
334
+ }
335
+
336
+ result = _filter_args_to_schema(args, "test_tool", None)
337
+
338
+ assert "param" in result
339
+ assert "original_something" not in result
340
+ assert "file_urls" not in result
341
+
342
+ def test_filter_exception_handling(self):
343
+ """Test filtering handles exceptions gracefully."""
344
+ tool_manager = Mock()
345
+ tool_manager.get_tools_schema.side_effect = Exception("Schema error")
346
+
347
+ args = {
348
+ "param": "value",
349
+ "original_param": "remove_me"
350
+ }
351
+
352
+ result = _filter_args_to_schema(args, "test_tool", tool_manager)
353
+
354
+ # Should fall back to conservative filtering
355
+ assert "param" in result
356
+ assert "original_param" not in result
@@ -0,0 +1,223 @@
1
+ """Test ToolAuthorizationService group filtering.
2
+
3
+ This test verifies that MCP server group restrictions are properly enforced
4
+ during tool authorization in chat execution.
5
+ """
6
+
7
+ from unittest.mock import patch
8
+
9
+ import pytest
10
+
11
+ from atlas.application.chat.policies.tool_authorization import ToolAuthorizationService
12
+
13
+
14
+ class MockToolManager:
15
+ """Mock tool manager with configurable server configs."""
16
+
17
+ def __init__(self, servers_config: dict):
18
+ self.servers_config = servers_config
19
+
20
+ async def get_authorized_servers(self, user_email: str, auth_check_func) -> list:
21
+ """Get list of servers the user is authorized to use."""
22
+ if auth_check_func is None:
23
+ raise TypeError("auth_check_func cannot be None")
24
+
25
+ authorized_servers = []
26
+ for server_name, server_config in self.servers_config.items():
27
+ if not server_config.get("enabled", True):
28
+ continue
29
+
30
+ required_groups = server_config.get("groups", [])
31
+ if not required_groups:
32
+ authorized_servers.append(server_name)
33
+ continue
34
+
35
+ # Check if user is in any of the required groups
36
+ group_checks = [await auth_check_func(user_email, group) for group in required_groups]
37
+ if any(group_checks):
38
+ authorized_servers.append(server_name)
39
+ return authorized_servers
40
+
41
+
42
+ @pytest.mark.asyncio
43
+ async def test_tool_authorization_enforces_group_restrictions():
44
+ """
45
+ Test that ToolAuthorizationService properly enforces group restrictions.
46
+
47
+ This test verifies that:
48
+ 1. Tools from servers requiring specific groups are filtered out for unauthorized users
49
+ 2. The authorization service does NOT fail open (return all tools) when group check fails
50
+
51
+ Bug context: Previously, ToolAuthorizationService passed None as the auth_check_func
52
+ to get_authorized_servers(), causing a TypeError that was caught and resulted in
53
+ returning all originally selected tools (fail-open behavior).
54
+ """
55
+ # Setup: Create servers with group restrictions
56
+ servers_config = {
57
+ "public_server": {
58
+ "enabled": True,
59
+ "groups": [] # No group restriction - available to all
60
+ },
61
+ "admin_server": {
62
+ "enabled": True,
63
+ "groups": ["admin"] # Only admin group can access
64
+ },
65
+ "users_server": {
66
+ "enabled": True,
67
+ "groups": ["users"] # Only users group can access
68
+ }
69
+ }
70
+
71
+ tool_manager = MockToolManager(servers_config)
72
+ auth_service = ToolAuthorizationService(tool_manager)
73
+
74
+ # User selects tools from all servers
75
+ selected_tools = [
76
+ "public_server_tool1",
77
+ "admin_server_tool1",
78
+ "users_server_tool1",
79
+ "canvas_canvas"
80
+ ]
81
+
82
+ # Mock is_user_in_group: user is in "users" group but not "admin"
83
+ async def mock_auth_check(user: str, group: str) -> bool:
84
+ return group == "users"
85
+
86
+ with patch("atlas.application.chat.policies.tool_authorization.is_user_in_group", mock_auth_check):
87
+ filtered_tools = await auth_service.filter_authorized_tools(
88
+ selected_tools=selected_tools,
89
+ user_email="regular@example.com"
90
+ )
91
+
92
+ # Assert: User should NOT have access to admin_server tools
93
+ assert "admin_server_tool1" not in filtered_tools, \
94
+ "Admin tools should be filtered out for non-admin users"
95
+
96
+ # canvas_canvas should always be allowed
97
+ assert "canvas_canvas" in filtered_tools, \
98
+ "canvas_canvas should always be allowed"
99
+
100
+ # public_server tools should be allowed (no group restriction)
101
+ assert "public_server_tool1" in filtered_tools, \
102
+ "Public server tools should be allowed for all users"
103
+
104
+ # users_server tools should be allowed (user is in users group)
105
+ assert "users_server_tool1" in filtered_tools, \
106
+ "Users server tools should be allowed for users in the group"
107
+
108
+
109
+ @pytest.mark.asyncio
110
+ async def test_tool_authorization_does_not_fail_open():
111
+ """
112
+ Test that tool authorization does not return all tools when auth check fails.
113
+
114
+ This specifically tests the fail-open bug where exceptions in authorization
115
+ cause all originally selected tools to be returned.
116
+ """
117
+ servers_config = {
118
+ "restricted_server": {
119
+ "enabled": True,
120
+ "groups": ["special_group"]
121
+ }
122
+ }
123
+
124
+ tool_manager = MockToolManager(servers_config)
125
+ auth_service = ToolAuthorizationService(tool_manager)
126
+
127
+ selected_tools = ["restricted_server_secret_tool"]
128
+
129
+ # Mock is_user_in_group: user is NOT in special_group
130
+ async def mock_auth_check(user: str, group: str) -> bool:
131
+ return False
132
+
133
+ with patch("atlas.application.chat.policies.tool_authorization.is_user_in_group", mock_auth_check):
134
+ filtered_tools = await auth_service.filter_authorized_tools(
135
+ selected_tools=selected_tools,
136
+ user_email="unauthorized@example.com"
137
+ )
138
+
139
+ # Assert: Restricted tools should NOT be accessible
140
+ assert "restricted_server_secret_tool" not in filtered_tools, \
141
+ "Restricted tools should not be accessible to unauthorized users (fail-open bug)"
142
+
143
+
144
+ @pytest.mark.asyncio
145
+ async def test_tool_authorization_with_real_mcp_tool_manager():
146
+ """
147
+ Integration test using the real MCPToolManager to verify auth function is passed.
148
+
149
+ This test ensures the ToolAuthorizationService properly integrates with
150
+ the real MCPToolManager.get_authorized_servers method.
151
+ """
152
+ from atlas.modules.mcp_tools.client import MCPToolManager
153
+
154
+ # Create a real MCPToolManager with test config
155
+ mcp_manager = MCPToolManager(None)
156
+ mcp_manager.servers_config = {
157
+ "public_server": {
158
+ "enabled": True,
159
+ "groups": []
160
+ },
161
+ "admin_server": {
162
+ "enabled": True,
163
+ "groups": ["admin"]
164
+ }
165
+ }
166
+
167
+ auth_service = ToolAuthorizationService(mcp_manager)
168
+
169
+ selected_tools = [
170
+ "public_server_tool1",
171
+ "admin_server_tool1"
172
+ ]
173
+
174
+ # Mock is_user_in_group: user is NOT in admin group
175
+ async def mock_auth_check(user: str, group: str) -> bool:
176
+ return False
177
+
178
+ with patch("atlas.application.chat.policies.tool_authorization.is_user_in_group", mock_auth_check):
179
+ filtered_tools = await auth_service.filter_authorized_tools(
180
+ selected_tools=selected_tools,
181
+ user_email="user@example.com"
182
+ )
183
+
184
+ # If we get here without the fix, the exception handler returns all tools
185
+ # So admin_server_tool1 would incorrectly be included
186
+ assert "admin_server_tool1" not in filtered_tools, \
187
+ "Admin tools should be filtered - auth function must be properly passed"
188
+ assert "public_server_tool1" in filtered_tools, \
189
+ "Public server tools should be accessible"
190
+
191
+
192
+ @pytest.mark.asyncio
193
+ async def test_tool_authorization_passes_auth_function_not_none():
194
+ """
195
+ Regression test: Ensure is_user_in_group is passed, not None.
196
+
197
+ This test will fail if None is passed to get_authorized_servers.
198
+ """
199
+ call_tracker = {"auth_func_received": None}
200
+
201
+ class TrackingToolManager:
202
+ def __init__(self):
203
+ self.servers_config = {"test_server": {"enabled": True, "groups": []}}
204
+
205
+ async def get_authorized_servers(self, user_email: str, auth_check_func):
206
+ call_tracker["auth_func_received"] = auth_check_func
207
+ if auth_check_func is None:
208
+ raise TypeError("auth_check_func cannot be None - security vulnerability!")
209
+ return ["test_server"]
210
+
211
+ tool_manager = TrackingToolManager()
212
+ auth_service = ToolAuthorizationService(tool_manager)
213
+
214
+ await auth_service.filter_authorized_tools(
215
+ selected_tools=["test_server_tool1"],
216
+ user_email="test@example.com"
217
+ )
218
+
219
+ # Verify that an actual function was passed, not None
220
+ assert call_tracker["auth_func_received"] is not None, \
221
+ "auth_check_func must not be None - this is a security vulnerability"
222
+ assert callable(call_tracker["auth_func_received"]), \
223
+ "auth_check_func must be callable"
@@ -0,0 +1,108 @@
1
+ """Test that tool details (description and inputSchema) are included in config API response."""
2
+
3
+ import os
4
+ import sys
5
+
6
+ import pytest
7
+
8
+ # Ensure backend is on path
9
+ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
10
+
11
+ from atlas.modules.mcp_tools.client import MCPToolManager
12
+
13
+
14
+ class FakeTool:
15
+ """Mock tool object for testing."""
16
+ def __init__(self, name, description="", inputSchema=None):
17
+ self.name = name
18
+ self.description = description
19
+ self.inputSchema = inputSchema or {"type": "object", "properties": {}}
20
+
21
+
22
+ @pytest.fixture
23
+ def mock_mcp_manager(monkeypatch):
24
+ """Create a mock MCP manager with test data."""
25
+ manager = MCPToolManager()
26
+
27
+ # Mock available_tools with detailed tool information
28
+ manager.available_tools = {
29
+ "test_server": {
30
+ "tools": [
31
+ FakeTool(
32
+ "test_tool",
33
+ "This is a test tool description",
34
+ {
35
+ "type": "object",
36
+ "properties": {
37
+ "arg1": {
38
+ "type": "string",
39
+ "description": "First argument"
40
+ },
41
+ "arg2": {
42
+ "type": "number",
43
+ "description": "Second argument"
44
+ }
45
+ },
46
+ "required": ["arg1"]
47
+ }
48
+ )
49
+ ],
50
+ "config": {
51
+ "description": "Test server",
52
+ "author": "Test Author"
53
+ }
54
+ }
55
+ }
56
+
57
+ manager.available_prompts = {}
58
+ return manager
59
+
60
+
61
+ def test_tools_detailed_includes_description_and_schema(mock_mcp_manager):
62
+ """Test that tools_detailed field contains description and inputSchema."""
63
+ server_tools = mock_mcp_manager.available_tools["test_server"]["tools"]
64
+ # Simulate what the config endpoint does
65
+ tools_detailed = []
66
+ for tool in server_tools:
67
+ tool_detail = {
68
+ 'name': tool.name,
69
+ 'description': tool.description or '',
70
+ 'inputSchema': getattr(tool, 'inputSchema', {}) or {}
71
+ }
72
+ tools_detailed.append(tool_detail)
73
+
74
+ # Verify the structure
75
+ assert len(tools_detailed) == 1
76
+ assert tools_detailed[0]['name'] == 'test_tool'
77
+ assert tools_detailed[0]['description'] == 'This is a test tool description'
78
+ assert 'inputSchema' in tools_detailed[0]
79
+ assert 'properties' in tools_detailed[0]['inputSchema']
80
+ assert 'arg1' in tools_detailed[0]['inputSchema']['properties']
81
+ assert tools_detailed[0]['inputSchema']['properties']['arg1']['type'] == 'string'
82
+ assert tools_detailed[0]['inputSchema']['properties']['arg1']['description'] == 'First argument'
83
+
84
+
85
+ def test_canvas_tool_has_detailed_info():
86
+ """Test that canvas pseudo-tool has detailed information."""
87
+ canvas_tools_detailed = [{
88
+ 'name': 'canvas',
89
+ 'description': 'Display final rendered content in a visual canvas panel. Use this for: 1) Complete code (not code discussions), 2) Final reports/documents (not report discussions), 3) Data visualizations, 4) Any polished content that should be viewed separately from the conversation.',
90
+ 'inputSchema': {
91
+ 'type': 'object',
92
+ 'properties': {
93
+ 'content': {
94
+ 'type': 'string',
95
+ 'description': 'The content to display in the canvas. Can be markdown, code, or plain text.'
96
+ }
97
+ },
98
+ 'required': ['content']
99
+ }
100
+ }]
101
+
102
+ # Verify canvas tool structure
103
+ assert len(canvas_tools_detailed) == 1
104
+ assert canvas_tools_detailed[0]['name'] == 'canvas'
105
+ assert 'description' in canvas_tools_detailed[0]
106
+ assert len(canvas_tools_detailed[0]['description']) > 0
107
+ assert 'inputSchema' in canvas_tools_detailed[0]
108
+ assert 'content' in canvas_tools_detailed[0]['inputSchema']['properties']