@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.
Files changed (287) hide show
  1. package/README.md +132 -0
  2. package/ciel-runtime-menu.py +56 -6
  3. package/ciel_runtime.py +9379 -35909
  4. package/ciel_runtime_support/advisor_client.py +193 -0
  5. package/ciel_runtime_support/advisor_policy.py +320 -0
  6. package/ciel_runtime_support/advisor_refinement.py +160 -0
  7. package/ciel_runtime_support/advisor_request_builder.py +261 -0
  8. package/ciel_runtime_support/agy_installer.py +169 -0
  9. package/ciel_runtime_support/agy_mcp_restore.py +182 -0
  10. package/ciel_runtime_support/anthropic_model_policy.py +186 -0
  11. package/ciel_runtime_support/anthropic_response_writer.py +255 -0
  12. package/ciel_runtime_support/anthropic_tool_turns.py +130 -0
  13. package/ciel_runtime_support/api_key_cooldown.py +159 -0
  14. package/ciel_runtime_support/architecture.py +488 -1
  15. package/ciel_runtime_support/architecture_budget.py +42 -0
  16. package/ciel_runtime_support/channel_backlog.py +90 -0
  17. package/ciel_runtime_support/channel_cli.py +119 -0
  18. package/ciel_runtime_support/channel_compact_injection.py +82 -0
  19. package/ciel_runtime_support/channel_compact_poll.py +67 -0
  20. package/ciel_runtime_support/channel_compact_request_repository.py +113 -0
  21. package/ciel_runtime_support/channel_config_service.py +281 -0
  22. package/ciel_runtime_support/channel_connection_lifecycle.py +180 -0
  23. package/ciel_runtime_support/channel_connection_registry.py +128 -0
  24. package/ciel_runtime_support/channel_connection_worker.py +284 -0
  25. package/ciel_runtime_support/channel_cursor_recovery.py +92 -0
  26. package/ciel_runtime_support/channel_cursor_repository.py +89 -0
  27. package/ciel_runtime_support/channel_cursor_service.py +178 -0
  28. package/ciel_runtime_support/channel_event_identity.py +212 -0
  29. package/ciel_runtime_support/channel_event_projection.py +315 -0
  30. package/ciel_runtime_support/channel_inflight.py +127 -0
  31. package/ciel_runtime_support/channel_injection.py +115 -0
  32. package/ciel_runtime_support/channel_launch_guard_repository.py +58 -0
  33. package/ciel_runtime_support/channel_launch_policy.py +180 -0
  34. package/ciel_runtime_support/channel_llm_context.py +156 -0
  35. package/ciel_runtime_support/channel_mcp_discovery.py +186 -0
  36. package/ciel_runtime_support/channel_mcp_http_controller.py +239 -0
  37. package/ciel_runtime_support/channel_mcp_ownership.py +148 -0
  38. package/ciel_runtime_support/channel_mcp_tools.py +240 -0
  39. package/ciel_runtime_support/channel_mcp_transport.py +394 -0
  40. package/ciel_runtime_support/channel_message_dedupe.py +65 -0
  41. package/ciel_runtime_support/channel_message_policy.py +256 -0
  42. package/ciel_runtime_support/channel_message_prompt.py +305 -0
  43. package/ciel_runtime_support/channel_message_repository.py +234 -0
  44. package/ciel_runtime_support/channel_notification_projection.py +217 -0
  45. package/ciel_runtime_support/channel_panel.py +162 -0
  46. package/ciel_runtime_support/channel_pending_injection.py +209 -0
  47. package/ciel_runtime_support/channel_pending_poll.py +109 -0
  48. package/ciel_runtime_support/channel_probe_cache.py +433 -0
  49. package/ciel_runtime_support/channel_probe_report.py +101 -0
  50. package/ciel_runtime_support/channel_runtime_environment.py +181 -0
  51. package/ciel_runtime_support/channel_session_lifecycle.py +114 -0
  52. package/ciel_runtime_support/channel_session_repository.py +90 -0
  53. package/ciel_runtime_support/channel_terminal_dispatch.py +115 -0
  54. package/ciel_runtime_support/channel_terminal_input.py +277 -0
  55. package/ciel_runtime_support/channel_terminal_proxy.py +447 -0
  56. package/ciel_runtime_support/channel_tool_context.py +166 -0
  57. package/ciel_runtime_support/channel_transcript.py +414 -0
  58. package/ciel_runtime_support/channel_transcript_repository.py +96 -0
  59. package/ciel_runtime_support/channel_wake_claim_repository.py +126 -0
  60. package/ciel_runtime_support/channel_wake_delivery_repository.py +88 -0
  61. package/ciel_runtime_support/chat_files.py +138 -0
  62. package/ciel_runtime_support/chat_http_controller.py +235 -0
  63. package/ciel_runtime_support/claude_environment.py +375 -0
  64. package/ciel_runtime_support/claude_router.py +247 -193
  65. package/ciel_runtime_support/cli_dispatch.py +792 -0
  66. package/ciel_runtime_support/cli_parser.py +165 -0
  67. package/ciel_runtime_support/cli_usage.py +100 -0
  68. package/ciel_runtime_support/codex_app_server.py +20 -5
  69. package/ciel_runtime_support/codex_channel_sse_launch.py +87 -0
  70. package/ciel_runtime_support/codex_cli.py +42 -6
  71. package/ciel_runtime_support/codex_config.py +323 -0
  72. package/ciel_runtime_support/codex_launch_configuration.py +240 -0
  73. package/ciel_runtime_support/codex_launch_policy.py +66 -0
  74. package/ciel_runtime_support/codex_mcp_integration.py +195 -0
  75. package/ciel_runtime_support/codex_mcp_restore.py +304 -0
  76. package/ciel_runtime_support/codex_model_catalog.py +133 -0
  77. package/ciel_runtime_support/codex_process_lifecycle.py +271 -0
  78. package/ciel_runtime_support/codex_router.py +147 -1
  79. package/ciel_runtime_support/codex_session_repository.py +115 -0
  80. package/ciel_runtime_support/codex_session_selection.py +114 -0
  81. package/ciel_runtime_support/command_asset_installer.py +103 -0
  82. package/ciel_runtime_support/compatibility_probe.py +295 -0
  83. package/ciel_runtime_support/compatibility_protocol.py +251 -0
  84. package/ciel_runtime_support/compatibility_runtime.py +166 -0
  85. package/ciel_runtime_support/compatibility_test.py +370 -0
  86. package/ciel_runtime_support/config_migrations.py +307 -0
  87. package/ciel_runtime_support/config_repository.py +175 -0
  88. package/ciel_runtime_support/config_value_codec.py +64 -0
  89. package/ciel_runtime_support/configuration_cli.py +374 -0
  90. package/ciel_runtime_support/context_compaction.py +280 -0
  91. package/ciel_runtime_support/context_setup.py +208 -0
  92. package/ciel_runtime_support/context_summary_policy.py +392 -0
  93. package/ciel_runtime_support/credential_cli.py +104 -0
  94. package/ciel_runtime_support/credential_management.py +261 -0
  95. package/ciel_runtime_support/credentials.py +269 -0
  96. package/ciel_runtime_support/executable_discovery.py +141 -0
  97. package/ciel_runtime_support/github_copilot_oauth.py +335 -0
  98. package/ciel_runtime_support/github_copilot_oauth_runtime.py +213 -0
  99. package/ciel_runtime_support/header_forwarding.py +73 -0
  100. package/ciel_runtime_support/headless_config.py +221 -0
  101. package/ciel_runtime_support/http_response.py +129 -0
  102. package/ciel_runtime_support/install_diagnostics.py +149 -0
  103. package/ciel_runtime_support/kimi_identity.py +123 -0
  104. package/ciel_runtime_support/launch_diagnostics.py +204 -0
  105. package/ciel_runtime_support/launch_state.py +127 -0
  106. package/ciel_runtime_support/live_api_key_controller.py +58 -0
  107. package/ciel_runtime_support/llm_config_http.py +148 -0
  108. package/ciel_runtime_support/llm_option_config.py +259 -0
  109. package/ciel_runtime_support/llm_presentation_data.py +447 -0
  110. package/ciel_runtime_support/llm_presets.py +773 -0
  111. package/ciel_runtime_support/lm_studio_runtime.py +401 -0
  112. package/ciel_runtime_support/managed_mcp_config.py +144 -0
  113. package/ciel_runtime_support/managed_mcp_discovery.py +151 -0
  114. package/ciel_runtime_support/managed_service_cleanup.py +89 -0
  115. package/ciel_runtime_support/mcp_config_reader.py +230 -0
  116. package/ciel_runtime_support/mcp_http_proxy.py +607 -0
  117. package/ciel_runtime_support/mcp_inventory.py +59 -0
  118. package/ciel_runtime_support/mcp_notification_wait_policy.py +159 -0
  119. package/ciel_runtime_support/mcp_probe_codec.py +136 -0
  120. package/ciel_runtime_support/mcp_probe_transport.py +328 -0
  121. package/ciel_runtime_support/mcp_proxy_codec.py +345 -0
  122. package/ciel_runtime_support/mcp_proxy_config.py +107 -0
  123. package/ciel_runtime_support/mcp_proxy_notifications.py +261 -0
  124. package/ciel_runtime_support/mcp_proxy_process.py +560 -0
  125. package/ciel_runtime_support/mcp_split_proxy_http.py +165 -0
  126. package/ciel_runtime_support/mcp_stdio_probe.py +240 -0
  127. package/ciel_runtime_support/mcp_transport.py +146 -0
  128. package/ciel_runtime_support/model_cache_lifecycle.py +78 -0
  129. package/ciel_runtime_support/model_catalog_projection.py +61 -0
  130. package/ciel_runtime_support/model_context_hints.py +109 -0
  131. package/ciel_runtime_support/model_panel.py +147 -0
  132. package/ciel_runtime_support/model_registry_repository.py +231 -0
  133. package/ciel_runtime_support/npm_runtime.py +191 -0
  134. package/ciel_runtime_support/ollama_catalog.py +462 -0
  135. package/ciel_runtime_support/ollama_catalog_cli.py +41 -0
  136. package/ciel_runtime_support/ollama_catalog_repository.py +75 -0
  137. package/ciel_runtime_support/ollama_context_sync.py +87 -0
  138. package/ciel_runtime_support/ollama_forwarding.py +449 -0
  139. package/ciel_runtime_support/openai_chat_passthrough.py +67 -0
  140. package/ciel_runtime_support/openai_chat_router.py +64 -0
  141. package/ciel_runtime_support/openai_forwarding.py +194 -0
  142. package/ciel_runtime_support/openai_responses_router.py +291 -0
  143. package/ciel_runtime_support/openai_responses_stream.py +135 -0
  144. package/ciel_runtime_support/output_budget.py +89 -0
  145. package/ciel_runtime_support/package_lifecycle.py +217 -0
  146. package/ciel_runtime_support/plan_artifact_controller.py +104 -0
  147. package/ciel_runtime_support/prelaunch.py +959 -0
  148. package/ciel_runtime_support/prelaunch_launch_preference.py +75 -0
  149. package/ciel_runtime_support/prelaunch_panel_projection.py +396 -0
  150. package/ciel_runtime_support/prelaunch_terminal.py +764 -0
  151. package/ciel_runtime_support/process_control.py +708 -0
  152. package/ciel_runtime_support/prompt_compaction.py +322 -0
  153. package/ciel_runtime_support/prompt_injection.py +176 -0
  154. package/ciel_runtime_support/protocols/__init__.py +24 -0
  155. package/ciel_runtime_support/protocols/anthropic_content.py +29 -0
  156. package/ciel_runtime_support/protocols/anthropic_thinking_policy.py +336 -0
  157. package/ciel_runtime_support/protocols/chat_projection.py +315 -0
  158. package/ciel_runtime_support/protocols/conversation_policy.py +217 -0
  159. package/ciel_runtime_support/protocols/conversation_turn_policy.py +972 -0
  160. package/ciel_runtime_support/protocols/ollama_chat.py +111 -0
  161. package/ciel_runtime_support/protocols/ollama_response.py +231 -0
  162. package/ciel_runtime_support/protocols/openai_reasoning.py +75 -0
  163. package/ciel_runtime_support/protocols/openai_responses.py +271 -0
  164. package/ciel_runtime_support/protocols/pseudo_tool_history.py +248 -0
  165. package/ciel_runtime_support/protocols/tool_result_projection.py +112 -0
  166. package/ciel_runtime_support/provider_adapters.py +160 -0
  167. package/ciel_runtime_support/provider_catalog_sources.py +316 -0
  168. package/ciel_runtime_support/provider_choice.py +201 -0
  169. package/ciel_runtime_support/provider_compatibility.py +165 -0
  170. package/ciel_runtime_support/provider_config_mutations.py +361 -0
  171. package/ciel_runtime_support/provider_configuration_service.py +162 -0
  172. package/ciel_runtime_support/provider_context.py +308 -0
  173. package/ciel_runtime_support/provider_contract_projection.py +73 -0
  174. package/ciel_runtime_support/provider_descriptor.py +82 -0
  175. package/ciel_runtime_support/provider_endpoint_policy.py +130 -0
  176. package/ciel_runtime_support/provider_endpoint_probe.py +165 -0
  177. package/ciel_runtime_support/provider_launch_endpoint.py +109 -0
  178. package/ciel_runtime_support/provider_limits.py +457 -0
  179. package/ciel_runtime_support/provider_model_identity.py +139 -0
  180. package/ciel_runtime_support/provider_model_selection.py +431 -0
  181. package/ciel_runtime_support/provider_model_specs.py +142 -0
  182. package/ciel_runtime_support/provider_models.py +263 -0
  183. package/ciel_runtime_support/provider_network.py +176 -0
  184. package/ciel_runtime_support/provider_option_cli.py +238 -0
  185. package/ciel_runtime_support/provider_option_panel.py +275 -0
  186. package/ciel_runtime_support/provider_option_status.py +192 -0
  187. package/ciel_runtime_support/provider_policy.py +101 -0
  188. package/ciel_runtime_support/provider_query_policy.py +67 -0
  189. package/ciel_runtime_support/provider_readiness.py +112 -0
  190. package/ciel_runtime_support/provider_request_access.py +131 -0
  191. package/ciel_runtime_support/provider_request_builder.py +250 -0
  192. package/ciel_runtime_support/provider_responses_passthrough.py +81 -0
  193. package/ciel_runtime_support/provider_runtime_info.py +113 -0
  194. package/ciel_runtime_support/provider_runtime_modes.py +150 -0
  195. package/ciel_runtime_support/provider_sampling_policy.py +46 -0
  196. package/ciel_runtime_support/provider_status.py +145 -0
  197. package/ciel_runtime_support/provider_timeout_policy.py +184 -0
  198. package/ciel_runtime_support/provider_tool_policy.py +145 -0
  199. package/ciel_runtime_support/providers/__init__.py +65 -0
  200. package/ciel_runtime_support/providers/anthropic.py +160 -0
  201. package/ciel_runtime_support/providers/anthropic_catalog.py +119 -0
  202. package/ciel_runtime_support/providers/base.py +232 -0
  203. package/ciel_runtime_support/providers/catalog.py +326 -0
  204. package/ciel_runtime_support/providers/cloud.py +194 -0
  205. package/ciel_runtime_support/providers/constants.py +54 -0
  206. package/ciel_runtime_support/providers/deepseek.py +115 -0
  207. package/ciel_runtime_support/providers/fireworks.py +158 -0
  208. package/ciel_runtime_support/providers/github_copilot_oauth.py +199 -0
  209. package/ciel_runtime_support/providers/kimi.py +297 -0
  210. package/ciel_runtime_support/providers/lm_studio.py +76 -0
  211. package/ciel_runtime_support/providers/meta.py +257 -0
  212. package/ciel_runtime_support/providers/native.py +194 -0
  213. package/ciel_runtime_support/providers/nim.py +69 -0
  214. package/ciel_runtime_support/providers/nvidia.py +158 -0
  215. package/ciel_runtime_support/providers/nvidia_runtime.py +285 -0
  216. package/ciel_runtime_support/providers/ollama.py +181 -0
  217. package/ciel_runtime_support/providers/ollama_context.py +195 -0
  218. package/ciel_runtime_support/providers/ollama_runtime.py +213 -0
  219. package/ciel_runtime_support/providers/opencode.py +220 -0
  220. package/ciel_runtime_support/providers/opencode_go.py +37 -0
  221. package/ciel_runtime_support/providers/openrouter.py +57 -0
  222. package/ciel_runtime_support/providers/vllm.py +65 -0
  223. package/ciel_runtime_support/providers/zai.py +119 -0
  224. package/ciel_runtime_support/pseudo_tool_parser.py +115 -0
  225. package/ciel_runtime_support/rate_limit_policy.py +117 -0
  226. package/ciel_runtime_support/rate_limit_repository.py +154 -0
  227. package/ciel_runtime_support/registry.py +46 -0
  228. package/ciel_runtime_support/request_shortcuts.py +253 -0
  229. package/ciel_runtime_support/request_trace.py +323 -0
  230. package/ciel_runtime_support/response_collection.py +209 -0
  231. package/ciel_runtime_support/router_access.py +238 -0
  232. package/ciel_runtime_support/router_client_lifecycle.py +366 -0
  233. package/ciel_runtime_support/router_health_policy.py +101 -0
  234. package/ciel_runtime_support/router_http.py +513 -0
  235. package/ciel_runtime_support/router_process_lifecycle.py +401 -0
  236. package/ciel_runtime_support/router_rate_limit_service.py +285 -0
  237. package/ciel_runtime_support/router_server_runtime.py +103 -0
  238. package/ciel_runtime_support/router_shortcuts.py +201 -0
  239. package/ciel_runtime_support/routing_fallback.py +73 -0
  240. package/ciel_runtime_support/runtime_activity_repository.py +143 -0
  241. package/ciel_runtime_support/runtime_adapters.py +104 -0
  242. package/ciel_runtime_support/runtime_command_factory.py +73 -0
  243. package/ciel_runtime_support/runtime_compatibility.py +50 -0
  244. package/ciel_runtime_support/runtime_constants.py +178 -0
  245. package/ciel_runtime_support/runtime_launch.py +1602 -0
  246. package/ciel_runtime_support/runtime_llm_options.py +312 -0
  247. package/ciel_runtime_support/runtime_logging.py +161 -0
  248. package/ciel_runtime_support/runtime_paths.py +157 -0
  249. package/ciel_runtime_support/runtime_restart.py +84 -0
  250. package/ciel_runtime_support/runtime_upgrade.py +149 -0
  251. package/ciel_runtime_support/secure_json_repository.py +55 -0
  252. package/ciel_runtime_support/session_import.py +356 -0
  253. package/ciel_runtime_support/settings_repository.py +8 -0
  254. package/ciel_runtime_support/slash_command_assets.py +211 -0
  255. package/ciel_runtime_support/sse_stream.py +57 -0
  256. package/ciel_runtime_support/sse_trace.py +225 -0
  257. package/ciel_runtime_support/statusline_script.py +593 -0
  258. package/ciel_runtime_support/statusline_settings.py +53 -0
  259. package/ciel_runtime_support/stream_chunk_policy.py +18 -0
  260. package/ciel_runtime_support/streaming_anthropic.py +1955 -0
  261. package/ciel_runtime_support/synthetic_tool_policy.py +105 -0
  262. package/ciel_runtime_support/terminal_platform_io.py +127 -0
  263. package/ciel_runtime_support/timeout_profile.py +196 -0
  264. package/ciel_runtime_support/tool_dialects.py +85 -0
  265. package/ciel_runtime_support/tool_exposure_policy.py +63 -0
  266. package/ciel_runtime_support/tool_guard_hooks.py +218 -0
  267. package/ciel_runtime_support/tool_request_projection.py +96 -0
  268. package/ciel_runtime_support/tool_schema.py +483 -0
  269. package/ciel_runtime_support/tool_side_effect_dedupe.py +95 -0
  270. package/ciel_runtime_support/ui_text.py +266 -0
  271. package/ciel_runtime_support/upstream_error_policy.py +104 -0
  272. package/ciel_runtime_support/upstream_retry.py +419 -0
  273. package/ciel_runtime_support/upstream_stream_io.py +106 -0
  274. package/ciel_runtime_support/usage_events.py +96 -0
  275. package/ciel_runtime_support/visible_stream_filters.py +130 -0
  276. package/ciel_runtime_support/web_endpoints.py +447 -0
  277. package/ciel_runtime_support/web_ui.py +915 -0
  278. package/ciel_runtime_support/web_ui_controller.py +189 -0
  279. package/ciel_runtime_support/windows_console_input.py +137 -0
  280. package/ciel_runtime_support/windows_console_mode.py +112 -0
  281. package/docs/Architecture.md +54 -0
  282. package/docs/Configuration.md +17 -0
  283. package/docs/Module-Map.md +1093 -26
  284. package/docs/Providers.md +46 -1
  285. package/docs/adr/0001-runtime-bounded-contexts.md +295 -0
  286. package/npm-bin/run-ciel-runtime.js +22 -2
  287. 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