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.
- atlas/__init__.py +40 -0
- atlas/application/__init__.py +7 -0
- atlas/application/chat/__init__.py +7 -0
- atlas/application/chat/agent/__init__.py +10 -0
- atlas/application/chat/agent/act_loop.py +179 -0
- atlas/application/chat/agent/factory.py +142 -0
- atlas/application/chat/agent/protocols.py +46 -0
- atlas/application/chat/agent/react_loop.py +338 -0
- atlas/application/chat/agent/think_act_loop.py +171 -0
- atlas/application/chat/approval_manager.py +151 -0
- atlas/application/chat/elicitation_manager.py +191 -0
- atlas/application/chat/events/__init__.py +1 -0
- atlas/application/chat/events/agent_event_relay.py +112 -0
- atlas/application/chat/modes/__init__.py +1 -0
- atlas/application/chat/modes/agent.py +125 -0
- atlas/application/chat/modes/plain.py +74 -0
- atlas/application/chat/modes/rag.py +81 -0
- atlas/application/chat/modes/tools.py +179 -0
- atlas/application/chat/orchestrator.py +213 -0
- atlas/application/chat/policies/__init__.py +1 -0
- atlas/application/chat/policies/tool_authorization.py +99 -0
- atlas/application/chat/preprocessors/__init__.py +1 -0
- atlas/application/chat/preprocessors/message_builder.py +92 -0
- atlas/application/chat/preprocessors/prompt_override_service.py +104 -0
- atlas/application/chat/service.py +454 -0
- atlas/application/chat/utilities/__init__.py +6 -0
- atlas/application/chat/utilities/error_handler.py +367 -0
- atlas/application/chat/utilities/event_notifier.py +546 -0
- atlas/application/chat/utilities/file_processor.py +613 -0
- atlas/application/chat/utilities/tool_executor.py +789 -0
- atlas/atlas_chat_cli.py +347 -0
- atlas/atlas_client.py +238 -0
- atlas/core/__init__.py +0 -0
- atlas/core/auth.py +205 -0
- atlas/core/authorization_manager.py +27 -0
- atlas/core/capabilities.py +123 -0
- atlas/core/compliance.py +215 -0
- atlas/core/domain_whitelist.py +147 -0
- atlas/core/domain_whitelist_middleware.py +82 -0
- atlas/core/http_client.py +28 -0
- atlas/core/log_sanitizer.py +102 -0
- atlas/core/metrics_logger.py +59 -0
- atlas/core/middleware.py +131 -0
- atlas/core/otel_config.py +242 -0
- atlas/core/prompt_risk.py +200 -0
- atlas/core/rate_limit.py +0 -0
- atlas/core/rate_limit_middleware.py +64 -0
- atlas/core/security_headers_middleware.py +51 -0
- atlas/domain/__init__.py +37 -0
- atlas/domain/chat/__init__.py +1 -0
- atlas/domain/chat/dtos.py +85 -0
- atlas/domain/errors.py +96 -0
- atlas/domain/messages/__init__.py +12 -0
- atlas/domain/messages/models.py +160 -0
- atlas/domain/rag_mcp_service.py +664 -0
- atlas/domain/sessions/__init__.py +7 -0
- atlas/domain/sessions/models.py +36 -0
- atlas/domain/unified_rag_service.py +371 -0
- atlas/infrastructure/__init__.py +10 -0
- atlas/infrastructure/app_factory.py +135 -0
- atlas/infrastructure/events/__init__.py +1 -0
- atlas/infrastructure/events/cli_event_publisher.py +140 -0
- atlas/infrastructure/events/websocket_publisher.py +140 -0
- atlas/infrastructure/sessions/in_memory_repository.py +56 -0
- atlas/infrastructure/transport/__init__.py +7 -0
- atlas/infrastructure/transport/websocket_connection_adapter.py +33 -0
- atlas/init_cli.py +226 -0
- atlas/interfaces/__init__.py +15 -0
- atlas/interfaces/events.py +134 -0
- atlas/interfaces/llm.py +54 -0
- atlas/interfaces/rag.py +40 -0
- atlas/interfaces/sessions.py +75 -0
- atlas/interfaces/tools.py +57 -0
- atlas/interfaces/transport.py +24 -0
- atlas/main.py +564 -0
- atlas/mcp/api_key_demo/README.md +76 -0
- atlas/mcp/api_key_demo/main.py +172 -0
- atlas/mcp/api_key_demo/run.sh +56 -0
- atlas/mcp/basictable/main.py +147 -0
- atlas/mcp/calculator/main.py +149 -0
- atlas/mcp/code-executor/execution_engine.py +98 -0
- atlas/mcp/code-executor/execution_environment.py +95 -0
- atlas/mcp/code-executor/main.py +528 -0
- atlas/mcp/code-executor/result_processing.py +276 -0
- atlas/mcp/code-executor/script_generation.py +195 -0
- atlas/mcp/code-executor/security_checker.py +140 -0
- atlas/mcp/corporate_cars/main.py +437 -0
- atlas/mcp/csv_reporter/main.py +545 -0
- atlas/mcp/duckduckgo/main.py +182 -0
- atlas/mcp/elicitation_demo/README.md +171 -0
- atlas/mcp/elicitation_demo/main.py +262 -0
- atlas/mcp/env-demo/README.md +158 -0
- atlas/mcp/env-demo/main.py +199 -0
- atlas/mcp/file_size_test/main.py +284 -0
- atlas/mcp/filesystem/main.py +348 -0
- atlas/mcp/image_demo/main.py +113 -0
- atlas/mcp/image_demo/requirements.txt +4 -0
- atlas/mcp/logging_demo/README.md +72 -0
- atlas/mcp/logging_demo/main.py +103 -0
- atlas/mcp/many_tools_demo/main.py +50 -0
- atlas/mcp/order_database/__init__.py +0 -0
- atlas/mcp/order_database/main.py +369 -0
- atlas/mcp/order_database/signal_data.csv +1001 -0
- atlas/mcp/pdfbasic/main.py +394 -0
- atlas/mcp/pptx_generator/main.py +760 -0
- atlas/mcp/pptx_generator/requirements.txt +13 -0
- atlas/mcp/pptx_generator/run_test.sh +1 -0
- atlas/mcp/pptx_generator/test_pptx_generator_security.py +169 -0
- atlas/mcp/progress_demo/main.py +167 -0
- atlas/mcp/progress_updates_demo/QUICKSTART.md +273 -0
- atlas/mcp/progress_updates_demo/README.md +120 -0
- atlas/mcp/progress_updates_demo/main.py +497 -0
- atlas/mcp/prompts/main.py +222 -0
- atlas/mcp/public_demo/main.py +189 -0
- atlas/mcp/sampling_demo/README.md +169 -0
- atlas/mcp/sampling_demo/main.py +234 -0
- atlas/mcp/thinking/main.py +77 -0
- atlas/mcp/tool_planner/main.py +240 -0
- atlas/mcp/ui-demo/badmesh.png +0 -0
- atlas/mcp/ui-demo/main.py +383 -0
- atlas/mcp/ui-demo/templates/button_demo.html +32 -0
- atlas/mcp/ui-demo/templates/data_visualization.html +32 -0
- atlas/mcp/ui-demo/templates/form_demo.html +28 -0
- atlas/mcp/username-override-demo/README.md +320 -0
- atlas/mcp/username-override-demo/main.py +308 -0
- atlas/modules/__init__.py +0 -0
- atlas/modules/config/__init__.py +34 -0
- atlas/modules/config/cli.py +231 -0
- atlas/modules/config/config_manager.py +1096 -0
- atlas/modules/file_storage/__init__.py +22 -0
- atlas/modules/file_storage/cli.py +330 -0
- atlas/modules/file_storage/content_extractor.py +290 -0
- atlas/modules/file_storage/manager.py +295 -0
- atlas/modules/file_storage/mock_s3_client.py +402 -0
- atlas/modules/file_storage/s3_client.py +417 -0
- atlas/modules/llm/__init__.py +19 -0
- atlas/modules/llm/caller.py +287 -0
- atlas/modules/llm/litellm_caller.py +675 -0
- atlas/modules/llm/models.py +19 -0
- atlas/modules/mcp_tools/__init__.py +17 -0
- atlas/modules/mcp_tools/client.py +2123 -0
- atlas/modules/mcp_tools/token_storage.py +556 -0
- atlas/modules/prompts/prompt_provider.py +130 -0
- atlas/modules/rag/__init__.py +24 -0
- atlas/modules/rag/atlas_rag_client.py +336 -0
- atlas/modules/rag/client.py +129 -0
- atlas/routes/admin_routes.py +865 -0
- atlas/routes/config_routes.py +484 -0
- atlas/routes/feedback_routes.py +361 -0
- atlas/routes/files_routes.py +274 -0
- atlas/routes/health_routes.py +40 -0
- atlas/routes/mcp_auth_routes.py +223 -0
- atlas/server_cli.py +164 -0
- atlas/tests/conftest.py +20 -0
- atlas/tests/integration/test_mcp_auth_integration.py +152 -0
- atlas/tests/manual_test_sampling.py +87 -0
- atlas/tests/modules/mcp_tools/test_client_auth.py +226 -0
- atlas/tests/modules/mcp_tools/test_client_env.py +191 -0
- atlas/tests/test_admin_mcp_server_management_routes.py +141 -0
- atlas/tests/test_agent_roa.py +135 -0
- atlas/tests/test_app_factory_smoke.py +47 -0
- atlas/tests/test_approval_manager.py +439 -0
- atlas/tests/test_atlas_client.py +188 -0
- atlas/tests/test_atlas_rag_client.py +447 -0
- atlas/tests/test_atlas_rag_integration.py +224 -0
- atlas/tests/test_attach_file_flow.py +287 -0
- atlas/tests/test_auth_utils.py +165 -0
- atlas/tests/test_backend_public_url.py +185 -0
- atlas/tests/test_banner_logging.py +287 -0
- atlas/tests/test_capability_tokens_and_injection.py +203 -0
- atlas/tests/test_compliance_level.py +54 -0
- atlas/tests/test_compliance_manager.py +253 -0
- atlas/tests/test_config_manager.py +617 -0
- atlas/tests/test_config_manager_paths.py +12 -0
- atlas/tests/test_core_auth.py +18 -0
- atlas/tests/test_core_utils.py +190 -0
- atlas/tests/test_docker_env_sync.py +202 -0
- atlas/tests/test_domain_errors.py +329 -0
- atlas/tests/test_domain_whitelist.py +359 -0
- atlas/tests/test_elicitation_manager.py +408 -0
- atlas/tests/test_elicitation_routing.py +296 -0
- atlas/tests/test_env_demo_server.py +88 -0
- atlas/tests/test_error_classification.py +113 -0
- atlas/tests/test_error_flow_integration.py +116 -0
- atlas/tests/test_feedback_routes.py +333 -0
- atlas/tests/test_file_content_extraction.py +1134 -0
- atlas/tests/test_file_extraction_routes.py +158 -0
- atlas/tests/test_file_library.py +107 -0
- atlas/tests/test_file_manager_unit.py +18 -0
- atlas/tests/test_health_route.py +49 -0
- atlas/tests/test_http_client_stub.py +8 -0
- atlas/tests/test_imports_smoke.py +30 -0
- atlas/tests/test_interfaces_llm_response.py +9 -0
- atlas/tests/test_issue_access_denied_fix.py +136 -0
- atlas/tests/test_llm_env_expansion.py +836 -0
- atlas/tests/test_log_level_sensitive_data.py +285 -0
- atlas/tests/test_mcp_auth_routes.py +341 -0
- atlas/tests/test_mcp_client_auth.py +331 -0
- atlas/tests/test_mcp_data_injection.py +270 -0
- atlas/tests/test_mcp_get_authorized_servers.py +95 -0
- atlas/tests/test_mcp_hot_reload.py +512 -0
- atlas/tests/test_mcp_image_content.py +424 -0
- atlas/tests/test_mcp_logging.py +172 -0
- atlas/tests/test_mcp_progress_updates.py +313 -0
- atlas/tests/test_mcp_prompt_override_system_prompt.py +102 -0
- atlas/tests/test_mcp_prompts_server.py +39 -0
- atlas/tests/test_mcp_tool_result_parsing.py +296 -0
- atlas/tests/test_metrics_logger.py +56 -0
- atlas/tests/test_middleware_auth.py +379 -0
- atlas/tests/test_prompt_risk_and_acl.py +141 -0
- atlas/tests/test_rag_mcp_aggregator.py +204 -0
- atlas/tests/test_rag_mcp_service.py +224 -0
- atlas/tests/test_rate_limit_middleware.py +45 -0
- atlas/tests/test_routes_config_smoke.py +60 -0
- atlas/tests/test_routes_files_download_token.py +41 -0
- atlas/tests/test_routes_files_health.py +18 -0
- atlas/tests/test_runtime_imports.py +53 -0
- atlas/tests/test_sampling_integration.py +482 -0
- atlas/tests/test_security_admin_routes.py +61 -0
- atlas/tests/test_security_capability_tokens.py +65 -0
- atlas/tests/test_security_file_stats_scope.py +21 -0
- atlas/tests/test_security_header_injection.py +191 -0
- atlas/tests/test_security_headers_and_filename.py +63 -0
- atlas/tests/test_shared_session_repository.py +101 -0
- atlas/tests/test_system_prompt_loading.py +181 -0
- atlas/tests/test_token_storage.py +505 -0
- atlas/tests/test_tool_approval_config.py +93 -0
- atlas/tests/test_tool_approval_utils.py +356 -0
- atlas/tests/test_tool_authorization_group_filtering.py +223 -0
- atlas/tests/test_tool_details_in_config.py +108 -0
- atlas/tests/test_tool_planner.py +300 -0
- atlas/tests/test_unified_rag_service.py +398 -0
- atlas/tests/test_username_override_in_approval.py +258 -0
- atlas/tests/test_websocket_auth_header.py +168 -0
- atlas/version.py +6 -0
- atlas_chat-0.1.0.data/data/.env.example +253 -0
- atlas_chat-0.1.0.data/data/config/defaults/compliance-levels.json +44 -0
- atlas_chat-0.1.0.data/data/config/defaults/domain-whitelist.json +123 -0
- atlas_chat-0.1.0.data/data/config/defaults/file-extractors.json +74 -0
- atlas_chat-0.1.0.data/data/config/defaults/help-config.json +198 -0
- atlas_chat-0.1.0.data/data/config/defaults/llmconfig-buggy.yml +11 -0
- atlas_chat-0.1.0.data/data/config/defaults/llmconfig.yml +19 -0
- atlas_chat-0.1.0.data/data/config/defaults/mcp.json +138 -0
- atlas_chat-0.1.0.data/data/config/defaults/rag-sources.json +17 -0
- atlas_chat-0.1.0.data/data/config/defaults/splash-config.json +16 -0
- atlas_chat-0.1.0.dist-info/METADATA +236 -0
- atlas_chat-0.1.0.dist-info/RECORD +250 -0
- atlas_chat-0.1.0.dist-info/WHEEL +5 -0
- atlas_chat-0.1.0.dist-info/entry_points.txt +4 -0
- 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
|
+
)
|