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,190 @@
1
+ from types import SimpleNamespace
2
+
3
+ import pytest
4
+
5
+ from atlas.core.log_sanitizer import get_current_user, sanitize_for_logging
6
+
7
+
8
+ @pytest.mark.asyncio
9
+ async def test_get_current_user_default():
10
+ class Dummy:
11
+ pass
12
+ req = SimpleNamespace(state=SimpleNamespace())
13
+ assert await get_current_user(req) == "test@test.com"
14
+
15
+
16
+ @pytest.mark.asyncio
17
+ async def test_get_current_user_from_state():
18
+ req = SimpleNamespace(state=SimpleNamespace(user_email="user@example.com"))
19
+ assert await get_current_user(req) == "user@example.com"
20
+
21
+
22
+ class TestSanitizeForLogging:
23
+ """Test suite for sanitize_for_logging function."""
24
+
25
+ def test_clean_string_unchanged(self):
26
+ """Test that strings without control characters are unchanged."""
27
+ assert sanitize_for_logging("Hello World") == "Hello World"
28
+ assert sanitize_for_logging("test123") == "test123"
29
+ assert sanitize_for_logging("user@example.com") == "user@example.com"
30
+
31
+ def test_removes_newlines(self):
32
+ """Test that newlines are removed."""
33
+ assert sanitize_for_logging("Hello\nWorld") == "HelloWorld"
34
+ assert sanitize_for_logging("Line1\nLine2\nLine3") == "Line1Line2Line3"
35
+ assert sanitize_for_logging("\nStarting newline") == "Starting newline"
36
+ assert sanitize_for_logging("Trailing newline\n") == "Trailing newline"
37
+
38
+ def test_removes_tabs(self):
39
+ """Test that tabs are removed."""
40
+ assert sanitize_for_logging("Hello\tWorld") == "HelloWorld"
41
+ assert sanitize_for_logging("\tIndented") == "Indented"
42
+
43
+ def test_removes_carriage_returns(self):
44
+ """Test that carriage returns are removed."""
45
+ assert sanitize_for_logging("Hello\rWorld") == "HelloWorld"
46
+ assert sanitize_for_logging("Windows\r\nLine") == "WindowsLine"
47
+
48
+ def test_removes_ansi_escape_sequences(self):
49
+ """Test that ANSI escape sequences are removed."""
50
+ # ANSI color codes
51
+ assert sanitize_for_logging("\x1b[31mRed Text\x1b[0m") == "[31mRed Text[0m"
52
+ assert sanitize_for_logging("\x1b[1;32mBold Green\x1b[0m") == "[1;32mBold Green[0m"
53
+
54
+ def test_removes_null_bytes(self):
55
+ """Test that null bytes are removed."""
56
+ assert sanitize_for_logging("Hello\x00World") == "HelloWorld"
57
+ assert sanitize_for_logging("\x00\x00test\x00") == "test"
58
+
59
+ def test_removes_control_characters(self):
60
+ """Test that various control characters are removed."""
61
+ # Test C0 control characters (0x00-0x1f)
62
+ assert sanitize_for_logging("Test\x01\x02\x03") == "Test"
63
+ assert sanitize_for_logging("\x07Bell\x08Backspace") == "BellBackspace"
64
+
65
+ # Test DEL character (0x7f)
66
+ assert sanitize_for_logging("Test\x7fDEL") == "TestDEL"
67
+
68
+ # Test C1 control characters (0x80-0x9f)
69
+ assert sanitize_for_logging("Test\x80\x81\x9f") == "Test"
70
+
71
+ def test_empty_string(self):
72
+ """Test that empty string returns empty string."""
73
+ assert sanitize_for_logging("") == ""
74
+
75
+ def test_none_value(self):
76
+ """Test that None returns empty string."""
77
+ assert sanitize_for_logging(None) == ""
78
+
79
+ def test_integer_value(self):
80
+ """Test that integers are converted to string."""
81
+ assert sanitize_for_logging(123) == "123"
82
+ assert sanitize_for_logging(0) == "0"
83
+ assert sanitize_for_logging(-456) == "-456"
84
+
85
+ def test_float_value(self):
86
+ """Test that floats are converted to string."""
87
+ assert sanitize_for_logging(3.14) == "3.14"
88
+ assert sanitize_for_logging(-0.5) == "-0.5"
89
+
90
+ def test_boolean_value(self):
91
+ """Test that booleans are converted to string."""
92
+ assert sanitize_for_logging(True) == "True"
93
+ assert sanitize_for_logging(False) == "False"
94
+
95
+ def test_list_value(self):
96
+ """Test that lists are converted to string."""
97
+ assert sanitize_for_logging([1, 2, 3]) == "[1, 2, 3]"
98
+ assert sanitize_for_logging(["a", "b"]) == "['a', 'b']"
99
+
100
+ def test_dict_value(self):
101
+ """Test that dicts are converted to string."""
102
+ result = sanitize_for_logging({"key": "value"})
103
+ assert "key" in result and "value" in result
104
+
105
+ def test_unicode_strings(self):
106
+ """Test that unicode strings are handled correctly."""
107
+ assert sanitize_for_logging("Hello 世界") == "Hello 世界"
108
+ assert sanitize_for_logging("Café ☕") == "Café ☕"
109
+ assert sanitize_for_logging("Test\n世界") == "Test世界"
110
+
111
+ def test_mixed_control_characters(self):
112
+ """Test strings with multiple types of control characters."""
113
+ assert sanitize_for_logging("Line1\r\nLine2\tTab\x00Null") == "Line1Line2TabNull"
114
+ assert sanitize_for_logging("\x01\x02Test\n\rData\x7f\x80") == "TestData"
115
+
116
+ def test_log_injection_attempt(self):
117
+ """Test that log injection attempts are sanitized."""
118
+ # Simulate log injection attack
119
+ malicious_input = "admin\n[INFO] Fake log entry\nAnother line"
120
+ sanitized = sanitize_for_logging(malicious_input)
121
+ assert "\n" not in sanitized
122
+ assert sanitized == "admin[INFO] Fake log entryAnother line"
123
+
124
+ def test_preserves_regular_punctuation(self):
125
+ """Test that regular punctuation and symbols are preserved."""
126
+ assert sanitize_for_logging("Hello, World!") == "Hello, World!"
127
+ assert sanitize_for_logging("Cost: $100 (20% off)") == "Cost: $100 (20% off)"
128
+ assert sanitize_for_logging("Email: test@example.com") == "Email: test@example.com"
129
+
130
+ def test_removes_unicode_line_separator(self):
131
+ """Test that Unicode LINE SEPARATOR (U+2028) is removed."""
132
+ assert sanitize_for_logging("Hello\u2028World") == "HelloWorld"
133
+ assert sanitize_for_logging("\u2028Starting") == "Starting"
134
+ assert sanitize_for_logging("Ending\u2028") == "Ending"
135
+ assert sanitize_for_logging("Line1\u2028Line2\u2028Line3") == "Line1Line2Line3"
136
+
137
+ def test_removes_unicode_paragraph_separator(self):
138
+ """Test that Unicode PARAGRAPH SEPARATOR (U+2029) is removed."""
139
+ assert sanitize_for_logging("Para1\u2029Para2") == "Para1Para2"
140
+ assert sanitize_for_logging("\u2029Starting") == "Starting"
141
+ assert sanitize_for_logging("Ending\u2029") == "Ending"
142
+ assert sanitize_for_logging("P1\u2029P2\u2029P3") == "P1P2P3"
143
+
144
+ def test_removes_both_unicode_separators(self):
145
+ """Test that both Unicode separators are removed together."""
146
+ assert sanitize_for_logging("Text\u2028with\u2029separators") == "Textwithseparators"
147
+ assert sanitize_for_logging("\u2028\u2029Mixed") == "Mixed"
148
+ assert sanitize_for_logging("A\u2028B\u2029C\u2028D") == "ABCD"
149
+
150
+ def test_unicode_separators_with_regular_unicode(self):
151
+ """Test Unicode separators mixed with regular Unicode characters."""
152
+ assert sanitize_for_logging("Hello\u2028世界") == "Hello世界"
153
+ assert sanitize_for_logging("Café\u2029☕") == "Café☕"
154
+ assert sanitize_for_logging("Test\u2028データ\u2029More") == "TestデータMore"
155
+
156
+ def test_unicode_separator_log_injection(self):
157
+ """Test that Unicode separators can't be used for log injection."""
158
+ malicious_input = "user@example.com\u2028[ERROR] Fake error message\u2029[INFO] Fake info"
159
+ sanitized = sanitize_for_logging(malicious_input)
160
+ assert "\u2028" not in sanitized
161
+ assert "\u2029" not in sanitized
162
+ assert sanitized == "user@example.com[ERROR] Fake error message[INFO] Fake info"
163
+
164
+ def test_multiple_consecutive_unicode_separators(self):
165
+ """Test multiple consecutive Unicode separators are all removed."""
166
+ assert sanitize_for_logging("Text\u2028\u2028\u2028More") == "TextMore"
167
+ assert sanitize_for_logging("Text\u2029\u2029\u2029More") == "TextMore"
168
+ assert sanitize_for_logging("\u2028\u2029\u2028\u2029Data") == "Data"
169
+
170
+ def test_unicode_separators_with_ascii_control_chars(self):
171
+ """Test Unicode separators combined with ASCII control characters."""
172
+ assert sanitize_for_logging("Test\n\u2028\rData\u2029\tEnd") == "TestDataEnd"
173
+ assert sanitize_for_logging("\x00\u2028Text\u2029\x1b[31m") == "Text[31m"
174
+
175
+ def test_complex_unicode_injection_scenario(self):
176
+ """Test complex scenario with Unicode separators in structured log attempt."""
177
+ attack = "Normal text\u2028[2025-11-08 10:00:00] CRITICAL: Injected message\u2029admin logged in"
178
+ sanitized = sanitize_for_logging(attack)
179
+ assert "\u2028" not in sanitized
180
+ assert "\u2029" not in sanitized
181
+ assert "\n" not in sanitized
182
+ assert sanitized == "Normal text[2025-11-08 10:00:00] CRITICAL: Injected messageadmin logged in"
183
+
184
+ def test_unicode_separators_do_not_affect_other_unicode(self):
185
+ """Test that removing Unicode separators doesn't affect other Unicode characters."""
186
+ text_with_emoji = "Hello\u2028😀\u2029World🌍"
187
+ assert sanitize_for_logging(text_with_emoji) == "Hello😀World🌍"
188
+
189
+ text_with_chars = "Test\u2028中文\u2029العربية\u2028Ελληνικά"
190
+ assert sanitize_for_logging(text_with_chars) == "Test中文العربيةΕλληνικά"
@@ -0,0 +1,202 @@
1
+ """Test to ensure docker-compose.yml environment variables stay in sync with .env.example.
2
+
3
+ This test ensures that the environment variables set in docker-compose.yml for the atlas-ui
4
+ service match those defined in .env.example, with appropriate exceptions for Docker-specific
5
+ configurations.
6
+ """
7
+
8
+ import re
9
+ from pathlib import Path
10
+
11
+
12
+ def parse_env_example(env_file_path: Path) -> dict[str, str]:
13
+ """Parse .env.example and extract all environment variables.
14
+
15
+ Args:
16
+ env_file_path: Path to the .env.example file
17
+
18
+ Returns:
19
+ Dictionary of environment variable names to values
20
+ """
21
+ env_vars = {}
22
+ with open(env_file_path, 'r', encoding='utf-8') as f:
23
+ for line in f:
24
+ line = line.strip()
25
+ if line and not line.startswith('#'):
26
+ match = re.match(r'^([A-Z_][A-Z0-9_]*)=(.*)$', line)
27
+ if match:
28
+ key, value = match.groups()
29
+ env_vars[key] = value
30
+ return env_vars
31
+
32
+
33
+ def parse_docker_compose_env(docker_compose_path: Path) -> dict[str, str]:
34
+ """Parse docker-compose.yml and extract environment variables for atlas-ui service.
35
+
36
+ Args:
37
+ docker_compose_path: Path to the docker-compose.yml file
38
+
39
+ Returns:
40
+ Dictionary of environment variable names to values
41
+ """
42
+ docker_env_vars = {}
43
+ with open(docker_compose_path, 'r', encoding='utf-8') as f:
44
+ in_atlas_ui_service = False
45
+ in_environment_section = False
46
+
47
+ for line in f:
48
+ # Detect when we enter the atlas-ui service block
49
+ if 'atlas-ui:' in line:
50
+ in_atlas_ui_service = True
51
+ in_environment_section = False
52
+ continue
53
+
54
+ # Detect when we enter another service block (exits atlas-ui)
55
+ stripped = line.strip()
56
+ if in_atlas_ui_service and line and stripped and not line[0].isspace() and ':' in line:
57
+ # We've reached a new top-level section
58
+ in_atlas_ui_service = False
59
+ in_environment_section = False
60
+ continue
61
+
62
+ # Check if we're entering the environment section within atlas-ui
63
+ if in_atlas_ui_service and 'environment:' in line:
64
+ in_environment_section = True
65
+ continue
66
+
67
+ # Check if we've exited the environment section (e.g., volumes:, depends_on:)
68
+ if in_environment_section and stripped and not stripped.startswith('-') and not stripped.startswith('#'):
69
+ if ':' in line and not stripped.startswith('- '):
70
+ # We've reached a new subsection (like volumes:)
71
+ in_environment_section = False
72
+ continue
73
+
74
+ # Parse environment variables
75
+ if in_environment_section and stripped.startswith('- '):
76
+ # Extract env var, handling quoted values
77
+ env_line = stripped[2:] # Remove "- "
78
+
79
+ # Handle quoted environment variables
80
+ if env_line.startswith('"'):
81
+ # Find the closing quote
82
+ end_quote = env_line.find('"', 1)
83
+ if end_quote != -1:
84
+ env_line = env_line[1:end_quote]
85
+
86
+ match = re.match(r'^([A-Z_][A-Z0-9_]*)=(.*)$', env_line)
87
+ if match:
88
+ key = match.group(1)
89
+ value = match.group(2).split('#')[0].strip() # Remove inline comments
90
+ docker_env_vars[key] = value
91
+
92
+ return docker_env_vars
93
+
94
+
95
+ def test_docker_compose_has_required_env_vars():
96
+ """Test that docker-compose.yml includes all required environment variables from .env.example.
97
+
98
+ This test ensures that Docker deployments have access to the same configuration
99
+ options as local development environments.
100
+ """
101
+ # Get paths to the files
102
+ repo_root = Path(__file__).parent.parent.parent
103
+ env_example_path = repo_root / '.env.example'
104
+ docker_compose_path = repo_root / 'docker-compose.yml'
105
+
106
+ # Parse both files
107
+ env_example_vars = parse_env_example(env_example_path)
108
+ docker_compose_vars = parse_docker_compose_env(docker_compose_path)
109
+
110
+ # Define variables that are intentionally different between .env.example and docker-compose.yml
111
+ # These have valid Docker-specific reasons to be different or omitted
112
+ docker_specific_exceptions = {
113
+ 'USE_MOCK_S3', # Docker uses real MinIO, not mock S3
114
+ 'VITE_APP_NAME', # Build-time arg, not runtime env var in docker-compose
115
+ 'VITE_FEATURE_POWERED_BY_ATLAS', # Build-time arg, not runtime env var in docker-compose
116
+ # Note: The following are in .env.example as commented out, not as active vars,
117
+ # so they won't appear in env_example_vars and don't need to be listed here:
118
+ # - ATLAS_HOST (Docker-specific, set to 0.0.0.0 for container networking)
119
+ # - Various MCP/proxy secret headers that are commented out in .env.example
120
+ }
121
+
122
+ # Find variables in .env.example but not in docker-compose.yml
123
+ missing_vars = set(env_example_vars.keys()) - set(docker_compose_vars.keys()) - docker_specific_exceptions
124
+
125
+ # Assert that no required variables are missing
126
+ if missing_vars:
127
+ missing_list = sorted(missing_vars)
128
+ error_msg = (
129
+ f"docker-compose.yml is missing {len(missing_vars)} environment variable(s) "
130
+ f"that are defined in .env.example:\n"
131
+ f"{', '.join(missing_list)}\n\n"
132
+ f"Please add these to the 'environment:' section of the 'atlas-ui' service "
133
+ f"in docker-compose.yml."
134
+ )
135
+ raise AssertionError(error_msg)
136
+
137
+ # Verify that key feature flags are present (as mentioned in the issue)
138
+ key_feature_flags = [
139
+ 'FEATURE_MARKETPLACE_ENABLED',
140
+ 'FEATURE_TOOLS_ENABLED',
141
+ 'FEATURE_FILES_PANEL_ENABLED',
142
+ 'FEATURE_RAG_ENABLED',
143
+ ]
144
+
145
+ for flag in key_feature_flags:
146
+ assert flag in docker_compose_vars, (
147
+ f"Key feature flag '{flag}' is missing from docker-compose.yml"
148
+ )
149
+
150
+
151
+ def test_docker_compose_env_var_values_reasonable():
152
+ """Test that environment variable values in docker-compose.yml are reasonable.
153
+
154
+ This is a sanity check to ensure values aren't accidentally corrupted.
155
+ """
156
+ repo_root = Path(__file__).parent.parent.parent
157
+ docker_compose_path = repo_root / 'docker-compose.yml'
158
+
159
+ docker_compose_vars = parse_docker_compose_env(docker_compose_path)
160
+
161
+ # Check that boolean feature flags have boolean-like values
162
+ feature_flags = [k for k in docker_compose_vars.keys() if k.startswith('FEATURE_')]
163
+ for flag in feature_flags:
164
+ value = docker_compose_vars[flag].lower()
165
+ assert value in ['true', 'false'], (
166
+ f"Feature flag '{flag}' has non-boolean value: '{docker_compose_vars[flag]}'"
167
+ )
168
+
169
+ # Check that numeric values are numeric
170
+ numeric_vars = ['PORT', 'AGENT_MAX_STEPS']
171
+ for var in numeric_vars:
172
+ if var in docker_compose_vars:
173
+ value = docker_compose_vars[var]
174
+ assert value.isdigit(), (
175
+ f"Numeric variable '{var}' has non-numeric value: '{value}'"
176
+ )
177
+
178
+
179
+ def test_docker_specific_vars_present():
180
+ """Test that Docker-specific environment variables are correctly set.
181
+
182
+ These variables have Docker-specific values that differ from .env.example.
183
+ """
184
+ repo_root = Path(__file__).parent.parent.parent
185
+ docker_compose_path = repo_root / 'docker-compose.yml'
186
+
187
+ docker_compose_vars = parse_docker_compose_env(docker_compose_path)
188
+
189
+ # ATLAS_HOST should be 0.0.0.0 for Docker (allows external connections)
190
+ assert 'ATLAS_HOST' in docker_compose_vars, (
191
+ "ATLAS_HOST is required in docker-compose.yml for container networking"
192
+ )
193
+ assert docker_compose_vars['ATLAS_HOST'] == '0.0.0.0', (
194
+ "ATLAS_HOST should be 0.0.0.0 in Docker for external access"
195
+ )
196
+
197
+ # MinIO/S3 configuration should be present for Docker
198
+ s3_vars = ['S3_ENDPOINT', 'S3_BUCKET_NAME', 'S3_ACCESS_KEY', 'S3_SECRET_KEY']
199
+ for var in s3_vars:
200
+ assert var in docker_compose_vars, (
201
+ f"S3 configuration variable '{var}' is required in docker-compose.yml for MinIO integration"
202
+ )