@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,115 @@
|
|
|
1
|
+
"""Runtime-neutral contracts for injecting channel prompts into interactive CLIs.
|
|
2
|
+
|
|
3
|
+
This module owns runtime input policy and terminal delivery orchestration. It
|
|
4
|
+
does not know about SSE, MCP, cursors, transcripts, or subprocess lifecycle.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Callable, Protocol
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class InputTransport(Protocol):
|
|
14
|
+
"""Input port implemented by PTY and Windows Console adapters."""
|
|
15
|
+
|
|
16
|
+
def write(self, data: bytes) -> None: ...
|
|
17
|
+
|
|
18
|
+
def wait_until_input_consumed(self, timeout_seconds: float = 2.0) -> bool: ...
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class RuntimeInjectionPolicy:
|
|
23
|
+
"""Runtime interaction semantics independent of the host platform."""
|
|
24
|
+
|
|
25
|
+
runtime: str
|
|
26
|
+
clear_input: bytes
|
|
27
|
+
submit_input: bytes
|
|
28
|
+
submit_delay_seconds: float
|
|
29
|
+
submit_attempts: int = 1
|
|
30
|
+
confirm_submission: bool = False
|
|
31
|
+
bracketed_paste: bool = False
|
|
32
|
+
input_drain_timeout_seconds: float = 2.0
|
|
33
|
+
|
|
34
|
+
def __post_init__(self) -> None:
|
|
35
|
+
if not self.runtime.strip():
|
|
36
|
+
raise ValueError("runtime is required")
|
|
37
|
+
if not self.submit_input:
|
|
38
|
+
raise ValueError("submit_input is required")
|
|
39
|
+
if self.submit_delay_seconds < 0:
|
|
40
|
+
raise ValueError("submit_delay_seconds cannot be negative")
|
|
41
|
+
if not 1 <= self.submit_attempts <= 8:
|
|
42
|
+
raise ValueError("submit_attempts must be between 1 and 8")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class PromptInjection:
|
|
47
|
+
prompt: str
|
|
48
|
+
policy: RuntimeInjectionPolicy
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ChannelPromptInjector:
|
|
52
|
+
"""Coordinates an input transport using an explicit runtime policy."""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
*,
|
|
57
|
+
sleep: Callable[[float], None],
|
|
58
|
+
retry_delay_seconds: Callable[[], float],
|
|
59
|
+
snapshot: Callable[[], str | None],
|
|
60
|
+
log: Callable[[str, str], None],
|
|
61
|
+
) -> None:
|
|
62
|
+
self._sleep = sleep
|
|
63
|
+
self._retry_delay_seconds = retry_delay_seconds
|
|
64
|
+
self._snapshot = snapshot
|
|
65
|
+
self._log = log
|
|
66
|
+
|
|
67
|
+
def inject(self, transport: InputTransport, request: PromptInjection) -> None:
|
|
68
|
+
policy = request.policy
|
|
69
|
+
prompt = request.prompt.encode("utf-8", errors="replace")
|
|
70
|
+
payload = policy.clear_input + prompt
|
|
71
|
+
if policy.bracketed_paste:
|
|
72
|
+
payload = policy.clear_input + b"\x1b[200~" + prompt + b"\x1b[201~"
|
|
73
|
+
transport.write(payload)
|
|
74
|
+
|
|
75
|
+
if not transport.wait_until_input_consumed(policy.input_drain_timeout_seconds):
|
|
76
|
+
self._log("WARN", "channel_input_drain_timeout")
|
|
77
|
+
if policy.submit_delay_seconds:
|
|
78
|
+
self._sleep(policy.submit_delay_seconds)
|
|
79
|
+
|
|
80
|
+
before = self._snapshot() if policy.confirm_submission and policy.submit_attempts > 1 else None
|
|
81
|
+
for attempt in range(policy.submit_attempts):
|
|
82
|
+
transport.write(policy.submit_input)
|
|
83
|
+
if attempt >= policy.submit_attempts - 1 or not before:
|
|
84
|
+
break
|
|
85
|
+
retry_delay = self._retry_delay_seconds()
|
|
86
|
+
if retry_delay:
|
|
87
|
+
self._sleep(retry_delay)
|
|
88
|
+
after = self._snapshot()
|
|
89
|
+
if after and after != before:
|
|
90
|
+
self._log("INFO", f"channel_stdin_proxy_submit_confirmed attempt={attempt + 1}")
|
|
91
|
+
break
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class CallableInputTransport:
|
|
95
|
+
"""Compatibility adapter for existing descriptors and writer objects."""
|
|
96
|
+
|
|
97
|
+
def __init__(self, target: object, write: Callable[[object, bytes], None]) -> None:
|
|
98
|
+
self._target = target
|
|
99
|
+
self._write = write
|
|
100
|
+
|
|
101
|
+
def write(self, data: bytes) -> None:
|
|
102
|
+
self._write(self._target, data)
|
|
103
|
+
|
|
104
|
+
def wait_until_input_consumed(self, timeout_seconds: float = 2.0) -> bool:
|
|
105
|
+
wait = getattr(self._target, "wait_until_input_consumed", None)
|
|
106
|
+
return bool(wait(timeout_seconds)) if callable(wait) else True
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
__all__ = [
|
|
110
|
+
"CallableInputTransport",
|
|
111
|
+
"ChannelPromptInjector",
|
|
112
|
+
"InputTransport",
|
|
113
|
+
"PromptInjection",
|
|
114
|
+
"RuntimeInjectionPolicy",
|
|
115
|
+
]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Callable
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True, slots=True)
|
|
10
|
+
class ChannelLaunchGuardRepository:
|
|
11
|
+
path: Path
|
|
12
|
+
now: Callable[[], float]
|
|
13
|
+
log: Callable[[str, str], None]
|
|
14
|
+
|
|
15
|
+
def read(self) -> dict[str, Any] | None:
|
|
16
|
+
try:
|
|
17
|
+
data = json.loads(self.path.read_text(encoding="utf-8"))
|
|
18
|
+
except FileNotFoundError:
|
|
19
|
+
return None
|
|
20
|
+
except (OSError, UnicodeError, json.JSONDecodeError, TypeError) as exc:
|
|
21
|
+
self.log(
|
|
22
|
+
"WARN",
|
|
23
|
+
f"channel_llm_launch_guard_read_failed error={type(exc).__name__}: {exc}",
|
|
24
|
+
)
|
|
25
|
+
return None
|
|
26
|
+
if not isinstance(data, dict):
|
|
27
|
+
self.log("WARN", "channel_llm_launch_guard_read_failed error=invalid_payload")
|
|
28
|
+
return None
|
|
29
|
+
try:
|
|
30
|
+
expires_at = float(data.get("expires_at") or 0)
|
|
31
|
+
max_existing_id = int(data.get("max_existing_id") or 0)
|
|
32
|
+
except (TypeError, ValueError) as exc:
|
|
33
|
+
self.log(
|
|
34
|
+
"WARN",
|
|
35
|
+
f"channel_llm_launch_guard_read_failed error={type(exc).__name__}: {exc}",
|
|
36
|
+
)
|
|
37
|
+
return None
|
|
38
|
+
if expires_at <= self.now() or max_existing_id <= 0:
|
|
39
|
+
return None
|
|
40
|
+
return {"max_existing_id": max_existing_id, "expires_at": expires_at}
|
|
41
|
+
|
|
42
|
+
def write(self, max_existing_id: int, ttl_seconds: float = 180.0) -> None:
|
|
43
|
+
try:
|
|
44
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
now = self.now()
|
|
46
|
+
payload = {
|
|
47
|
+
"created_at": now,
|
|
48
|
+
"expires_at": now + max(1.0, float(ttl_seconds)),
|
|
49
|
+
"max_existing_id": max(0, int(max_existing_id)),
|
|
50
|
+
}
|
|
51
|
+
temporary = self.path.with_suffix(".json.tmp")
|
|
52
|
+
temporary.write_text(json.dumps(payload, separators=(",", ":")) + "\n", encoding="utf-8")
|
|
53
|
+
temporary.replace(self.path)
|
|
54
|
+
except (OSError, TypeError, ValueError, OverflowError) as exc:
|
|
55
|
+
self.log(
|
|
56
|
+
"WARN",
|
|
57
|
+
f"channel_llm_launch_guard_write_failed error={type(exc).__name__}: {exc}",
|
|
58
|
+
)
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""Channel launch-mode decisions and Claude channel capability probe."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
import json
|
|
7
|
+
import subprocess
|
|
8
|
+
from typing import Any, Protocol
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
RuntimeConfig = dict[str, Any]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PassthroughOptionQuery(Protocol):
|
|
15
|
+
def __call__(self, passthrough: list[str], *names: str) -> bool: ...
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ChannelSpecQuery(Protocol):
|
|
19
|
+
def __call__(
|
|
20
|
+
self,
|
|
21
|
+
config: RuntimeConfig,
|
|
22
|
+
passthrough: list[str],
|
|
23
|
+
extra_specs: list[str] | None = None,
|
|
24
|
+
) -> list[str]: ...
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ChannelDeliveryModeQuery(Protocol):
|
|
28
|
+
def __call__(self, config: RuntimeConfig | None = None) -> str: ...
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AuthStatusRunner(Protocol):
|
|
32
|
+
def __call__(
|
|
33
|
+
self,
|
|
34
|
+
command: list[str],
|
|
35
|
+
*,
|
|
36
|
+
text: bool,
|
|
37
|
+
stdout: int,
|
|
38
|
+
stderr: int,
|
|
39
|
+
timeout: float,
|
|
40
|
+
) -> subprocess.CompletedProcess[str]: ...
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True, slots=True)
|
|
44
|
+
class ChannelLaunchPorts:
|
|
45
|
+
has_option: PassthroughOptionQuery
|
|
46
|
+
channel_specs: ChannelSpecQuery
|
|
47
|
+
delivery_mode: ChannelDeliveryModeQuery
|
|
48
|
+
run_auth_status: AuthStatusRunner
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True, slots=True)
|
|
52
|
+
class ChannelLaunchPolicy:
|
|
53
|
+
native_router_names: frozenset[str]
|
|
54
|
+
ports: ChannelLaunchPorts
|
|
55
|
+
|
|
56
|
+
def native_passthrough_requested(self, passthrough: list[str]) -> bool:
|
|
57
|
+
return self.ports.has_option(
|
|
58
|
+
passthrough,
|
|
59
|
+
"--channels",
|
|
60
|
+
"--dangerously-load-development-channels",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def claude_args(
|
|
64
|
+
self,
|
|
65
|
+
config: RuntimeConfig,
|
|
66
|
+
passthrough: list[str],
|
|
67
|
+
extra_specs: list[str] | None = None,
|
|
68
|
+
*,
|
|
69
|
+
native_channel_bridge: bool = False,
|
|
70
|
+
) -> list[str]:
|
|
71
|
+
if not native_channel_bridge:
|
|
72
|
+
return []
|
|
73
|
+
if self.native_passthrough_requested(passthrough):
|
|
74
|
+
return []
|
|
75
|
+
specs = [
|
|
76
|
+
spec
|
|
77
|
+
for spec in self.ports.channel_specs(
|
|
78
|
+
config,
|
|
79
|
+
passthrough,
|
|
80
|
+
extra_specs,
|
|
81
|
+
)
|
|
82
|
+
if not self._native_router_spec(spec)
|
|
83
|
+
]
|
|
84
|
+
if not specs:
|
|
85
|
+
return []
|
|
86
|
+
return ["--dangerously-load-development-channels", *specs]
|
|
87
|
+
|
|
88
|
+
def native_bridge(
|
|
89
|
+
self,
|
|
90
|
+
use_router_mode: bool,
|
|
91
|
+
config: RuntimeConfig,
|
|
92
|
+
passthrough: list[str],
|
|
93
|
+
) -> bool:
|
|
94
|
+
return bool(
|
|
95
|
+
not use_router_mode
|
|
96
|
+
and self.ports.delivery_mode(config) == "native"
|
|
97
|
+
and not self.native_passthrough_requested(passthrough)
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def llm_delivery(
|
|
101
|
+
self,
|
|
102
|
+
use_router_mode: bool,
|
|
103
|
+
passthrough: list[str],
|
|
104
|
+
) -> bool:
|
|
105
|
+
return bool(
|
|
106
|
+
use_router_mode
|
|
107
|
+
and not self.native_passthrough_requested(passthrough)
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def stdin_proxy(
|
|
111
|
+
self,
|
|
112
|
+
use_router_mode: bool,
|
|
113
|
+
passthrough: list[str],
|
|
114
|
+
config: RuntimeConfig | None = None,
|
|
115
|
+
) -> bool:
|
|
116
|
+
if not self.llm_delivery(use_router_mode, passthrough):
|
|
117
|
+
return False
|
|
118
|
+
if self.ports.has_option(passthrough, "-p", "--print"):
|
|
119
|
+
return False
|
|
120
|
+
claude_config = (
|
|
121
|
+
config.get("claude_code")
|
|
122
|
+
if isinstance(config, dict)
|
|
123
|
+
else {}
|
|
124
|
+
)
|
|
125
|
+
if (
|
|
126
|
+
isinstance(claude_config, dict)
|
|
127
|
+
and claude_config.get("web_chat_session_bridge") is False
|
|
128
|
+
):
|
|
129
|
+
return False
|
|
130
|
+
return self.ports.delivery_mode(config) == "llm"
|
|
131
|
+
|
|
132
|
+
@staticmethod
|
|
133
|
+
def process_starts_sse(
|
|
134
|
+
stdin_proxy: bool,
|
|
135
|
+
native_bridge: bool,
|
|
136
|
+
llm_delivery: bool,
|
|
137
|
+
) -> bool:
|
|
138
|
+
return bool((stdin_proxy or native_bridge) and not llm_delivery)
|
|
139
|
+
|
|
140
|
+
def specs_include_external_server(self, specs: list[str]) -> bool:
|
|
141
|
+
for spec in specs:
|
|
142
|
+
text = str(spec or "").strip()
|
|
143
|
+
if not text:
|
|
144
|
+
continue
|
|
145
|
+
name = text.split(":", 1)[1] if ":" in text else text
|
|
146
|
+
if name.strip().lower() not in self.native_router_names:
|
|
147
|
+
return True
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
def claude_auth_available(self, executable: str) -> tuple[bool, str]:
|
|
151
|
+
try:
|
|
152
|
+
process = self.ports.run_auth_status(
|
|
153
|
+
[executable, "auth", "status"],
|
|
154
|
+
text=True,
|
|
155
|
+
stdout=subprocess.PIPE,
|
|
156
|
+
stderr=subprocess.PIPE,
|
|
157
|
+
timeout=5,
|
|
158
|
+
)
|
|
159
|
+
except (OSError, subprocess.SubprocessError) as exc:
|
|
160
|
+
return True, f"auth_status_unavailable:{type(exc).__name__}"
|
|
161
|
+
if process.returncode != 0:
|
|
162
|
+
return True, f"auth_status_rc_{process.returncode}"
|
|
163
|
+
try:
|
|
164
|
+
data = json.loads(process.stdout or "{}")
|
|
165
|
+
except (json.JSONDecodeError, TypeError):
|
|
166
|
+
return True, "auth_status_unparseable"
|
|
167
|
+
if not bool(data.get("loggedIn")):
|
|
168
|
+
return False, "not_logged_in"
|
|
169
|
+
return True, str(data.get("authMethod") or "logged_in")
|
|
170
|
+
|
|
171
|
+
def _native_router_spec(self, spec: str) -> bool:
|
|
172
|
+
text = str(spec)
|
|
173
|
+
return bool(
|
|
174
|
+
text.startswith("server:")
|
|
175
|
+
and text.split(":", 1)[1].strip().lower()
|
|
176
|
+
in self.native_router_names
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
__all__ = ["ChannelLaunchPolicy", "ChannelLaunchPorts"]
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Inject pending channel messages into an LLM request context."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
INTERNAL_METADATA_PREFIX = "ciel_runtime_"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def strip_internal_metadata(body: dict[str, Any]) -> dict[str, Any]:
|
|
14
|
+
"""Return the original body unless private metadata requires projection."""
|
|
15
|
+
|
|
16
|
+
metadata = body.get("metadata")
|
|
17
|
+
if not isinstance(metadata, dict) or not metadata:
|
|
18
|
+
return body
|
|
19
|
+
public_metadata = {
|
|
20
|
+
key: value
|
|
21
|
+
for key, value in metadata.items()
|
|
22
|
+
if not str(key).startswith(INTERNAL_METADATA_PREFIX)
|
|
23
|
+
}
|
|
24
|
+
if len(public_metadata) == len(metadata):
|
|
25
|
+
return body
|
|
26
|
+
projected = dict(body)
|
|
27
|
+
if public_metadata:
|
|
28
|
+
projected["metadata"] = public_metadata
|
|
29
|
+
else:
|
|
30
|
+
projected.pop("metadata", None)
|
|
31
|
+
return projected
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True, slots=True)
|
|
35
|
+
class ChannelLlmContextPolicy:
|
|
36
|
+
wake_request: Callable[[dict[str, Any]], bool]
|
|
37
|
+
plan_mode_active: Callable[[dict[str, Any]], bool]
|
|
38
|
+
delivery_mode: Callable[[], str]
|
|
39
|
+
ids_in_request: Callable[[dict[str, Any]], set[int]]
|
|
40
|
+
scan_limit: Callable[[], int]
|
|
41
|
+
skip_reason: Callable[[dict[str, Any]], str]
|
|
42
|
+
stdin_skip_reason: Callable[[int], str]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True, slots=True)
|
|
46
|
+
class ChannelLlmContextRepository:
|
|
47
|
+
lock: Callable[[], Any]
|
|
48
|
+
read_cursor: Callable[[], int]
|
|
49
|
+
commit_cursor: Callable[[int], None]
|
|
50
|
+
read_messages: Callable[[int, int], list[dict[str, Any]]]
|
|
51
|
+
superseded_ids: Callable[[list[dict[str, Any]]], set[int]]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True, slots=True)
|
|
55
|
+
class ChannelLlmContextProjection:
|
|
56
|
+
remove_wake_prompt: Callable[[dict[str, Any]], dict[str, Any]]
|
|
57
|
+
format_prompt: Callable[[list[dict[str, Any]]], str]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True, slots=True)
|
|
61
|
+
class ChannelLlmContextServices:
|
|
62
|
+
policy: ChannelLlmContextPolicy
|
|
63
|
+
repository: ChannelLlmContextRepository
|
|
64
|
+
projection: ChannelLlmContextProjection
|
|
65
|
+
log: Callable[[str, str], Any]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def inject_pending_channel_context(
|
|
69
|
+
body: dict[str, Any],
|
|
70
|
+
services: ChannelLlmContextServices,
|
|
71
|
+
) -> dict[str, Any]:
|
|
72
|
+
policy = services.policy
|
|
73
|
+
repository = services.repository
|
|
74
|
+
wake_request = policy.wake_request(body)
|
|
75
|
+
if policy.plan_mode_active(body):
|
|
76
|
+
if not wake_request:
|
|
77
|
+
services.log("INFO", "channel_llm_inject_skipped reason=plan_mode_active")
|
|
78
|
+
return body
|
|
79
|
+
services.log("INFO", "channel_llm_inject_plan_mode_override reason=channel_wake")
|
|
80
|
+
if policy.delivery_mode() != "llm":
|
|
81
|
+
return body
|
|
82
|
+
|
|
83
|
+
ids_already_in_request = policy.ids_in_request(body)
|
|
84
|
+
with repository.lock():
|
|
85
|
+
last_id = repository.read_cursor()
|
|
86
|
+
pending: list[dict[str, Any]] = []
|
|
87
|
+
max_seen = last_id
|
|
88
|
+
candidates = repository.read_messages(last_id, policy.scan_limit())
|
|
89
|
+
superseded_ids = repository.superseded_ids(candidates)
|
|
90
|
+
for message in candidates:
|
|
91
|
+
try:
|
|
92
|
+
message_id = int(message.get("id") or 0)
|
|
93
|
+
except (TypeError, ValueError):
|
|
94
|
+
continue
|
|
95
|
+
reason = _candidate_skip_reason(
|
|
96
|
+
message,
|
|
97
|
+
message_id,
|
|
98
|
+
ids_already_in_request,
|
|
99
|
+
superseded_ids,
|
|
100
|
+
policy,
|
|
101
|
+
)
|
|
102
|
+
if reason:
|
|
103
|
+
if reason not in {"stdin_wake_delivered", "stdin_wake_claimed"}:
|
|
104
|
+
max_seen = max(max_seen, message_id)
|
|
105
|
+
services.log(
|
|
106
|
+
"INFO",
|
|
107
|
+
f"channel_llm_inject_skipped message_id={message.get('id')} "
|
|
108
|
+
f"channel={message.get('channel')} reason={reason}",
|
|
109
|
+
)
|
|
110
|
+
continue
|
|
111
|
+
pending.append(message)
|
|
112
|
+
max_seen = message_id
|
|
113
|
+
break
|
|
114
|
+
if not pending:
|
|
115
|
+
if max_seen != last_id:
|
|
116
|
+
repository.commit_cursor(max_seen)
|
|
117
|
+
return services.projection.remove_wake_prompt(body) if wake_request else body
|
|
118
|
+
|
|
119
|
+
base_body = services.projection.remove_wake_prompt(body) if wake_request else body
|
|
120
|
+
messages = [message for message in base_body.get("messages", []) if isinstance(message, dict)]
|
|
121
|
+
messages.append(
|
|
122
|
+
{
|
|
123
|
+
"role": "user",
|
|
124
|
+
"content": [{"type": "text", "text": services.projection.format_prompt(pending)}],
|
|
125
|
+
}
|
|
126
|
+
)
|
|
127
|
+
out = dict(base_body)
|
|
128
|
+
out["messages"] = messages
|
|
129
|
+
ids = ",".join(str(message.get("id") or "") for message in pending)
|
|
130
|
+
channels = ",".join(sorted({str(message.get("channel") or "default") for message in pending}))
|
|
131
|
+
metadata = dict(out.get("metadata") if isinstance(out.get("metadata"), dict) else {})
|
|
132
|
+
metadata.update(
|
|
133
|
+
{
|
|
134
|
+
"ciel_runtime_channel_injected": True,
|
|
135
|
+
"ciel_runtime_channel_message_ids": ids,
|
|
136
|
+
"ciel_runtime_channel_cursor_last_id": str(max_seen),
|
|
137
|
+
}
|
|
138
|
+
)
|
|
139
|
+
out["metadata"] = metadata
|
|
140
|
+
services.log("INFO", f"channel_llm_injected count={len(pending)} message_ids={ids} channels={channels}")
|
|
141
|
+
return out
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _candidate_skip_reason(
|
|
145
|
+
message: dict[str, Any],
|
|
146
|
+
message_id: int,
|
|
147
|
+
ids_already_in_request: set[int],
|
|
148
|
+
superseded_ids: set[int],
|
|
149
|
+
policy: ChannelLlmContextPolicy,
|
|
150
|
+
) -> str:
|
|
151
|
+
if message_id in ids_already_in_request:
|
|
152
|
+
return "already_in_request"
|
|
153
|
+
if message_id in superseded_ids:
|
|
154
|
+
return "superseded_channel_notice"
|
|
155
|
+
reason = policy.skip_reason(message)
|
|
156
|
+
return reason or policy.stdin_skip_reason(message_id)
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""Discover and start HTTP MCP channel transports from configuration files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable, Iterable, Mapping
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True, slots=True)
|
|
12
|
+
class ChannelMcpDiscoveryPorts:
|
|
13
|
+
environment: Mapping[str, str]
|
|
14
|
+
config_paths: Callable[..., list[Path]]
|
|
15
|
+
path_key: Callable[[Path], str]
|
|
16
|
+
read_config: Callable[[Path, Path], list[dict[str, Any]]]
|
|
17
|
+
dedupe: Callable[[Iterable[str]], list[str]]
|
|
18
|
+
native_router_names: frozenset[str]
|
|
19
|
+
public_name: Callable[[str], str]
|
|
20
|
+
start_connection: Callable[[dict[str, Any]], dict[str, Any]]
|
|
21
|
+
log: Callable[[str, str], None]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ChannelMcpDiscoveryService:
|
|
25
|
+
def __init__(self, ports: ChannelMcpDiscoveryPorts) -> None:
|
|
26
|
+
self.ports = ports
|
|
27
|
+
|
|
28
|
+
def runtime_headers(self, server: dict[str, Any]) -> dict[str, str]:
|
|
29
|
+
headers = server.get("headers")
|
|
30
|
+
raw_headers = headers if isinstance(headers, dict) else {}
|
|
31
|
+
projected = {
|
|
32
|
+
str(key): str(value)
|
|
33
|
+
for key, value in raw_headers.items()
|
|
34
|
+
if str(key).strip() and value is not None
|
|
35
|
+
}
|
|
36
|
+
env_headers = server.get("env_http_headers")
|
|
37
|
+
if isinstance(env_headers, dict):
|
|
38
|
+
for raw_header, raw_environment_name in env_headers.items():
|
|
39
|
+
header = str(raw_header or "").strip()
|
|
40
|
+
environment_name = str(raw_environment_name or "").strip()
|
|
41
|
+
value = self.ports.environment.get(environment_name)
|
|
42
|
+
if header and environment_name and value:
|
|
43
|
+
projected[header] = value
|
|
44
|
+
token = str(server.get("bearer_token") or server.get("token") or "").strip()
|
|
45
|
+
token_environment = str(
|
|
46
|
+
server.get("bearer_token_env_var") or server.get("token_env_var") or ""
|
|
47
|
+
).strip()
|
|
48
|
+
if not token and token_environment:
|
|
49
|
+
token = str(self.ports.environment.get(token_environment) or "").strip()
|
|
50
|
+
if token and not any(key.lower() == "authorization" for key in projected):
|
|
51
|
+
projected["Authorization"] = f"Bearer {token}"
|
|
52
|
+
return projected
|
|
53
|
+
|
|
54
|
+
def servers_from_mapping(self, mapping: Any) -> list[dict[str, Any]]:
|
|
55
|
+
if not isinstance(mapping, dict):
|
|
56
|
+
return []
|
|
57
|
+
found: list[dict[str, Any]] = []
|
|
58
|
+
for key in ("mcpServers", "servers"):
|
|
59
|
+
servers = mapping.get(key)
|
|
60
|
+
if not isinstance(servers, dict):
|
|
61
|
+
continue
|
|
62
|
+
for raw_name, raw_server in servers.items():
|
|
63
|
+
projected = self._project_server(raw_name, raw_server)
|
|
64
|
+
if projected is not None:
|
|
65
|
+
found.append(projected)
|
|
66
|
+
return found
|
|
67
|
+
|
|
68
|
+
def _project_server(
|
|
69
|
+
self, raw_name: Any, raw_server: Any
|
|
70
|
+
) -> dict[str, Any] | None:
|
|
71
|
+
name = str(raw_name or "").strip()
|
|
72
|
+
if not name or not isinstance(raw_server, dict):
|
|
73
|
+
return None
|
|
74
|
+
url = str(raw_server.get("url") or raw_server.get("endpoint") or "").strip()
|
|
75
|
+
if not url.startswith(("http://", "https://")):
|
|
76
|
+
return None
|
|
77
|
+
server_type = str(raw_server.get("type") or "").strip().lower()
|
|
78
|
+
if server_type and server_type not in ("sse", "http", "streamable-http"):
|
|
79
|
+
return None
|
|
80
|
+
transport = (
|
|
81
|
+
"streamable-http" if server_type in ("http", "streamable-http") else "sse"
|
|
82
|
+
)
|
|
83
|
+
requires_session = raw_server.get(
|
|
84
|
+
"streamable_requires_session",
|
|
85
|
+
raw_server.get(
|
|
86
|
+
"require_session", raw_server.get("mcp_session_required", True)
|
|
87
|
+
),
|
|
88
|
+
)
|
|
89
|
+
return {
|
|
90
|
+
"name": f"mcp-{name}",
|
|
91
|
+
"url": url,
|
|
92
|
+
"type": server_type or transport,
|
|
93
|
+
"transport": transport,
|
|
94
|
+
"headers": self.runtime_headers(raw_server),
|
|
95
|
+
"channel": name,
|
|
96
|
+
"sender_id": name,
|
|
97
|
+
"recipient": "all",
|
|
98
|
+
"mcp": True,
|
|
99
|
+
"streamable_requires_session": requires_session,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
def external_names(
|
|
103
|
+
self,
|
|
104
|
+
passthrough: list[str] | None = None,
|
|
105
|
+
cwd: Path | None = None,
|
|
106
|
+
home: Path | None = None,
|
|
107
|
+
extra_config_paths: list[Path | str] | None = None,
|
|
108
|
+
) -> list[str]:
|
|
109
|
+
cwd = cwd or Path.cwd()
|
|
110
|
+
names: list[str] = []
|
|
111
|
+
for path in self._paths(
|
|
112
|
+
passthrough, cwd, home, extra_config_paths, include_default=True
|
|
113
|
+
):
|
|
114
|
+
for server in self.ports.read_config(path, cwd):
|
|
115
|
+
name = str(server.get("channel") or "").strip()
|
|
116
|
+
if name and name.lower() not in self.ports.native_router_names:
|
|
117
|
+
names.append(name)
|
|
118
|
+
return self.ports.dedupe(names)
|
|
119
|
+
|
|
120
|
+
def auto_start(
|
|
121
|
+
self,
|
|
122
|
+
passthrough: list[str] | None = None,
|
|
123
|
+
cwd: Path | None = None,
|
|
124
|
+
home: Path | None = None,
|
|
125
|
+
extra_config_paths: list[Path | str] | None = None,
|
|
126
|
+
allowed_server_names: Iterable[str] | None = None,
|
|
127
|
+
include_default_paths: bool = True,
|
|
128
|
+
) -> list[dict[str, Any]]:
|
|
129
|
+
cwd = cwd or Path.cwd()
|
|
130
|
+
allowed = (
|
|
131
|
+
{
|
|
132
|
+
self.ports.public_name(str(name or "").strip())
|
|
133
|
+
for name in allowed_server_names
|
|
134
|
+
if str(name or "").strip()
|
|
135
|
+
}
|
|
136
|
+
if allowed_server_names is not None
|
|
137
|
+
else None
|
|
138
|
+
)
|
|
139
|
+
started: list[dict[str, Any]] = []
|
|
140
|
+
for path in self._paths(
|
|
141
|
+
passthrough,
|
|
142
|
+
cwd,
|
|
143
|
+
home,
|
|
144
|
+
extra_config_paths,
|
|
145
|
+
include_default=include_default_paths,
|
|
146
|
+
):
|
|
147
|
+
for server in self.ports.read_config(path, cwd):
|
|
148
|
+
public_name = self.ports.public_name(str(server.get("name") or ""))
|
|
149
|
+
if allowed is not None and public_name not in allowed:
|
|
150
|
+
continue
|
|
151
|
+
try:
|
|
152
|
+
status = self.ports.start_connection(server)
|
|
153
|
+
started.append(status)
|
|
154
|
+
self.ports.log(
|
|
155
|
+
"INFO",
|
|
156
|
+
f"channel_sse_auto_started name={status.get('name')} "
|
|
157
|
+
f"url={status.get('url')}",
|
|
158
|
+
)
|
|
159
|
+
except Exception as exc:
|
|
160
|
+
self.ports.log(
|
|
161
|
+
"WARN",
|
|
162
|
+
f"channel_sse_auto_start_failed path={path} "
|
|
163
|
+
f"error={type(exc).__name__}: {exc}",
|
|
164
|
+
)
|
|
165
|
+
return started
|
|
166
|
+
|
|
167
|
+
def _paths(
|
|
168
|
+
self,
|
|
169
|
+
passthrough: list[str] | None,
|
|
170
|
+
cwd: Path,
|
|
171
|
+
home: Path | None,
|
|
172
|
+
extra_config_paths: list[Path | str] | None,
|
|
173
|
+
*,
|
|
174
|
+
include_default: bool,
|
|
175
|
+
) -> list[Path]:
|
|
176
|
+
paths = [Path(path).expanduser() for path in extra_config_paths or []]
|
|
177
|
+
if include_default:
|
|
178
|
+
paths.extend(self.ports.config_paths(passthrough, cwd, home))
|
|
179
|
+
unique: list[Path] = []
|
|
180
|
+
seen: set[str] = set()
|
|
181
|
+
for path in paths:
|
|
182
|
+
key = self.ports.path_key(path)
|
|
183
|
+
if key not in seen and path.is_file():
|
|
184
|
+
seen.add(key)
|
|
185
|
+
unique.append(path)
|
|
186
|
+
return unique
|