@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,281 @@
|
|
|
1
|
+
"""Application service for persisted Channel configuration and CLI imports."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable, Iterable, Mapping
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True, slots=True)
|
|
11
|
+
class ChannelConfigPorts:
|
|
12
|
+
load: Callable[[], dict[str, Any]]
|
|
13
|
+
save: Callable[[dict[str, Any]], None]
|
|
14
|
+
invalidate: Callable[[], None]
|
|
15
|
+
dedupe: Callable[[Iterable[str]], list[str]]
|
|
16
|
+
log: Callable[[str, str], None]
|
|
17
|
+
environment: Mapping[str, str]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ChannelConfigService:
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
builtin_spec: str,
|
|
24
|
+
ports: ChannelConfigPorts,
|
|
25
|
+
) -> None:
|
|
26
|
+
self.builtin_spec = builtin_spec
|
|
27
|
+
self.ports = ports
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
def is_tagged(spec: str) -> bool:
|
|
31
|
+
return spec.startswith("plugin:") or spec.startswith("server:")
|
|
32
|
+
|
|
33
|
+
def configured_specs(self, config: dict[str, Any]) -> list[str]:
|
|
34
|
+
raw = config.setdefault("claude_code", {}).get("channels", [])
|
|
35
|
+
if isinstance(raw, str):
|
|
36
|
+
items = [raw]
|
|
37
|
+
elif isinstance(raw, list):
|
|
38
|
+
items = raw
|
|
39
|
+
else:
|
|
40
|
+
items = []
|
|
41
|
+
return self.ports.dedupe(
|
|
42
|
+
[self.builtin_spec, *(str(item).strip() for item in items)]
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def parse_passthrough(self, passthrough: list[str]) -> list[str]:
|
|
46
|
+
specs: list[str] = []
|
|
47
|
+
index = 0
|
|
48
|
+
options = ("--channels", "--dangerously-load-development-channels")
|
|
49
|
+
while index < len(passthrough):
|
|
50
|
+
argument = passthrough[index]
|
|
51
|
+
if argument in options:
|
|
52
|
+
index += 1
|
|
53
|
+
while index < len(passthrough) and self.is_tagged(passthrough[index]):
|
|
54
|
+
specs.append(passthrough[index])
|
|
55
|
+
index += 1
|
|
56
|
+
continue
|
|
57
|
+
if any(argument.startswith(option + "=") for option in options):
|
|
58
|
+
value = argument.split("=", 1)[1].strip()
|
|
59
|
+
if value and self.is_tagged(value):
|
|
60
|
+
specs.append(value)
|
|
61
|
+
index += 1
|
|
62
|
+
return self.ports.dedupe(specs)
|
|
63
|
+
|
|
64
|
+
def normalize_passthrough(
|
|
65
|
+
self,
|
|
66
|
+
passthrough: list[str],
|
|
67
|
+
) -> list[str]:
|
|
68
|
+
normalized: list[str] = []
|
|
69
|
+
index = 0
|
|
70
|
+
while index < len(passthrough):
|
|
71
|
+
argument = passthrough[index]
|
|
72
|
+
if argument == "--channels":
|
|
73
|
+
normalized.append(
|
|
74
|
+
"--dangerously-load-development-channels"
|
|
75
|
+
)
|
|
76
|
+
index += 1
|
|
77
|
+
while (
|
|
78
|
+
index < len(passthrough)
|
|
79
|
+
and self.is_tagged(passthrough[index])
|
|
80
|
+
):
|
|
81
|
+
normalized.append(passthrough[index])
|
|
82
|
+
index += 1
|
|
83
|
+
continue
|
|
84
|
+
if argument.startswith("--channels="):
|
|
85
|
+
value = argument.split("=", 1)[1].strip()
|
|
86
|
+
normalized.append(
|
|
87
|
+
"--dangerously-load-development-channels"
|
|
88
|
+
)
|
|
89
|
+
if value:
|
|
90
|
+
normalized.append(value)
|
|
91
|
+
index += 1
|
|
92
|
+
continue
|
|
93
|
+
normalized.append(argument)
|
|
94
|
+
index += 1
|
|
95
|
+
return normalized
|
|
96
|
+
|
|
97
|
+
def auto_import(self, passthrough: list[str]) -> list[str]:
|
|
98
|
+
specs = self.parse_passthrough(passthrough)
|
|
99
|
+
if not specs:
|
|
100
|
+
return []
|
|
101
|
+
config = self.ports.load()
|
|
102
|
+
existing = set(self.configured_specs(config))
|
|
103
|
+
if all(spec in existing for spec in specs):
|
|
104
|
+
return []
|
|
105
|
+
merged = [
|
|
106
|
+
spec
|
|
107
|
+
for spec in self.configured_specs(config)
|
|
108
|
+
if spec != self.builtin_spec
|
|
109
|
+
]
|
|
110
|
+
added: list[str] = []
|
|
111
|
+
for spec in specs:
|
|
112
|
+
if spec not in existing and spec != self.builtin_spec:
|
|
113
|
+
merged.append(spec)
|
|
114
|
+
existing.add(spec)
|
|
115
|
+
added.append(spec)
|
|
116
|
+
if not added:
|
|
117
|
+
return []
|
|
118
|
+
config.setdefault("claude_code", {})["channels"] = merged
|
|
119
|
+
self.ports.save(config)
|
|
120
|
+
self.ports.invalidate()
|
|
121
|
+
self.ports.log(
|
|
122
|
+
"INFO",
|
|
123
|
+
f"channels_auto_imported_from_passthrough count={len(added)} "
|
|
124
|
+
f"specs={','.join(added)}",
|
|
125
|
+
)
|
|
126
|
+
return added
|
|
127
|
+
|
|
128
|
+
def launch_specs(
|
|
129
|
+
self,
|
|
130
|
+
config: dict[str, Any],
|
|
131
|
+
extra_specs: list[str] | None = None,
|
|
132
|
+
) -> list[str]:
|
|
133
|
+
specs = [
|
|
134
|
+
spec
|
|
135
|
+
for spec in self.configured_specs(config)
|
|
136
|
+
if self.is_tagged(spec)
|
|
137
|
+
]
|
|
138
|
+
if extra_specs:
|
|
139
|
+
specs.extend(extra_specs)
|
|
140
|
+
return self.ports.dedupe(spec for spec in specs if self.is_tagged(spec))
|
|
141
|
+
|
|
142
|
+
@staticmethod
|
|
143
|
+
def normalize_delivery(value: Any) -> str:
|
|
144
|
+
text = str(value or "").strip().lower().replace("_", "-")
|
|
145
|
+
if text in {
|
|
146
|
+
"native",
|
|
147
|
+
"native-channel",
|
|
148
|
+
"native-channel-bridge",
|
|
149
|
+
"claude-channel",
|
|
150
|
+
"claude/native",
|
|
151
|
+
}:
|
|
152
|
+
return "native"
|
|
153
|
+
if text in {
|
|
154
|
+
"stdin",
|
|
155
|
+
"pty",
|
|
156
|
+
"terminal",
|
|
157
|
+
"wake",
|
|
158
|
+
"wake-proxy",
|
|
159
|
+
"legacy",
|
|
160
|
+
}:
|
|
161
|
+
return "stdin"
|
|
162
|
+
return "llm"
|
|
163
|
+
|
|
164
|
+
def delivery_mode(self, config: dict[str, Any] | None = None) -> str:
|
|
165
|
+
environment_value = self.ports.environment.get("CIEL_RUNTIME_CHANNEL_DELIVERY")
|
|
166
|
+
if environment_value is not None:
|
|
167
|
+
return self.normalize_delivery(environment_value)
|
|
168
|
+
config = config or self.ports.load()
|
|
169
|
+
value = config.setdefault("claude_code", {}).get("channel_delivery", "llm")
|
|
170
|
+
return self.normalize_delivery(value)
|
|
171
|
+
|
|
172
|
+
def set_delivery(self, value: Any) -> list[str]:
|
|
173
|
+
mode = self.normalize_delivery(value)
|
|
174
|
+
config = self.ports.load()
|
|
175
|
+
config.setdefault("claude_code", {})["channel_delivery"] = mode
|
|
176
|
+
self.ports.save(config)
|
|
177
|
+
messages = {
|
|
178
|
+
"native": "Channel delivery set to native claude/channel bridge.",
|
|
179
|
+
"llm": "Channel delivery set to LLM context injection.",
|
|
180
|
+
"stdin": "Channel delivery set to stdin wake proxy.",
|
|
181
|
+
}
|
|
182
|
+
return [messages[mode]]
|
|
183
|
+
|
|
184
|
+
def add(self, spec: str) -> list[str]:
|
|
185
|
+
spec = spec.strip()
|
|
186
|
+
if not spec:
|
|
187
|
+
return ["Channel spec was empty."]
|
|
188
|
+
if not self.is_tagged(spec):
|
|
189
|
+
return ["Channel spec must start with plugin: or server:."]
|
|
190
|
+
if spec == self.builtin_spec:
|
|
191
|
+
return ["Ciel Runtime router channel is always enabled."]
|
|
192
|
+
config = self.ports.load()
|
|
193
|
+
channels = [
|
|
194
|
+
item
|
|
195
|
+
for item in self.configured_specs(config)
|
|
196
|
+
if item != self.builtin_spec
|
|
197
|
+
]
|
|
198
|
+
if spec not in channels:
|
|
199
|
+
channels.append(spec)
|
|
200
|
+
config.setdefault("claude_code", {})["channels"] = channels
|
|
201
|
+
self.ports.save(config)
|
|
202
|
+
return [f"Channel added: {spec}."]
|
|
203
|
+
|
|
204
|
+
def remove(self, spec: str) -> list[str]:
|
|
205
|
+
if spec == self.builtin_spec:
|
|
206
|
+
return ["Ciel Runtime router channel is always enabled and cannot be removed."]
|
|
207
|
+
config = self.ports.load()
|
|
208
|
+
before = [
|
|
209
|
+
item
|
|
210
|
+
for item in self.configured_specs(config)
|
|
211
|
+
if item != self.builtin_spec
|
|
212
|
+
]
|
|
213
|
+
after = [item for item in before if item != spec]
|
|
214
|
+
config.setdefault("claude_code", {})["channels"] = after
|
|
215
|
+
self.ports.save(config)
|
|
216
|
+
message = (
|
|
217
|
+
f"Channel removed: {spec}."
|
|
218
|
+
if len(after) != len(before)
|
|
219
|
+
else f"Channel was not configured: {spec}."
|
|
220
|
+
)
|
|
221
|
+
return [message]
|
|
222
|
+
|
|
223
|
+
def clear(self) -> list[str]:
|
|
224
|
+
config = self.ports.load()
|
|
225
|
+
config.setdefault("claude_code", {})["channels"] = []
|
|
226
|
+
self.ports.save(config)
|
|
227
|
+
return [
|
|
228
|
+
"External Claude Code channels cleared. Ciel Runtime router remains enabled."
|
|
229
|
+
]
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@dataclass(frozen=True, slots=True)
|
|
233
|
+
class ChannelConfigApi:
|
|
234
|
+
"""Explicit public adapter for late-bound Channel configuration services."""
|
|
235
|
+
|
|
236
|
+
service_factory: Callable[[], ChannelConfigService]
|
|
237
|
+
|
|
238
|
+
def parse_passthrough_channel_specs(self, passthrough: list[str]) -> list[str]:
|
|
239
|
+
return self.service_factory().parse_passthrough(passthrough)
|
|
240
|
+
|
|
241
|
+
def auto_import_passthrough_channels(self, passthrough: list[str]) -> list[str]:
|
|
242
|
+
return self.service_factory().auto_import(passthrough)
|
|
243
|
+
|
|
244
|
+
def normalize_channel_passthrough(
|
|
245
|
+
self,
|
|
246
|
+
passthrough: list[str],
|
|
247
|
+
) -> list[str]:
|
|
248
|
+
return self.service_factory().normalize_passthrough(passthrough)
|
|
249
|
+
|
|
250
|
+
def channel_specs_for_launch(
|
|
251
|
+
self,
|
|
252
|
+
cfg: dict[str, Any],
|
|
253
|
+
passthrough: list[str],
|
|
254
|
+
extra_specs: list[str] | None = None,
|
|
255
|
+
) -> list[str]:
|
|
256
|
+
del passthrough
|
|
257
|
+
return self.service_factory().launch_specs(cfg, extra_specs)
|
|
258
|
+
|
|
259
|
+
def is_channel_spec_tagged(self, spec: str) -> bool:
|
|
260
|
+
return self.service_factory().is_tagged(spec)
|
|
261
|
+
|
|
262
|
+
def normalize_channel_delivery(self, value: Any) -> str:
|
|
263
|
+
return self.service_factory().normalize_delivery(value)
|
|
264
|
+
|
|
265
|
+
def channel_delivery_mode(self, cfg: dict[str, Any] | None = None) -> str:
|
|
266
|
+
return self.service_factory().delivery_mode(cfg)
|
|
267
|
+
|
|
268
|
+
def set_channel_delivery_config(self, value: Any) -> list[str]:
|
|
269
|
+
return self.service_factory().set_delivery(value)
|
|
270
|
+
|
|
271
|
+
def add_channel_spec(
|
|
272
|
+
self, spec: str, *, development: bool = False
|
|
273
|
+
) -> list[str]:
|
|
274
|
+
del development
|
|
275
|
+
return self.service_factory().add(spec)
|
|
276
|
+
|
|
277
|
+
def remove_channel_spec(self, spec: str) -> list[str]:
|
|
278
|
+
return self.service_factory().remove(spec)
|
|
279
|
+
|
|
280
|
+
def clear_channel_specs(self) -> list[str]:
|
|
281
|
+
return self.service_factory().clear()
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""Lifecycle service for creating and stopping channel transport connections."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import threading
|
|
6
|
+
import time
|
|
7
|
+
import urllib.parse
|
|
8
|
+
import uuid
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from threading import Lock
|
|
11
|
+
from typing import Any, Callable
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True, slots=True)
|
|
15
|
+
class ChannelConnectionLifecycleEffects:
|
|
16
|
+
safe_segment: Callable[[str, str], str]
|
|
17
|
+
close_session: Callable[[dict[str, Any], str], None]
|
|
18
|
+
cleanup_stale_sessions: Callable[[str, str, dict[str, str], str], None]
|
|
19
|
+
public_status: Callable[[str, dict[str, Any]], dict[str, Any]]
|
|
20
|
+
all_statuses: Callable[[], dict[str, Any]]
|
|
21
|
+
sse_worker: Callable[[str, str | None], None]
|
|
22
|
+
streamable_http_worker: Callable[[str, str | None], None]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True, slots=True)
|
|
26
|
+
class ChannelConnectionLifecyclePolicy:
|
|
27
|
+
streamable_protocol_version: str
|
|
28
|
+
legacy_sse_protocol_version: str
|
|
29
|
+
parse_bool: Callable[[Any, bool], bool]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True, slots=True)
|
|
33
|
+
class ChannelConnectionLifecycleStore:
|
|
34
|
+
states: dict[str, dict[str, Any]]
|
|
35
|
+
lock: Lock
|
|
36
|
+
|
|
37
|
+
def replace(self, name: str, state: dict[str, Any]) -> dict[str, Any] | None:
|
|
38
|
+
with self.lock:
|
|
39
|
+
prior = self.states.get(name)
|
|
40
|
+
if prior:
|
|
41
|
+
prior["running"] = False
|
|
42
|
+
self.states[name] = state
|
|
43
|
+
return dict(prior) if prior else None
|
|
44
|
+
|
|
45
|
+
def stop(self, name: str | None) -> tuple[list[str], list[dict[str, Any]]]:
|
|
46
|
+
stopped: list[str] = []
|
|
47
|
+
states_to_close: list[dict[str, Any]] = []
|
|
48
|
+
with self.lock:
|
|
49
|
+
targets = [name] if name else list(self.states)
|
|
50
|
+
for target in targets:
|
|
51
|
+
if not target:
|
|
52
|
+
continue
|
|
53
|
+
state = self.states.get(target)
|
|
54
|
+
if state:
|
|
55
|
+
state["running"] = False
|
|
56
|
+
states_to_close.append(dict(state))
|
|
57
|
+
stopped.append(target)
|
|
58
|
+
return stopped, states_to_close
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True, slots=True)
|
|
62
|
+
class ChannelConnectionLifecycle:
|
|
63
|
+
store: ChannelConnectionLifecycleStore
|
|
64
|
+
effects: ChannelConnectionLifecycleEffects
|
|
65
|
+
policy: ChannelConnectionLifecyclePolicy
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def _headers(config: dict[str, Any]) -> dict[str, str]:
|
|
69
|
+
raw_headers = config.get("headers") if isinstance(config.get("headers"), dict) else {}
|
|
70
|
+
headers = {str(key): str(value) for key, value in raw_headers.items() if str(key).strip()}
|
|
71
|
+
token = str(config.get("bearer_token") or config.get("token") or "").strip()
|
|
72
|
+
if token and "Authorization" not in headers:
|
|
73
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
74
|
+
return headers
|
|
75
|
+
|
|
76
|
+
@staticmethod
|
|
77
|
+
def _event_filter(config: dict[str, Any]) -> list[str]:
|
|
78
|
+
event_filter = config.get("event_filter")
|
|
79
|
+
if isinstance(event_filter, str):
|
|
80
|
+
return [item.strip() for item in event_filter.split(",") if item.strip()]
|
|
81
|
+
if isinstance(event_filter, list):
|
|
82
|
+
return [str(item).strip() for item in event_filter if str(item).strip()]
|
|
83
|
+
return []
|
|
84
|
+
|
|
85
|
+
def _new_state(
|
|
86
|
+
self,
|
|
87
|
+
config: dict[str, Any],
|
|
88
|
+
name: str,
|
|
89
|
+
url: str,
|
|
90
|
+
transport: str,
|
|
91
|
+
headers: dict[str, str],
|
|
92
|
+
connection_id: str,
|
|
93
|
+
) -> dict[str, Any]:
|
|
94
|
+
protocol_version = (
|
|
95
|
+
self.policy.streamable_protocol_version
|
|
96
|
+
if transport == "streamable-http"
|
|
97
|
+
else self.policy.legacy_sse_protocol_version
|
|
98
|
+
)
|
|
99
|
+
return {
|
|
100
|
+
"name": name,
|
|
101
|
+
"connection_id": connection_id,
|
|
102
|
+
"url": url,
|
|
103
|
+
"headers": headers,
|
|
104
|
+
"channel": str(config.get("channel") or "default"),
|
|
105
|
+
"sender_id": str(config.get("sender_id") or config.get("sender") or name),
|
|
106
|
+
"recipient": str(config.get("recipient") or config.get("recipient_id") or config.get("to") or "all"),
|
|
107
|
+
"running": True,
|
|
108
|
+
"started_at": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
|
109
|
+
"last_event_at": None,
|
|
110
|
+
"messages_received": 0,
|
|
111
|
+
"last_error": None,
|
|
112
|
+
"event_filter": self._event_filter(config),
|
|
113
|
+
"last_sse_event_id": str(
|
|
114
|
+
config.get("last_sse_event_id")
|
|
115
|
+
or config.get("last_event_id")
|
|
116
|
+
or config.get("lastEventId")
|
|
117
|
+
or ""
|
|
118
|
+
).strip(),
|
|
119
|
+
"sse_reconnects": 0,
|
|
120
|
+
"read_timeout_seconds": float(config.get("read_timeout_seconds") or config.get("timeout") or 300.0),
|
|
121
|
+
"retry_seconds": float(config.get("retry_seconds") or 5.0),
|
|
122
|
+
"mcp_enabled": bool(config.get("mcp", config.get("mcp_enabled", True))),
|
|
123
|
+
"mcp_endpoint": None,
|
|
124
|
+
"mcp_initialized": False,
|
|
125
|
+
"mcp_session_id": None,
|
|
126
|
+
"mcp_last_error": None,
|
|
127
|
+
"mcp_rpc_results": {},
|
|
128
|
+
"transport": transport,
|
|
129
|
+
"streamable_requires_session": self.policy.parse_bool(
|
|
130
|
+
config.get(
|
|
131
|
+
"streamable_requires_session",
|
|
132
|
+
config.get("require_session", config.get("mcp_session_required", True)),
|
|
133
|
+
),
|
|
134
|
+
True,
|
|
135
|
+
),
|
|
136
|
+
"mcp_protocol_version": str(config.get("mcp_protocol_version") or protocol_version),
|
|
137
|
+
"mcp_timeout_seconds": float(config.get("mcp_timeout_seconds") or 20.0),
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
def start(self, config: dict[str, Any]) -> dict[str, Any]:
|
|
141
|
+
url = str(config.get("url") or "").strip()
|
|
142
|
+
if not url.startswith(("http://", "https://")):
|
|
143
|
+
raise ValueError("SSE url must start with http:// or https://")
|
|
144
|
+
name = self.effects.safe_segment(
|
|
145
|
+
str(config.get("name") or urllib.parse.urlparse(url).netloc or "sse"), "sse"
|
|
146
|
+
)
|
|
147
|
+
declared_transport = str(config.get("transport") or config.get("type") or "").strip().lower()
|
|
148
|
+
transport = "streamable-http" if declared_transport in {"http", "streamable-http"} else "sse"
|
|
149
|
+
headers = self._headers(config)
|
|
150
|
+
connection_id = uuid.uuid4().hex
|
|
151
|
+
state = self._new_state(config, name, url, transport, headers, connection_id)
|
|
152
|
+
prior_state = self.store.replace(name, state)
|
|
153
|
+
if prior_state:
|
|
154
|
+
self.effects.close_session(prior_state, "replace_connection")
|
|
155
|
+
if transport == "streamable-http":
|
|
156
|
+
self.effects.cleanup_stale_sessions(
|
|
157
|
+
name,
|
|
158
|
+
url,
|
|
159
|
+
headers,
|
|
160
|
+
str(state.get("mcp_protocol_version") or self.policy.streamable_protocol_version),
|
|
161
|
+
)
|
|
162
|
+
worker = (
|
|
163
|
+
self.effects.streamable_http_worker
|
|
164
|
+
if transport == "streamable-http"
|
|
165
|
+
else self.effects.sse_worker
|
|
166
|
+
)
|
|
167
|
+
thread = threading.Thread(
|
|
168
|
+
target=worker,
|
|
169
|
+
args=(name, connection_id),
|
|
170
|
+
daemon=True,
|
|
171
|
+
name=f"ciel-runtime-channel-{transport}-{name}",
|
|
172
|
+
)
|
|
173
|
+
thread.start()
|
|
174
|
+
return self.effects.public_status(name, state)
|
|
175
|
+
|
|
176
|
+
def stop(self, name: str | None = None) -> dict[str, Any]:
|
|
177
|
+
stopped, states_to_close = self.store.stop(name)
|
|
178
|
+
for state in states_to_close:
|
|
179
|
+
self.effects.close_session(state, "stop_connection")
|
|
180
|
+
return {"stopped": stopped, "connections": self.effects.all_statuses()}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Thread-safe repository for live channel transport connection state."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import time
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from threading import Condition, Lock
|
|
9
|
+
from typing import Any, Callable
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True, slots=True)
|
|
13
|
+
class ChannelConnectionRegistry:
|
|
14
|
+
states: dict[str, dict[str, Any]]
|
|
15
|
+
lock: Lock
|
|
16
|
+
rpc_condition: Condition
|
|
17
|
+
log: Callable[[str, str], None]
|
|
18
|
+
clock: Callable[[], float] = time.time
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def public_status(name: str, state: dict[str, Any]) -> dict[str, Any]:
|
|
22
|
+
return {
|
|
23
|
+
"name": name,
|
|
24
|
+
"url": state.get("url"),
|
|
25
|
+
"channel": state.get("channel"),
|
|
26
|
+
"sender_id": state.get("sender_id"),
|
|
27
|
+
"recipient": state.get("recipient"),
|
|
28
|
+
"running": bool(state.get("running")),
|
|
29
|
+
"started_at": state.get("started_at"),
|
|
30
|
+
"last_event_at": state.get("last_event_at"),
|
|
31
|
+
"messages_received": int(state.get("messages_received") or 0),
|
|
32
|
+
"event_filter": state.get("event_filter") or [],
|
|
33
|
+
"read_timeout_seconds": state.get("read_timeout_seconds"),
|
|
34
|
+
"last_sse_event_id": state.get("last_sse_event_id"),
|
|
35
|
+
"sse_reconnects": int(state.get("sse_reconnects") or 0),
|
|
36
|
+
"transport": state.get("transport") or "sse",
|
|
37
|
+
"mcp_endpoint": state.get("mcp_endpoint"),
|
|
38
|
+
"mcp_initialized": bool(state.get("mcp_initialized")),
|
|
39
|
+
"mcp_session_id": state.get("mcp_session_id"),
|
|
40
|
+
"mcp_protocol_version": state.get("mcp_protocol_version"),
|
|
41
|
+
"mcp_last_error": state.get("mcp_last_error"),
|
|
42
|
+
"last_error": state.get("last_error"),
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
def statuses(self) -> dict[str, Any]:
|
|
46
|
+
with self.lock:
|
|
47
|
+
return {name: self.public_status(name, state) for name, state in self.states.items()}
|
|
48
|
+
|
|
49
|
+
def update(self, name: str, **updates: Any) -> None:
|
|
50
|
+
with self.lock:
|
|
51
|
+
if state := self.states.get(name):
|
|
52
|
+
state.update(updates)
|
|
53
|
+
|
|
54
|
+
def mark_session_lost(self, name: str, reason: str) -> None:
|
|
55
|
+
with self.lock:
|
|
56
|
+
state = self.states.get(name)
|
|
57
|
+
if not state:
|
|
58
|
+
return
|
|
59
|
+
state.update(
|
|
60
|
+
mcp_initialized=False,
|
|
61
|
+
mcp_session_id=None,
|
|
62
|
+
mcp_last_error=reason,
|
|
63
|
+
last_error=reason,
|
|
64
|
+
sse_reconnects=int(state.get("sse_reconnects") or 0) + 1,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def store_rpc_response(self, name: str, data_text: str) -> bool:
|
|
68
|
+
try:
|
|
69
|
+
payload = json.loads(str(data_text or "").strip())
|
|
70
|
+
except (json.JSONDecodeError, TypeError):
|
|
71
|
+
return False
|
|
72
|
+
if (
|
|
73
|
+
not isinstance(payload, dict)
|
|
74
|
+
or payload.get("id") is None
|
|
75
|
+
or ("result" not in payload and "error" not in payload)
|
|
76
|
+
):
|
|
77
|
+
return False
|
|
78
|
+
rpc_id = str(payload["id"])
|
|
79
|
+
with self.rpc_condition:
|
|
80
|
+
with self.lock:
|
|
81
|
+
state = self.states.get(name)
|
|
82
|
+
if not state:
|
|
83
|
+
return True
|
|
84
|
+
results = state.get("mcp_rpc_results")
|
|
85
|
+
if not isinstance(results, dict):
|
|
86
|
+
results = {}
|
|
87
|
+
state["mcp_rpc_results"] = results
|
|
88
|
+
results[rpc_id] = payload
|
|
89
|
+
for old_id in list(results)[: max(0, len(results) - 200)]:
|
|
90
|
+
results.pop(old_id, None)
|
|
91
|
+
self.rpc_condition.notify_all()
|
|
92
|
+
self.log("INFO", f"channel_sse_mcp_rpc_response name={name} id={rpc_id}")
|
|
93
|
+
return True
|
|
94
|
+
|
|
95
|
+
def take_rpc_response(
|
|
96
|
+
self, name: str, rpc_id: Any, timeout: float
|
|
97
|
+
) -> dict[str, Any] | None:
|
|
98
|
+
key = str(rpc_id)
|
|
99
|
+
deadline = self.clock() + max(0.1, timeout)
|
|
100
|
+
with self.rpc_condition:
|
|
101
|
+
while True:
|
|
102
|
+
with self.lock:
|
|
103
|
+
state = self.states.get(name)
|
|
104
|
+
results = state.get("mcp_rpc_results") if isinstance(state, dict) else None
|
|
105
|
+
if isinstance(results, dict) and key in results:
|
|
106
|
+
found = results.pop(key)
|
|
107
|
+
return found if isinstance(found, dict) else None
|
|
108
|
+
remaining = deadline - self.clock()
|
|
109
|
+
if remaining <= 0:
|
|
110
|
+
return None
|
|
111
|
+
self.rpc_condition.wait(min(remaining, 1.0))
|
|
112
|
+
|
|
113
|
+
@staticmethod
|
|
114
|
+
def public_mcp_name(name: str) -> str:
|
|
115
|
+
text = str(name or "").strip()
|
|
116
|
+
return text[4:] if text.startswith("mcp-") else text
|
|
117
|
+
|
|
118
|
+
def state_name_for_mcp_server(self, server_name: str) -> str | None:
|
|
119
|
+
text = str(server_name or "").strip()
|
|
120
|
+
candidates = [text, text[4:] if text.startswith("mcp-") else f"mcp-{text}"] if text else []
|
|
121
|
+
with self.lock:
|
|
122
|
+
for candidate in candidates:
|
|
123
|
+
if candidate in self.states:
|
|
124
|
+
return candidate
|
|
125
|
+
return next(
|
|
126
|
+
(name for name in self.states if self.public_mcp_name(name) == text),
|
|
127
|
+
None,
|
|
128
|
+
)
|