@oneciel-ai/ciel-runtime 0.1.1 → 0.2.0
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.
- package/README.md +132 -0
- package/ciel-runtime-menu.py +56 -6
- package/ciel_runtime.py +9379 -35909
- package/ciel_runtime_support/advisor_client.py +193 -0
- package/ciel_runtime_support/advisor_policy.py +320 -0
- package/ciel_runtime_support/advisor_refinement.py +160 -0
- package/ciel_runtime_support/advisor_request_builder.py +261 -0
- package/ciel_runtime_support/agy_installer.py +169 -0
- package/ciel_runtime_support/agy_mcp_restore.py +182 -0
- package/ciel_runtime_support/anthropic_model_policy.py +186 -0
- package/ciel_runtime_support/anthropic_response_writer.py +255 -0
- package/ciel_runtime_support/anthropic_tool_turns.py +130 -0
- package/ciel_runtime_support/api_key_cooldown.py +159 -0
- package/ciel_runtime_support/architecture.py +488 -1
- package/ciel_runtime_support/architecture_budget.py +42 -0
- package/ciel_runtime_support/channel_backlog.py +90 -0
- package/ciel_runtime_support/channel_cli.py +119 -0
- package/ciel_runtime_support/channel_compact_injection.py +82 -0
- package/ciel_runtime_support/channel_compact_poll.py +67 -0
- package/ciel_runtime_support/channel_compact_request_repository.py +113 -0
- package/ciel_runtime_support/channel_config_service.py +281 -0
- package/ciel_runtime_support/channel_connection_lifecycle.py +180 -0
- package/ciel_runtime_support/channel_connection_registry.py +128 -0
- package/ciel_runtime_support/channel_connection_worker.py +284 -0
- package/ciel_runtime_support/channel_cursor_recovery.py +92 -0
- package/ciel_runtime_support/channel_cursor_repository.py +89 -0
- package/ciel_runtime_support/channel_cursor_service.py +178 -0
- package/ciel_runtime_support/channel_event_identity.py +212 -0
- package/ciel_runtime_support/channel_event_projection.py +315 -0
- package/ciel_runtime_support/channel_inflight.py +127 -0
- package/ciel_runtime_support/channel_injection.py +115 -0
- package/ciel_runtime_support/channel_launch_guard_repository.py +58 -0
- package/ciel_runtime_support/channel_launch_policy.py +180 -0
- package/ciel_runtime_support/channel_llm_context.py +156 -0
- package/ciel_runtime_support/channel_mcp_discovery.py +186 -0
- package/ciel_runtime_support/channel_mcp_http_controller.py +239 -0
- package/ciel_runtime_support/channel_mcp_ownership.py +148 -0
- package/ciel_runtime_support/channel_mcp_tools.py +240 -0
- package/ciel_runtime_support/channel_mcp_transport.py +394 -0
- package/ciel_runtime_support/channel_message_dedupe.py +65 -0
- package/ciel_runtime_support/channel_message_policy.py +256 -0
- package/ciel_runtime_support/channel_message_prompt.py +305 -0
- package/ciel_runtime_support/channel_message_repository.py +234 -0
- package/ciel_runtime_support/channel_notification_projection.py +217 -0
- package/ciel_runtime_support/channel_panel.py +162 -0
- package/ciel_runtime_support/channel_pending_injection.py +209 -0
- package/ciel_runtime_support/channel_pending_poll.py +109 -0
- package/ciel_runtime_support/channel_probe_cache.py +433 -0
- package/ciel_runtime_support/channel_probe_report.py +101 -0
- package/ciel_runtime_support/channel_runtime_environment.py +181 -0
- package/ciel_runtime_support/channel_session_lifecycle.py +114 -0
- package/ciel_runtime_support/channel_session_repository.py +90 -0
- package/ciel_runtime_support/channel_terminal_dispatch.py +115 -0
- package/ciel_runtime_support/channel_terminal_input.py +277 -0
- package/ciel_runtime_support/channel_terminal_proxy.py +447 -0
- package/ciel_runtime_support/channel_tool_context.py +166 -0
- package/ciel_runtime_support/channel_transcript.py +414 -0
- package/ciel_runtime_support/channel_transcript_repository.py +96 -0
- package/ciel_runtime_support/channel_wake_claim_repository.py +126 -0
- package/ciel_runtime_support/channel_wake_delivery_repository.py +88 -0
- package/ciel_runtime_support/chat_files.py +138 -0
- package/ciel_runtime_support/chat_http_controller.py +235 -0
- package/ciel_runtime_support/claude_environment.py +375 -0
- package/ciel_runtime_support/claude_router.py +247 -193
- package/ciel_runtime_support/cli_dispatch.py +792 -0
- package/ciel_runtime_support/cli_parser.py +165 -0
- package/ciel_runtime_support/cli_usage.py +100 -0
- package/ciel_runtime_support/codex_app_server.py +20 -5
- package/ciel_runtime_support/codex_channel_sse_launch.py +87 -0
- package/ciel_runtime_support/codex_cli.py +42 -6
- package/ciel_runtime_support/codex_config.py +323 -0
- package/ciel_runtime_support/codex_launch_configuration.py +240 -0
- package/ciel_runtime_support/codex_launch_policy.py +66 -0
- package/ciel_runtime_support/codex_mcp_integration.py +195 -0
- package/ciel_runtime_support/codex_mcp_restore.py +304 -0
- package/ciel_runtime_support/codex_model_catalog.py +133 -0
- package/ciel_runtime_support/codex_process_lifecycle.py +271 -0
- package/ciel_runtime_support/codex_router.py +147 -1
- package/ciel_runtime_support/codex_session_repository.py +115 -0
- package/ciel_runtime_support/codex_session_selection.py +114 -0
- package/ciel_runtime_support/command_asset_installer.py +103 -0
- package/ciel_runtime_support/compatibility_probe.py +295 -0
- package/ciel_runtime_support/compatibility_protocol.py +251 -0
- package/ciel_runtime_support/compatibility_runtime.py +166 -0
- package/ciel_runtime_support/compatibility_test.py +370 -0
- package/ciel_runtime_support/config_migrations.py +307 -0
- package/ciel_runtime_support/config_repository.py +175 -0
- package/ciel_runtime_support/config_value_codec.py +64 -0
- package/ciel_runtime_support/configuration_cli.py +374 -0
- package/ciel_runtime_support/context_compaction.py +280 -0
- package/ciel_runtime_support/context_setup.py +208 -0
- package/ciel_runtime_support/context_summary_policy.py +392 -0
- package/ciel_runtime_support/credential_cli.py +104 -0
- package/ciel_runtime_support/credential_management.py +261 -0
- package/ciel_runtime_support/credentials.py +269 -0
- package/ciel_runtime_support/executable_discovery.py +141 -0
- package/ciel_runtime_support/github_copilot_oauth.py +335 -0
- package/ciel_runtime_support/github_copilot_oauth_runtime.py +213 -0
- package/ciel_runtime_support/header_forwarding.py +73 -0
- package/ciel_runtime_support/headless_config.py +221 -0
- package/ciel_runtime_support/http_response.py +129 -0
- package/ciel_runtime_support/install_diagnostics.py +149 -0
- package/ciel_runtime_support/kimi_identity.py +123 -0
- package/ciel_runtime_support/launch_diagnostics.py +204 -0
- package/ciel_runtime_support/launch_state.py +127 -0
- package/ciel_runtime_support/live_api_key_controller.py +58 -0
- package/ciel_runtime_support/llm_config_http.py +148 -0
- package/ciel_runtime_support/llm_option_config.py +259 -0
- package/ciel_runtime_support/llm_presentation_data.py +447 -0
- package/ciel_runtime_support/llm_presets.py +773 -0
- package/ciel_runtime_support/lm_studio_runtime.py +401 -0
- package/ciel_runtime_support/managed_mcp_config.py +144 -0
- package/ciel_runtime_support/managed_mcp_discovery.py +151 -0
- package/ciel_runtime_support/managed_service_cleanup.py +89 -0
- package/ciel_runtime_support/mcp_config_reader.py +230 -0
- package/ciel_runtime_support/mcp_http_proxy.py +607 -0
- package/ciel_runtime_support/mcp_inventory.py +59 -0
- package/ciel_runtime_support/mcp_notification_wait_policy.py +159 -0
- package/ciel_runtime_support/mcp_probe_codec.py +136 -0
- package/ciel_runtime_support/mcp_probe_transport.py +328 -0
- package/ciel_runtime_support/mcp_proxy_codec.py +345 -0
- package/ciel_runtime_support/mcp_proxy_config.py +107 -0
- package/ciel_runtime_support/mcp_proxy_notifications.py +261 -0
- package/ciel_runtime_support/mcp_proxy_process.py +560 -0
- package/ciel_runtime_support/mcp_split_proxy_http.py +165 -0
- package/ciel_runtime_support/mcp_stdio_probe.py +240 -0
- package/ciel_runtime_support/mcp_transport.py +146 -0
- package/ciel_runtime_support/model_cache_lifecycle.py +78 -0
- package/ciel_runtime_support/model_catalog_projection.py +61 -0
- package/ciel_runtime_support/model_context_hints.py +109 -0
- package/ciel_runtime_support/model_panel.py +147 -0
- package/ciel_runtime_support/model_registry_repository.py +231 -0
- package/ciel_runtime_support/npm_runtime.py +191 -0
- package/ciel_runtime_support/ollama_catalog.py +462 -0
- package/ciel_runtime_support/ollama_catalog_cli.py +41 -0
- package/ciel_runtime_support/ollama_catalog_repository.py +75 -0
- package/ciel_runtime_support/ollama_context_sync.py +87 -0
- package/ciel_runtime_support/ollama_forwarding.py +449 -0
- package/ciel_runtime_support/openai_chat_passthrough.py +67 -0
- package/ciel_runtime_support/openai_chat_router.py +64 -0
- package/ciel_runtime_support/openai_forwarding.py +194 -0
- package/ciel_runtime_support/openai_responses_router.py +291 -0
- package/ciel_runtime_support/openai_responses_stream.py +135 -0
- package/ciel_runtime_support/output_budget.py +89 -0
- package/ciel_runtime_support/package_lifecycle.py +217 -0
- package/ciel_runtime_support/plan_artifact_controller.py +104 -0
- package/ciel_runtime_support/prelaunch.py +959 -0
- package/ciel_runtime_support/prelaunch_launch_preference.py +75 -0
- package/ciel_runtime_support/prelaunch_panel_projection.py +396 -0
- package/ciel_runtime_support/prelaunch_terminal.py +764 -0
- package/ciel_runtime_support/process_control.py +708 -0
- package/ciel_runtime_support/prompt_compaction.py +322 -0
- package/ciel_runtime_support/prompt_injection.py +176 -0
- package/ciel_runtime_support/protocols/__init__.py +24 -0
- package/ciel_runtime_support/protocols/anthropic_content.py +29 -0
- package/ciel_runtime_support/protocols/anthropic_thinking_policy.py +336 -0
- package/ciel_runtime_support/protocols/chat_projection.py +315 -0
- package/ciel_runtime_support/protocols/conversation_policy.py +217 -0
- package/ciel_runtime_support/protocols/conversation_turn_policy.py +972 -0
- package/ciel_runtime_support/protocols/ollama_chat.py +111 -0
- package/ciel_runtime_support/protocols/ollama_response.py +231 -0
- package/ciel_runtime_support/protocols/openai_reasoning.py +75 -0
- package/ciel_runtime_support/protocols/openai_responses.py +271 -0
- package/ciel_runtime_support/protocols/pseudo_tool_history.py +248 -0
- package/ciel_runtime_support/protocols/tool_result_projection.py +112 -0
- package/ciel_runtime_support/provider_adapters.py +160 -0
- package/ciel_runtime_support/provider_catalog_sources.py +316 -0
- package/ciel_runtime_support/provider_choice.py +201 -0
- package/ciel_runtime_support/provider_compatibility.py +165 -0
- package/ciel_runtime_support/provider_config_mutations.py +361 -0
- package/ciel_runtime_support/provider_configuration_service.py +162 -0
- package/ciel_runtime_support/provider_context.py +308 -0
- package/ciel_runtime_support/provider_contract_projection.py +73 -0
- package/ciel_runtime_support/provider_descriptor.py +82 -0
- package/ciel_runtime_support/provider_endpoint_policy.py +130 -0
- package/ciel_runtime_support/provider_endpoint_probe.py +165 -0
- package/ciel_runtime_support/provider_launch_endpoint.py +109 -0
- package/ciel_runtime_support/provider_limits.py +457 -0
- package/ciel_runtime_support/provider_model_identity.py +139 -0
- package/ciel_runtime_support/provider_model_selection.py +431 -0
- package/ciel_runtime_support/provider_model_specs.py +142 -0
- package/ciel_runtime_support/provider_models.py +263 -0
- package/ciel_runtime_support/provider_network.py +176 -0
- package/ciel_runtime_support/provider_option_cli.py +238 -0
- package/ciel_runtime_support/provider_option_panel.py +275 -0
- package/ciel_runtime_support/provider_option_status.py +192 -0
- package/ciel_runtime_support/provider_policy.py +101 -0
- package/ciel_runtime_support/provider_query_policy.py +67 -0
- package/ciel_runtime_support/provider_readiness.py +112 -0
- package/ciel_runtime_support/provider_request_access.py +131 -0
- package/ciel_runtime_support/provider_request_builder.py +250 -0
- package/ciel_runtime_support/provider_responses_passthrough.py +81 -0
- package/ciel_runtime_support/provider_runtime_info.py +113 -0
- package/ciel_runtime_support/provider_runtime_modes.py +150 -0
- package/ciel_runtime_support/provider_sampling_policy.py +46 -0
- package/ciel_runtime_support/provider_status.py +145 -0
- package/ciel_runtime_support/provider_timeout_policy.py +184 -0
- package/ciel_runtime_support/provider_tool_policy.py +145 -0
- package/ciel_runtime_support/providers/__init__.py +65 -0
- package/ciel_runtime_support/providers/anthropic.py +160 -0
- package/ciel_runtime_support/providers/anthropic_catalog.py +119 -0
- package/ciel_runtime_support/providers/base.py +232 -0
- package/ciel_runtime_support/providers/catalog.py +326 -0
- package/ciel_runtime_support/providers/cloud.py +194 -0
- package/ciel_runtime_support/providers/constants.py +54 -0
- package/ciel_runtime_support/providers/deepseek.py +115 -0
- package/ciel_runtime_support/providers/fireworks.py +158 -0
- package/ciel_runtime_support/providers/github_copilot_oauth.py +199 -0
- package/ciel_runtime_support/providers/kimi.py +297 -0
- package/ciel_runtime_support/providers/lm_studio.py +76 -0
- package/ciel_runtime_support/providers/meta.py +257 -0
- package/ciel_runtime_support/providers/native.py +194 -0
- package/ciel_runtime_support/providers/nim.py +69 -0
- package/ciel_runtime_support/providers/nvidia.py +158 -0
- package/ciel_runtime_support/providers/nvidia_runtime.py +285 -0
- package/ciel_runtime_support/providers/ollama.py +181 -0
- package/ciel_runtime_support/providers/ollama_context.py +195 -0
- package/ciel_runtime_support/providers/ollama_runtime.py +213 -0
- package/ciel_runtime_support/providers/opencode.py +220 -0
- package/ciel_runtime_support/providers/opencode_go.py +37 -0
- package/ciel_runtime_support/providers/openrouter.py +57 -0
- package/ciel_runtime_support/providers/vllm.py +65 -0
- package/ciel_runtime_support/providers/zai.py +119 -0
- package/ciel_runtime_support/pseudo_tool_parser.py +115 -0
- package/ciel_runtime_support/rate_limit_policy.py +117 -0
- package/ciel_runtime_support/rate_limit_repository.py +154 -0
- package/ciel_runtime_support/registry.py +46 -0
- package/ciel_runtime_support/request_shortcuts.py +253 -0
- package/ciel_runtime_support/request_trace.py +323 -0
- package/ciel_runtime_support/response_collection.py +209 -0
- package/ciel_runtime_support/router_access.py +238 -0
- package/ciel_runtime_support/router_client_lifecycle.py +366 -0
- package/ciel_runtime_support/router_health_policy.py +101 -0
- package/ciel_runtime_support/router_http.py +513 -0
- package/ciel_runtime_support/router_process_lifecycle.py +401 -0
- package/ciel_runtime_support/router_rate_limit_service.py +285 -0
- package/ciel_runtime_support/router_server_runtime.py +103 -0
- package/ciel_runtime_support/router_shortcuts.py +201 -0
- package/ciel_runtime_support/routing_fallback.py +73 -0
- package/ciel_runtime_support/runtime_activity_repository.py +143 -0
- package/ciel_runtime_support/runtime_adapters.py +104 -0
- package/ciel_runtime_support/runtime_command_factory.py +73 -0
- package/ciel_runtime_support/runtime_compatibility.py +50 -0
- package/ciel_runtime_support/runtime_constants.py +178 -0
- package/ciel_runtime_support/runtime_launch.py +1602 -0
- package/ciel_runtime_support/runtime_llm_options.py +312 -0
- package/ciel_runtime_support/runtime_logging.py +161 -0
- package/ciel_runtime_support/runtime_paths.py +157 -0
- package/ciel_runtime_support/runtime_restart.py +84 -0
- package/ciel_runtime_support/runtime_upgrade.py +149 -0
- package/ciel_runtime_support/secure_json_repository.py +55 -0
- package/ciel_runtime_support/session_import.py +356 -0
- package/ciel_runtime_support/settings_repository.py +8 -0
- package/ciel_runtime_support/slash_command_assets.py +211 -0
- package/ciel_runtime_support/sse_stream.py +57 -0
- package/ciel_runtime_support/sse_trace.py +225 -0
- package/ciel_runtime_support/statusline_script.py +593 -0
- package/ciel_runtime_support/statusline_settings.py +53 -0
- package/ciel_runtime_support/stream_chunk_policy.py +18 -0
- package/ciel_runtime_support/streaming_anthropic.py +1955 -0
- package/ciel_runtime_support/synthetic_tool_policy.py +105 -0
- package/ciel_runtime_support/terminal_platform_io.py +127 -0
- package/ciel_runtime_support/timeout_profile.py +196 -0
- package/ciel_runtime_support/tool_dialects.py +85 -0
- package/ciel_runtime_support/tool_exposure_policy.py +63 -0
- package/ciel_runtime_support/tool_guard_hooks.py +218 -0
- package/ciel_runtime_support/tool_request_projection.py +96 -0
- package/ciel_runtime_support/tool_schema.py +483 -0
- package/ciel_runtime_support/tool_side_effect_dedupe.py +95 -0
- package/ciel_runtime_support/ui_text.py +266 -0
- package/ciel_runtime_support/upstream_error_policy.py +104 -0
- package/ciel_runtime_support/upstream_retry.py +419 -0
- package/ciel_runtime_support/upstream_stream_io.py +106 -0
- package/ciel_runtime_support/usage_events.py +96 -0
- package/ciel_runtime_support/visible_stream_filters.py +130 -0
- package/ciel_runtime_support/web_endpoints.py +447 -0
- package/ciel_runtime_support/web_ui.py +915 -0
- package/ciel_runtime_support/web_ui_controller.py +189 -0
- package/ciel_runtime_support/windows_console_input.py +137 -0
- package/ciel_runtime_support/windows_console_mode.py +112 -0
- package/docs/Architecture.md +54 -0
- package/docs/Configuration.md +17 -0
- package/docs/Module-Map.md +1093 -26
- package/docs/Providers.md +46 -1
- package/docs/adr/0001-runtime-bounded-contexts.md +295 -0
- package/npm-bin/run-ciel-runtime.js +22 -2
- package/package.json +9 -2
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
"""Tool schema registry and input repair service."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from typing import Any, Callable
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _noop_log(level: str, message: str) -> None:
|
|
11
|
+
del level, message
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def tool_schema_in_body(body: dict[str, Any], tool_name: str) -> dict[str, Any] | None:
|
|
15
|
+
tools = body.get("tools")
|
|
16
|
+
if not isinstance(tools, list):
|
|
17
|
+
return None
|
|
18
|
+
for tool in tools:
|
|
19
|
+
if isinstance(tool, dict) and str(tool.get("name") or "") == tool_name:
|
|
20
|
+
schema = tool.get("input_schema")
|
|
21
|
+
return schema if isinstance(schema, dict) else None
|
|
22
|
+
return None
|
|
23
|
+
TASK_UPDATE_STATUSES = {"pending", "in_progress", "completed", "deleted"}
|
|
24
|
+
TASK_UPDATE_STATUS_ALIASES = {
|
|
25
|
+
"active": "in_progress",
|
|
26
|
+
"assigned": "in_progress",
|
|
27
|
+
"current": "in_progress",
|
|
28
|
+
"doing": "in_progress",
|
|
29
|
+
"inprogress": "in_progress",
|
|
30
|
+
"in_progress": "in_progress",
|
|
31
|
+
"in-progress": "in_progress",
|
|
32
|
+
"in progress": "in_progress",
|
|
33
|
+
"ongoing": "in_progress",
|
|
34
|
+
"processing": "in_progress",
|
|
35
|
+
"running": "in_progress",
|
|
36
|
+
"started": "in_progress",
|
|
37
|
+
"working": "in_progress",
|
|
38
|
+
"complete": "completed",
|
|
39
|
+
"completed": "completed",
|
|
40
|
+
"done": "completed",
|
|
41
|
+
"finished": "completed",
|
|
42
|
+
"resolved": "completed",
|
|
43
|
+
"success": "completed",
|
|
44
|
+
"closed": "completed",
|
|
45
|
+
"open": "pending",
|
|
46
|
+
"pending": "pending",
|
|
47
|
+
"queued": "pending",
|
|
48
|
+
"todo": "pending",
|
|
49
|
+
"to_do": "pending",
|
|
50
|
+
"to-do": "pending",
|
|
51
|
+
"waiting": "pending",
|
|
52
|
+
"cancel": "deleted",
|
|
53
|
+
"cancelled": "deleted",
|
|
54
|
+
"canceled": "deleted",
|
|
55
|
+
"delete": "deleted",
|
|
56
|
+
"deleted": "deleted",
|
|
57
|
+
"remove": "deleted",
|
|
58
|
+
"removed": "deleted",
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
_TOOL_SCHEMA_REGISTRY: dict[str, dict[str, Any]] = {}
|
|
62
|
+
|
|
63
|
+
_BUILTIN_TOOL_SCHEMAS: dict[str, dict[str, Any]] = {
|
|
64
|
+
"Bash": {
|
|
65
|
+
"required": ["command"],
|
|
66
|
+
"properties": {
|
|
67
|
+
"command": {"type": "string"},
|
|
68
|
+
"description": {"type": "string"},
|
|
69
|
+
"timeout": {"type": "integer"},
|
|
70
|
+
"run_in_background": {"type": "boolean"},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
"Read": {
|
|
74
|
+
"required": ["file_path"],
|
|
75
|
+
"properties": {
|
|
76
|
+
"file_path": {"type": "string"},
|
|
77
|
+
"offset": {"type": "integer"},
|
|
78
|
+
"limit": {"type": "integer"},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
"Write": {
|
|
82
|
+
"required": ["file_path", "content"],
|
|
83
|
+
"properties": {
|
|
84
|
+
"file_path": {"type": "string"},
|
|
85
|
+
"content": {"type": "string"},
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
"Edit": {
|
|
89
|
+
"required": ["file_path", "old_string", "new_string"],
|
|
90
|
+
"properties": {
|
|
91
|
+
"file_path": {"type": "string"},
|
|
92
|
+
"old_string": {"type": "string"},
|
|
93
|
+
"new_string": {"type": "string"},
|
|
94
|
+
"replace_all": {"type": "boolean"},
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
"Glob": {
|
|
98
|
+
"required": ["pattern"],
|
|
99
|
+
"properties": {
|
|
100
|
+
"pattern": {"type": "string"},
|
|
101
|
+
"path": {"type": "string"},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
"Grep": {
|
|
105
|
+
"required": ["pattern"],
|
|
106
|
+
"properties": {
|
|
107
|
+
"pattern": {"type": "string"},
|
|
108
|
+
"path": {"type": "string"},
|
|
109
|
+
"output_mode": {"type": "string"},
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
"TaskList": {
|
|
113
|
+
"required": [],
|
|
114
|
+
"properties": {},
|
|
115
|
+
},
|
|
116
|
+
"TaskUpdate": {
|
|
117
|
+
"required": ["taskId"],
|
|
118
|
+
"properties": {
|
|
119
|
+
"taskId": {"type": "string"},
|
|
120
|
+
"subject": {"type": "string"},
|
|
121
|
+
"description": {"type": "string"},
|
|
122
|
+
"activeForm": {"type": "string"},
|
|
123
|
+
"status": {"type": "string", "enum": ["pending", "in_progress", "completed", "deleted"]},
|
|
124
|
+
"owner": {"type": "string"},
|
|
125
|
+
"addBlocks": {"type": "array"},
|
|
126
|
+
"addBlockedBy": {"type": "array"},
|
|
127
|
+
"metadata": {"type": "object"},
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
"TaskCreate": {
|
|
131
|
+
"required": ["subject", "description"],
|
|
132
|
+
"properties": {
|
|
133
|
+
"subject": {"type": "string"},
|
|
134
|
+
"description": {"type": "string"},
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
"TaskGet": {
|
|
138
|
+
"required": ["taskId"],
|
|
139
|
+
"properties": {
|
|
140
|
+
"taskId": {"type": "string"},
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
"TaskStop": {
|
|
144
|
+
"required": ["task_id"],
|
|
145
|
+
"properties": {
|
|
146
|
+
"task_id": {"type": "string"},
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
"CronCreate": {
|
|
150
|
+
"required": ["cron", "prompt"],
|
|
151
|
+
"properties": {
|
|
152
|
+
"cron": {"type": "string"},
|
|
153
|
+
"prompt": {"type": "string"},
|
|
154
|
+
"recurring": {"type": "boolean"},
|
|
155
|
+
"durable": {"type": "boolean"},
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
"CronDelete": {
|
|
159
|
+
"required": ["id"],
|
|
160
|
+
"properties": {
|
|
161
|
+
"id": {"type": "string"},
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
"CronList": {
|
|
165
|
+
"required": [],
|
|
166
|
+
"properties": {},
|
|
167
|
+
},
|
|
168
|
+
"advisor": {
|
|
169
|
+
"required": ["question"],
|
|
170
|
+
"properties": {
|
|
171
|
+
"question": {"type": "string"},
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _update_tool_schema_registry(tools: Any) -> None:
|
|
178
|
+
"""Cache tool schemas from incoming Anthropic requests."""
|
|
179
|
+
if not isinstance(tools, list):
|
|
180
|
+
return
|
|
181
|
+
for tool in tools:
|
|
182
|
+
if not isinstance(tool, dict):
|
|
183
|
+
continue
|
|
184
|
+
name = tool.get("name")
|
|
185
|
+
if not name:
|
|
186
|
+
continue
|
|
187
|
+
_TOOL_SCHEMA_REGISTRY[name] = tool.get("input_schema") or {}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _lookup_tool_schema(tool_name: str) -> dict[str, Any] | None:
|
|
191
|
+
"""Look up a tool schema by name, checking registry then builtins."""
|
|
192
|
+
if tool_name in _TOOL_SCHEMA_REGISTRY:
|
|
193
|
+
return _TOOL_SCHEMA_REGISTRY[tool_name]
|
|
194
|
+
if tool_name in _BUILTIN_TOOL_SCHEMAS:
|
|
195
|
+
return _BUILTIN_TOOL_SCHEMAS[tool_name]
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _fuzzy_match_tool_name(name: str) -> str | None:
|
|
200
|
+
"""Fuzzy match a tool name against known schemas (case-insensitive, prefix)."""
|
|
201
|
+
low = name.lower()
|
|
202
|
+
candidates = list(_TOOL_SCHEMA_REGISTRY.keys()) + list(_BUILTIN_TOOL_SCHEMAS.keys())
|
|
203
|
+
# Exact match first
|
|
204
|
+
for c in candidates:
|
|
205
|
+
if c == name:
|
|
206
|
+
return c
|
|
207
|
+
# Case-insensitive
|
|
208
|
+
for c in candidates:
|
|
209
|
+
if c.lower() == low:
|
|
210
|
+
return c
|
|
211
|
+
# Prefix/substring match
|
|
212
|
+
for c in candidates:
|
|
213
|
+
if low in c.lower() or c.lower() in low:
|
|
214
|
+
return c
|
|
215
|
+
return None
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _coerce_value(value: Any, expected_type: str | None) -> Any:
|
|
219
|
+
"""Coerce a value to the expected JSON schema type."""
|
|
220
|
+
if expected_type is None:
|
|
221
|
+
return value
|
|
222
|
+
if isinstance(value, bool) and expected_type == "boolean":
|
|
223
|
+
return value
|
|
224
|
+
if isinstance(value, (int, float)) and expected_type == "integer":
|
|
225
|
+
return int(value)
|
|
226
|
+
if isinstance(value, (int, float)) and expected_type == "number":
|
|
227
|
+
return float(value)
|
|
228
|
+
if isinstance(value, str) and expected_type == "string":
|
|
229
|
+
return value
|
|
230
|
+
if expected_type == "array":
|
|
231
|
+
if isinstance(value, list):
|
|
232
|
+
return value
|
|
233
|
+
if isinstance(value, tuple):
|
|
234
|
+
return list(value)
|
|
235
|
+
if isinstance(value, str) and value.strip():
|
|
236
|
+
text = value.strip()
|
|
237
|
+
try:
|
|
238
|
+
parsed = json.loads(text)
|
|
239
|
+
if isinstance(parsed, list):
|
|
240
|
+
return parsed
|
|
241
|
+
except Exception:
|
|
242
|
+
pass
|
|
243
|
+
return [text]
|
|
244
|
+
if value is None:
|
|
245
|
+
return []
|
|
246
|
+
return value
|
|
247
|
+
if expected_type == "object":
|
|
248
|
+
if isinstance(value, dict):
|
|
249
|
+
return value
|
|
250
|
+
if isinstance(value, str) and value.strip():
|
|
251
|
+
try:
|
|
252
|
+
parsed = json.loads(value)
|
|
253
|
+
if isinstance(parsed, dict):
|
|
254
|
+
return parsed
|
|
255
|
+
except Exception:
|
|
256
|
+
pass
|
|
257
|
+
return value
|
|
258
|
+
# Coerce string -> integer
|
|
259
|
+
if isinstance(value, str) and expected_type in ("integer", "number"):
|
|
260
|
+
try:
|
|
261
|
+
return int(value) if expected_type == "integer" else float(value)
|
|
262
|
+
except Exception:
|
|
263
|
+
pass
|
|
264
|
+
# Coerce string -> boolean
|
|
265
|
+
if isinstance(value, str) and expected_type == "boolean":
|
|
266
|
+
low = value.lower()
|
|
267
|
+
if low in ("true", "yes", "on", "1"):
|
|
268
|
+
return True
|
|
269
|
+
if low in ("false", "no", "off", "0"):
|
|
270
|
+
return False
|
|
271
|
+
# Coerce int/float -> string
|
|
272
|
+
if isinstance(value, (int, float)) and expected_type == "string":
|
|
273
|
+
return str(value)
|
|
274
|
+
# Coerce anything -> string as last resort
|
|
275
|
+
if expected_type == "string" and value is not None:
|
|
276
|
+
return str(value)
|
|
277
|
+
return value
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def normalize_task_update_status(value: Any) -> str | None:
|
|
281
|
+
if value is None:
|
|
282
|
+
return None
|
|
283
|
+
text = str(value).strip()
|
|
284
|
+
if not text:
|
|
285
|
+
return None
|
|
286
|
+
normalized = re.sub(r"[\s\-]+", "_", text.lower())
|
|
287
|
+
normalized = re.sub(r"[^a-z0-9_]", "", normalized)
|
|
288
|
+
if normalized in TASK_UPDATE_STATUSES:
|
|
289
|
+
return normalized
|
|
290
|
+
return TASK_UPDATE_STATUS_ALIASES.get(text.lower()) or TASK_UPDATE_STATUS_ALIASES.get(normalized)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _default_for_missing_required(tool_name: str, field: str) -> Any:
|
|
294
|
+
"""Return a safe default for known required fields."""
|
|
295
|
+
defaults: dict[str, dict[str, Any]] = {
|
|
296
|
+
"Bash": {"command": "true", "timeout": 30000, "description": "", "run_in_background": False},
|
|
297
|
+
"Read": {"offset": 0, "limit": 0},
|
|
298
|
+
"Edit": {"replace_all": False},
|
|
299
|
+
"Glob": {"path": "."},
|
|
300
|
+
"Grep": {"output_mode": "content"},
|
|
301
|
+
"TaskCreate": {"description": ""},
|
|
302
|
+
"TaskStop": {},
|
|
303
|
+
}
|
|
304
|
+
return defaults.get(tool_name, {}).get(field)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _is_empty_value(value: Any) -> bool:
|
|
308
|
+
"""Check if a value is effectively empty and should be defaulted."""
|
|
309
|
+
if value is None:
|
|
310
|
+
return True
|
|
311
|
+
if isinstance(value, str) and value.strip() == "":
|
|
312
|
+
return True
|
|
313
|
+
return False
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _move_first_present(fixed: dict[str, Any], target: str, aliases: tuple[str, ...]) -> None:
|
|
317
|
+
"""Move the first non-empty alias value to the target field."""
|
|
318
|
+
if target in fixed and not _is_empty_value(fixed.get(target)):
|
|
319
|
+
return
|
|
320
|
+
for alias in aliases:
|
|
321
|
+
value = fixed.get(alias)
|
|
322
|
+
if not _is_empty_value(value):
|
|
323
|
+
fixed[target] = value
|
|
324
|
+
break
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _validate_and_fix_tool_input(
|
|
328
|
+
tool_name: str,
|
|
329
|
+
input_dict: dict[str, Any],
|
|
330
|
+
source_body: dict[str, Any] | None = None,
|
|
331
|
+
log: Callable[[str, str], None] | None = None,
|
|
332
|
+
) -> dict[str, Any]:
|
|
333
|
+
"""
|
|
334
|
+
Validate tool_use input against schema and fix common errors:
|
|
335
|
+
- fuzzy-match tool name
|
|
336
|
+
- coerce types to match schema
|
|
337
|
+
- add defaults for missing required fields
|
|
338
|
+
- keep unknown fields (Claude Code may accept extra fields)
|
|
339
|
+
"""
|
|
340
|
+
schema = tool_schema_in_body(source_body, tool_name) if isinstance(source_body, dict) else None
|
|
341
|
+
if schema is None:
|
|
342
|
+
schema = _lookup_tool_schema(tool_name)
|
|
343
|
+
matched_name = tool_name
|
|
344
|
+
if schema is None:
|
|
345
|
+
matched = _fuzzy_match_tool_name(tool_name)
|
|
346
|
+
if matched:
|
|
347
|
+
matched_name = matched
|
|
348
|
+
schema = _lookup_tool_schema(matched)
|
|
349
|
+
|
|
350
|
+
if schema is None:
|
|
351
|
+
# No schema known: just ensure it's a dict and return
|
|
352
|
+
return input_dict if isinstance(input_dict, dict) else {}
|
|
353
|
+
|
|
354
|
+
properties = schema.get("properties") or {}
|
|
355
|
+
required = set(schema.get("required") or [])
|
|
356
|
+
fixed: dict[str, Any] = {}
|
|
357
|
+
|
|
358
|
+
for key, raw_value in input_dict.items():
|
|
359
|
+
prop_schema = properties.get(key)
|
|
360
|
+
if prop_schema is None:
|
|
361
|
+
# Unknown field: keep it rather than dropping it.
|
|
362
|
+
# Claude Code may accept fields not in our static registry.
|
|
363
|
+
fixed[key] = raw_value
|
|
364
|
+
continue
|
|
365
|
+
expected_type = prop_schema.get("type") if isinstance(prop_schema, dict) else None
|
|
366
|
+
fixed[key] = _coerce_value(raw_value, expected_type)
|
|
367
|
+
|
|
368
|
+
if matched_name == "TaskUpdate":
|
|
369
|
+
if "taskId" not in fixed or _is_empty_value(fixed.get("taskId")):
|
|
370
|
+
for alias in ("task_id", "id"):
|
|
371
|
+
value = fixed.get(alias)
|
|
372
|
+
if isinstance(value, (str, int, float)) and not isinstance(value, bool) and str(value).strip():
|
|
373
|
+
fixed["taskId"] = str(value)
|
|
374
|
+
break
|
|
375
|
+
if "status" in fixed:
|
|
376
|
+
status = normalize_task_update_status(fixed.get("status"))
|
|
377
|
+
if status:
|
|
378
|
+
fixed["status"] = status
|
|
379
|
+
for alias in ("task_id", "id"):
|
|
380
|
+
fixed.pop(alias, None)
|
|
381
|
+
|
|
382
|
+
if matched_name == "CronCreate":
|
|
383
|
+
_move_first_present(
|
|
384
|
+
fixed,
|
|
385
|
+
"cron",
|
|
386
|
+
("schedule", "cronExpression", "cron_expression", "expression", "interval", "time"),
|
|
387
|
+
)
|
|
388
|
+
_move_first_present(
|
|
389
|
+
fixed,
|
|
390
|
+
"prompt",
|
|
391
|
+
("message", "task", "instruction", "instructions", "command", "query"),
|
|
392
|
+
)
|
|
393
|
+
for key in ("cron", "prompt", "recurring", "durable"):
|
|
394
|
+
prop_schema = properties.get(key)
|
|
395
|
+
expected_type = prop_schema.get("type") if isinstance(prop_schema, dict) else None
|
|
396
|
+
if key in fixed:
|
|
397
|
+
fixed[key] = _coerce_value(fixed[key], expected_type)
|
|
398
|
+
for alias in (
|
|
399
|
+
"schedule",
|
|
400
|
+
"cronExpression",
|
|
401
|
+
"cron_expression",
|
|
402
|
+
"expression",
|
|
403
|
+
"interval",
|
|
404
|
+
"time",
|
|
405
|
+
"message",
|
|
406
|
+
"task",
|
|
407
|
+
"instruction",
|
|
408
|
+
"instructions",
|
|
409
|
+
"command",
|
|
410
|
+
"query",
|
|
411
|
+
):
|
|
412
|
+
fixed.pop(alias, None)
|
|
413
|
+
|
|
414
|
+
if matched_name == "CronDelete":
|
|
415
|
+
_move_first_present(fixed, "id", ("taskId", "task_id", "jobId", "job_id", "cronId", "cron_id"))
|
|
416
|
+
if "id" in fixed:
|
|
417
|
+
fixed["id"] = _coerce_value(fixed["id"], "string")
|
|
418
|
+
for alias in ("taskId", "task_id", "jobId", "job_id", "cronId", "cron_id"):
|
|
419
|
+
fixed.pop(alias, None)
|
|
420
|
+
|
|
421
|
+
if matched_name == "CronList":
|
|
422
|
+
fixed = {}
|
|
423
|
+
|
|
424
|
+
# Fill in missing or empty required fields with defaults
|
|
425
|
+
injected: list[str] = []
|
|
426
|
+
for req in required:
|
|
427
|
+
if req not in fixed or _is_empty_value(fixed.get(req)):
|
|
428
|
+
default = _default_for_missing_required(matched_name, req)
|
|
429
|
+
if default is not None:
|
|
430
|
+
fixed[req] = default
|
|
431
|
+
elif matched_name == "TaskUpdate" and req == "taskId":
|
|
432
|
+
continue
|
|
433
|
+
elif req not in fixed:
|
|
434
|
+
# No known default: inject empty value matching expected type
|
|
435
|
+
prop_schema = properties.get(req)
|
|
436
|
+
expected_type = prop_schema.get("type") if isinstance(prop_schema, dict) else None
|
|
437
|
+
if expected_type == "string":
|
|
438
|
+
fixed[req] = ""
|
|
439
|
+
elif expected_type == "integer":
|
|
440
|
+
fixed[req] = 0
|
|
441
|
+
elif expected_type == "number":
|
|
442
|
+
fixed[req] = 0.0
|
|
443
|
+
elif expected_type == "boolean":
|
|
444
|
+
fixed[req] = False
|
|
445
|
+
elif expected_type == "array":
|
|
446
|
+
fixed[req] = []
|
|
447
|
+
elif expected_type == "object":
|
|
448
|
+
fixed[req] = {}
|
|
449
|
+
else:
|
|
450
|
+
fixed[req] = ""
|
|
451
|
+
injected.append(req)
|
|
452
|
+
|
|
453
|
+
if injected:
|
|
454
|
+
(log or _noop_log)("WARN", f"tool_guard: {matched_name}: injected missing required fields: {', '.join(injected)}")
|
|
455
|
+
|
|
456
|
+
return fixed
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def _missing_required_tool_fields(tool_name: str, input_dict: dict[str, Any], source_body: dict[str, Any] | None = None) -> list[str]:
|
|
460
|
+
schema = tool_schema_in_body(source_body, tool_name) if isinstance(source_body, dict) else None
|
|
461
|
+
if schema is None:
|
|
462
|
+
schema = _lookup_tool_schema(tool_name)
|
|
463
|
+
if not isinstance(schema, dict):
|
|
464
|
+
return []
|
|
465
|
+
required = schema.get("required") or []
|
|
466
|
+
if not isinstance(required, list):
|
|
467
|
+
return []
|
|
468
|
+
missing: list[str] = []
|
|
469
|
+
for field in required:
|
|
470
|
+
if not isinstance(field, str):
|
|
471
|
+
continue
|
|
472
|
+
if field not in input_dict or _is_empty_value(input_dict.get(field)):
|
|
473
|
+
missing.append(field)
|
|
474
|
+
return missing
|
|
475
|
+
|
|
476
|
+
__all__ = [
|
|
477
|
+
"_fuzzy_match_tool_name",
|
|
478
|
+
"_lookup_tool_schema",
|
|
479
|
+
"_missing_required_tool_fields",
|
|
480
|
+
"_update_tool_schema_registry",
|
|
481
|
+
"_validate_and_fix_tool_input",
|
|
482
|
+
"normalize_task_update_status",
|
|
483
|
+
]
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Deduplicate exact repeated side-effect tool calls within a bounded window."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from threading import Lock, RLock
|
|
8
|
+
from typing import Any, Callable
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True, slots=True)
|
|
12
|
+
class ToolSideEffectDedupePolicy:
|
|
13
|
+
side_effect_suffixes: frozenset[str]
|
|
14
|
+
ttl_seconds: float = 600.0
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ToolSideEffectDedupeRepository:
|
|
18
|
+
def __init__(self, recent: dict[str, float], lock: Lock | RLock) -> None:
|
|
19
|
+
self.recent = recent
|
|
20
|
+
self._lock = lock
|
|
21
|
+
|
|
22
|
+
def previous_or_record(self, key: str, now: float, ttl_seconds: float) -> float | None:
|
|
23
|
+
with self._lock:
|
|
24
|
+
expired = [name for name, seen_at in self.recent.items() if now - seen_at > ttl_seconds]
|
|
25
|
+
for name in expired:
|
|
26
|
+
self.recent.pop(name, None)
|
|
27
|
+
previous = self.recent.get(key)
|
|
28
|
+
if previous is None or now - previous > ttl_seconds:
|
|
29
|
+
self.recent[key] = now
|
|
30
|
+
return None
|
|
31
|
+
return previous
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True, slots=True)
|
|
35
|
+
class ToolSideEffectDedupePorts:
|
|
36
|
+
now: Callable[[], float]
|
|
37
|
+
audit: Callable[[str, dict[str, Any]], None]
|
|
38
|
+
log: Callable[[str, str], None]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True, slots=True)
|
|
42
|
+
class ToolSideEffectDedupeService:
|
|
43
|
+
policy: ToolSideEffectDedupePolicy
|
|
44
|
+
repository: ToolSideEffectDedupeRepository
|
|
45
|
+
ports: ToolSideEffectDedupePorts
|
|
46
|
+
|
|
47
|
+
def key(self, tool_name: str, tool_input: dict[str, Any]) -> str | None:
|
|
48
|
+
if not isinstance(tool_name, str) or not tool_name:
|
|
49
|
+
return None
|
|
50
|
+
normalized_name = tool_name.strip()
|
|
51
|
+
tool_leaf = normalized_name.rsplit("__", 1)[-1].strip().lower()
|
|
52
|
+
if tool_leaf not in self.policy.side_effect_suffixes:
|
|
53
|
+
return None
|
|
54
|
+
try:
|
|
55
|
+
payload = json.dumps(
|
|
56
|
+
tool_input or {}, sort_keys=True, ensure_ascii=False, separators=(",", ":"), default=str
|
|
57
|
+
)
|
|
58
|
+
except Exception:
|
|
59
|
+
payload = repr(tool_input)
|
|
60
|
+
digest = hashlib.sha256(payload.encode("utf-8", errors="replace")).hexdigest()
|
|
61
|
+
return f"{normalized_name}:{digest}"
|
|
62
|
+
|
|
63
|
+
def should_drop(self, tool_name: str, tool_input: dict[str, Any], raw_name: str = "") -> bool:
|
|
64
|
+
key = self.key(tool_name, tool_input)
|
|
65
|
+
if not key:
|
|
66
|
+
return False
|
|
67
|
+
now = self.ports.now()
|
|
68
|
+
previous = self.repository.previous_or_record(key, now, self.policy.ttl_seconds)
|
|
69
|
+
if previous is None:
|
|
70
|
+
return False
|
|
71
|
+
age = now - previous
|
|
72
|
+
self.ports.audit(
|
|
73
|
+
"dropped_duplicate_side_effect_tool_call",
|
|
74
|
+
{
|
|
75
|
+
"raw_name": raw_name or tool_name,
|
|
76
|
+
"matched_name": tool_name,
|
|
77
|
+
"emitted_input": tool_input,
|
|
78
|
+
"age_seconds": round(age, 3),
|
|
79
|
+
"ttl_seconds": self.policy.ttl_seconds,
|
|
80
|
+
},
|
|
81
|
+
)
|
|
82
|
+
self.ports.log(
|
|
83
|
+
"WARN",
|
|
84
|
+
f"dropped duplicate side-effect tool call raw_name={raw_name or tool_name!r} "
|
|
85
|
+
f"matched_name={tool_name!r} age={age:.1f}s",
|
|
86
|
+
)
|
|
87
|
+
return True
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
__all__ = [
|
|
91
|
+
"ToolSideEffectDedupePolicy",
|
|
92
|
+
"ToolSideEffectDedupePorts",
|
|
93
|
+
"ToolSideEffectDedupeRepository",
|
|
94
|
+
"ToolSideEffectDedupeService",
|
|
95
|
+
]
|