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,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']
|