@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,73 @@
1
+ """Transparent end-to-end request header forwarding policies."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+
8
+ HOP_BY_HOP_REQUEST_HEADERS = frozenset(
9
+ {
10
+ "accept-encoding",
11
+ "connection",
12
+ "content-length",
13
+ "expect",
14
+ "forwarded",
15
+ "host",
16
+ "keep-alive",
17
+ "proxy-authenticate",
18
+ "proxy-authorization",
19
+ "te",
20
+ "trailer",
21
+ "transfer-encoding",
22
+ "upgrade",
23
+ "via",
24
+ "x-forwarded-for",
25
+ "x-forwarded-host",
26
+ "x-forwarded-proto",
27
+ }
28
+ )
29
+ CONFIGURED_PROVIDER_CREDENTIAL_HEADERS = frozenset(
30
+ {"authorization", "cookie", "set-cookie", "x-api-key"}
31
+ )
32
+
33
+
34
+ def project_end_to_end_request_headers(
35
+ inbound_headers: Any | None,
36
+ *,
37
+ replace_credentials: bool,
38
+ transport_headers: frozenset[str] = HOP_BY_HOP_REQUEST_HEADERS,
39
+ ) -> dict[str, str]:
40
+ """Copy client headers verbatim except transport-owned connection headers.
41
+
42
+ Header names and values are not interpreted or reconstructed. Configured
43
+ provider routes additionally remove client credentials before the provider
44
+ adapter installs the selected provider credential.
45
+ """
46
+
47
+ if inbound_headers is None:
48
+ return {}
49
+ try:
50
+ items = inbound_headers.items()
51
+ except Exception:
52
+ return {}
53
+ excluded = transport_headers
54
+ if replace_credentials:
55
+ excluded = excluded | CONFIGURED_PROVIDER_CREDENTIAL_HEADERS
56
+ forwarded: dict[str, str] = {}
57
+ for raw_name, raw_value in items:
58
+ original_name = str(raw_name)
59
+ normalized_name = original_name.casefold()
60
+ if (
61
+ normalized_name not in excluded
62
+ and not normalized_name.startswith("x-ciel-runtime-")
63
+ and raw_value is not None
64
+ ):
65
+ forwarded[original_name] = str(raw_value)
66
+ return forwarded
67
+
68
+
69
+ __all__ = [
70
+ "CONFIGURED_PROVIDER_CREDENTIAL_HEADERS",
71
+ "HOP_BY_HOP_REQUEST_HEADERS",
72
+ "project_end_to_end_request_headers",
73
+ ]
@@ -0,0 +1,221 @@
1
+ """Application service for applying headless environment configuration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from collections.abc import Callable, Mapping
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+
10
+
11
+ @dataclass(frozen=True, slots=True)
12
+ class HeadlessConfigCommands:
13
+ set_language: Callable[[str], None]
14
+ set_web_fetch: Callable[[bool], None]
15
+ set_provider: Callable[[str], None]
16
+ set_api_keys: Callable[[str, list[str]], None]
17
+ set_api_key: Callable[[str, str], None]
18
+ set_base_url: Callable[[str, str], None]
19
+ set_model: Callable[[str], None]
20
+ set_advisor_model: Callable[[str], None]
21
+ set_provider_options: Callable[[list[str]], None]
22
+ set_ollama_options: Callable[[list[str]], None]
23
+
24
+
25
+ @dataclass(frozen=True, slots=True)
26
+ class HeadlessChannelCommands:
27
+ add_channel: Callable[[str], None]
28
+ set_delivery: Callable[[str], None]
29
+
30
+
31
+ @dataclass(frozen=True, slots=True)
32
+ class HeadlessConfigServices:
33
+ environ: Mapping[str, str]
34
+ parse_bool: Callable[[str | None, bool | None], bool | None]
35
+ current_provider: Callable[[], str]
36
+ commands: HeadlessConfigCommands
37
+ channels: HeadlessChannelCommands
38
+
39
+
40
+ @dataclass(frozen=True, slots=True)
41
+ class HeadlessConfigResult:
42
+ skip_menu: bool
43
+ web_search_override: bool | None
44
+ update_check_override: bool | None
45
+ self_update_check_override: bool | None
46
+ force_menu: bool
47
+
48
+ def as_tuple(self) -> tuple[bool, bool | None, bool | None, bool | None, bool]:
49
+ return (
50
+ self.skip_menu,
51
+ self.web_search_override,
52
+ self.update_check_override,
53
+ self.self_update_check_override,
54
+ self.force_menu,
55
+ )
56
+
57
+
58
+ @dataclass(frozen=True, slots=True)
59
+ class HeadlessEnvFileLoader:
60
+ load: Callable[..., None]
61
+
62
+ def pop_args(self, argv: list[str]) -> list[str]:
63
+ cleaned: list[str] = []
64
+ index = 0
65
+ while index < len(argv):
66
+ argument = argv[index]
67
+ if (
68
+ argument == "--ca-env-file"
69
+ or argument.startswith("--ca-env-file=")
70
+ ):
71
+ value = (
72
+ argument.split("=", 1)[1]
73
+ if "=" in argument
74
+ else None
75
+ )
76
+ if value is None:
77
+ if index + 1 >= len(argv):
78
+ raise SystemExit(
79
+ "Missing path for --ca-env-file"
80
+ )
81
+ value = argv[index + 1]
82
+ index += 2
83
+ else:
84
+ index += 1
85
+ path = Path(value).expanduser()
86
+ if not path.exists():
87
+ raise SystemExit(
88
+ f"--ca-env-file not found: {path}"
89
+ )
90
+ self.load(path, override=True)
91
+ continue
92
+ cleaned.append(argument)
93
+ index += 1
94
+ return cleaned
95
+
96
+
97
+ PROVIDER_OPTION_ENV = {
98
+ "CIEL_RUNTIME_MAX_OUTPUT_TOKENS": "max_output_tokens",
99
+ "CIEL_RUNTIME_CONTEXT_WINDOW": "context_window",
100
+ "CIEL_RUNTIME_REQUEST_TIMEOUT_MS": "request_timeout_ms",
101
+ "CIEL_RUNTIME_STREAM_IDLE_TIMEOUT_MS": "stream_idle_timeout_ms",
102
+ "CIEL_RUNTIME_RATE_LIMIT_RPM": "rate_limit_rpm",
103
+ "CIEL_RUNTIME_RATE_LIMIT_STATUS": "rate_limit_status",
104
+ "CIEL_RUNTIME_STREAM": "stream_enabled",
105
+ "CIEL_RUNTIME_STREAM_WORD_CHUNKING": "stream_word_chunking",
106
+ }
107
+
108
+
109
+ def apply_headless_config(services: HeadlessConfigServices) -> HeadlessConfigResult:
110
+ env = services.environ
111
+ commands = services.commands
112
+ skip_menu = env.get("CIEL_RUNTIME_SKIP_MENU") == "1"
113
+ force_menu = bool(services.parse_bool(env.get("CIEL_RUNTIME_FORCE_MENU"), False))
114
+ web_search_override = services.parse_bool(env.get("CIEL_RUNTIME_WEB_SEARCH"), None)
115
+ update_check_override = services.parse_bool(env.get("CIEL_RUNTIME_UPDATE_CHECK"), None)
116
+ self_update_check_override = services.parse_bool(env.get("CIEL_RUNTIME_SELF_UPDATE_CHECK"), None)
117
+
118
+ language = env.get("CIEL_RUNTIME_LANGUAGE", "").strip()
119
+ if language:
120
+ commands.set_language(language)
121
+ skip_menu = True
122
+ web_fetch = services.parse_bool(env.get("CIEL_RUNTIME_WEB_FETCH"), None)
123
+ if web_fetch is not None:
124
+ commands.set_web_fetch(web_fetch)
125
+ skip_menu = True
126
+ provider = env.get("CIEL_RUNTIME_PROVIDER", "").strip()
127
+ if provider:
128
+ commands.set_provider(provider)
129
+ skip_menu = True
130
+
131
+ current_provider = services.current_provider()
132
+ skip_menu = _apply_api_key_config(env, current_provider, commands) or skip_menu
133
+
134
+ base_url = env.get("CIEL_RUNTIME_BASE_URL", "").strip()
135
+ if base_url:
136
+ commands.set_base_url(services.current_provider(), base_url)
137
+ skip_menu = True
138
+ model = env.get("CIEL_RUNTIME_MODEL", "").strip()
139
+ if model:
140
+ commands.set_model(model)
141
+ skip_menu = True
142
+ advisor_model = env.get("CIEL_RUNTIME_ADVISOR_MODEL", "").strip()
143
+ if advisor_model:
144
+ commands.set_advisor_model(advisor_model)
145
+ skip_menu = True
146
+
147
+ provider_values = [
148
+ f"{option_key}={env[env_key].strip()}"
149
+ for env_key, option_key in PROVIDER_OPTION_ENV.items()
150
+ if env.get(env_key, "").strip()
151
+ ]
152
+ if provider_values:
153
+ commands.set_provider_options(provider_values)
154
+ skip_menu = True
155
+
156
+ ollama_values = _ollama_option_values(env)
157
+ if ollama_values:
158
+ commands.set_ollama_options(ollama_values)
159
+ skip_menu = True
160
+
161
+ for channel in _split_values(env.get("CIEL_RUNTIME_CHANNELS", "")):
162
+ services.channels.add_channel(channel)
163
+ skip_menu = True
164
+ for channel in _split_values(env.get("CIEL_RUNTIME_DEV_CHANNELS", "")):
165
+ services.channels.add_channel(channel)
166
+ skip_menu = True
167
+ channel_delivery = env.get("CIEL_RUNTIME_CHANNEL_DELIVERY", "").strip()
168
+ if channel_delivery:
169
+ services.channels.set_delivery(channel_delivery)
170
+ skip_menu = True
171
+
172
+ return HeadlessConfigResult(
173
+ skip_menu=skip_menu,
174
+ web_search_override=web_search_override,
175
+ update_check_override=update_check_override,
176
+ self_update_check_override=self_update_check_override,
177
+ force_menu=force_menu,
178
+ )
179
+
180
+
181
+ def _apply_api_key_config(
182
+ env: Mapping[str, str],
183
+ provider: str,
184
+ commands: HeadlessConfigCommands,
185
+ ) -> bool:
186
+ api_keys_env = env.get("CIEL_RUNTIME_API_KEYS_ENV", "").strip()
187
+ api_keys = env.get("CIEL_RUNTIME_API_KEYS", "").strip()
188
+ api_key_env = env.get("CIEL_RUNTIME_API_KEY_ENV", "").strip()
189
+ api_key = env.get("CIEL_RUNTIME_API_KEY", "").strip()
190
+ if api_keys_env:
191
+ value = env.get(api_keys_env, "")
192
+ if not value:
193
+ raise SystemExit(f"Environment variable {api_keys_env} is empty or not set")
194
+ commands.set_api_keys(provider, [value])
195
+ return True
196
+ if api_keys:
197
+ commands.set_api_keys(provider, [api_keys])
198
+ return True
199
+ if api_key_env:
200
+ value = env.get(api_key_env, "")
201
+ if not value:
202
+ raise SystemExit(f"Environment variable {api_key_env} is empty or not set")
203
+ commands.set_api_key(provider, value)
204
+ return True
205
+ if api_key:
206
+ commands.set_api_key(provider, api_key)
207
+ return True
208
+ return False
209
+
210
+
211
+ def _ollama_option_values(env: Mapping[str, str]) -> list[str]:
212
+ values: list[str] = []
213
+ num_ctx = env.get("CIEL_RUNTIME_OLLAMA_NUM_CTX", "").strip()
214
+ if num_ctx:
215
+ values.append(f"num_ctx={num_ctx}")
216
+ values.extend(item for item in env.get("CIEL_RUNTIME_OLLAMA_OPTIONS", "").replace(",", " ").split() if item)
217
+ return values
218
+
219
+
220
+ def _split_values(value: str) -> list[str]:
221
+ return [item.strip() for item in re.split(r"[\s,]+", value.strip()) if item.strip()]
@@ -0,0 +1,129 @@
1
+ """HTTP response writer and pending channel-delivery guard."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import errno
6
+ import json
7
+ from typing import Any, Callable
8
+
9
+
10
+ CLIENT_DISCONNECT_ERRNOS = {
11
+ errno.EPIPE,
12
+ errno.ECONNRESET,
13
+ getattr(errno, "ECONNABORTED", errno.ECONNRESET),
14
+ }
15
+
16
+
17
+ class HttpResponseAdapter:
18
+ def __init__(self, log: Callable[[str, str], None]) -> None:
19
+ self.log = log
20
+
21
+ @staticmethod
22
+ def write_json(handler: Any, value: Any, status: int = 200) -> None:
23
+ body = json.dumps(value).encode("utf-8")
24
+ handler.send_response(status)
25
+ handler.send_header("content-type", "application/json")
26
+ handler.send_header("content-length", str(len(body)))
27
+ handler.end_headers()
28
+ handler.wfile.write(body)
29
+
30
+ @staticmethod
31
+ def is_client_disconnect(error: BaseException) -> bool:
32
+ if isinstance(error, (BrokenPipeError, ConnectionResetError, ConnectionAbortedError)):
33
+ return True
34
+ return isinstance(error, OSError) and getattr(error, "errno", None) in CLIENT_DISCONNECT_ERRNOS
35
+
36
+ def try_write_json(self, handler: Any, value: Any, status: int = 200) -> bool:
37
+ try:
38
+ self.write_json(handler, value, status)
39
+ return True
40
+ except Exception as exc:
41
+ if self.is_client_disconnect(exc):
42
+ self.log(
43
+ "WARN",
44
+ f"write_json_client_disconnected status={status} "
45
+ f"error={type(exc).__name__}: {exc}",
46
+ )
47
+ return False
48
+ raise
49
+
50
+ @staticmethod
51
+ def response_status(handler: Any) -> int | None:
52
+ try:
53
+ return int(getattr(handler, "_ciel_runtime_response_status", None))
54
+ except Exception:
55
+ return None
56
+
57
+ @staticmethod
58
+ def write_empty(handler: Any, status: int = 202) -> None:
59
+ handler.send_response(status)
60
+ handler.send_header("content-length", "0")
61
+ handler.end_headers()
62
+
63
+ @staticmethod
64
+ def write_accepted(handler: Any) -> None:
65
+ body = b"Accepted"
66
+ handler.send_response(202)
67
+ handler.send_header("content-type", "text/plain")
68
+ handler.send_header("content-length", str(len(body)))
69
+ handler.end_headers()
70
+ handler.wfile.write(body)
71
+
72
+
73
+ class ChannelDeliveryGuard:
74
+ def __init__(self, log: Callable[[str, str], None]) -> None:
75
+ self.log = log
76
+
77
+ @staticmethod
78
+ def metadata_enabled(metadata: dict[str, Any] | None) -> bool:
79
+ return isinstance(metadata, dict) and bool(
80
+ metadata.get("ciel_runtime_channel_cursor_last_id")
81
+ )
82
+
83
+ def begin(self, handler: Any | None, body: dict[str, Any]) -> None:
84
+ if handler is None:
85
+ return
86
+ metadata = body.get("metadata") if isinstance(body.get("metadata"), dict) else {}
87
+ if not self.metadata_enabled(metadata):
88
+ return
89
+ try:
90
+ setattr(handler, "_ciel_runtime_channel_delivery_guard", True)
91
+ setattr(handler, "_ciel_runtime_channel_delivery_ok", False)
92
+ setattr(handler, "_ciel_runtime_channel_delivery_reason", "pending")
93
+ except Exception as exc:
94
+ self.log(
95
+ "WARN",
96
+ f"channel_delivery_guard_begin_failed error={type(exc).__name__}: {exc}",
97
+ )
98
+
99
+ def success(self, handler: Any | None, reason: str = "response_complete") -> None:
100
+ if handler is None or not getattr(handler, "_ciel_runtime_channel_delivery_guard", False):
101
+ return
102
+ try:
103
+ setattr(handler, "_ciel_runtime_channel_delivery_ok", True)
104
+ setattr(handler, "_ciel_runtime_channel_delivery_reason", reason)
105
+ except Exception as exc:
106
+ self.log(
107
+ "WARN",
108
+ f"channel_delivery_guard_success_failed reason={reason} "
109
+ f"error={type(exc).__name__}: {exc}",
110
+ )
111
+
112
+ def failed(self, handler: Any | None, reason: str = "response_failed") -> None:
113
+ if handler is None or not getattr(handler, "_ciel_runtime_channel_delivery_guard", False):
114
+ return
115
+ try:
116
+ setattr(handler, "_ciel_runtime_channel_delivery_ok", False)
117
+ setattr(handler, "_ciel_runtime_channel_delivery_reason", reason)
118
+ except Exception as exc:
119
+ self.log(
120
+ "WARN",
121
+ f"channel_delivery_guard_failure_failed reason={reason} "
122
+ f"error={type(exc).__name__}: {exc}",
123
+ )
124
+
125
+ @staticmethod
126
+ def confirmed(handler: Any | None) -> bool:
127
+ if handler is None or not getattr(handler, "_ciel_runtime_channel_delivery_guard", False):
128
+ return True
129
+ return bool(getattr(handler, "_ciel_runtime_channel_delivery_ok", False))
@@ -0,0 +1,149 @@
1
+ """Discovery and presentation of duplicate Ciel Runtime installations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ import subprocess
7
+ from collections.abc import Callable, Mapping
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+
11
+
12
+ @dataclass(frozen=True, slots=True)
13
+ class InstallDiagnosticsSettings:
14
+ home: Path
15
+ environ: Mapping[str, str]
16
+ windows: bool
17
+
18
+
19
+ @dataclass(frozen=True, slots=True)
20
+ class InstallDiagnosticsPorts:
21
+ extra_dirs: Callable[[], list[Path]]
22
+ package_root: Callable[[Path], Path | None]
23
+ current_root: Callable[[], Path | None]
24
+ parse_version: Callable[[str], tuple[int, ...]]
25
+ diagnostics: Callable[[], list[dict[str, str]]]
26
+ stdin_isatty: Callable[[], bool]
27
+ stdout_isatty: Callable[[], bool]
28
+ write_error: Callable[[str], None]
29
+
30
+
31
+ @dataclass(frozen=True, slots=True)
32
+ class InstallDiagnosticsService:
33
+ settings: InstallDiagnosticsSettings
34
+ ports: InstallDiagnosticsPorts
35
+
36
+ def candidate_dirs(self) -> list[Path]:
37
+ raw_dirs = [
38
+ Path(entry)
39
+ for entry in self.settings.environ.get("PATH", "").split(";" if self.settings.windows else ":")
40
+ if entry
41
+ ]
42
+ raw_dirs.extend(self.ports.extra_dirs())
43
+ raw_dirs.extend(
44
+ [self.settings.home / ".npm-global" / "bin", self.settings.home / "bin"]
45
+ )
46
+ if not self.settings.windows:
47
+ raw_dirs.extend([Path("/usr/local/bin"), Path("/usr/bin")])
48
+ return list(dict.fromkeys(raw_dirs))
49
+
50
+ def candidates(self) -> list[Path]:
51
+ names = ["ciel-runtime"]
52
+ if self.settings.windows:
53
+ names.extend(["ciel-runtime.cmd", "ciel-runtime.exe"])
54
+ candidates: list[Path] = []
55
+ seen: set[str] = set()
56
+ for directory in self.candidate_dirs():
57
+ for name in names:
58
+ candidate = directory / name
59
+ if not candidate.exists():
60
+ continue
61
+ try:
62
+ key = str(candidate.resolve(strict=False))
63
+ except Exception:
64
+ key = str(candidate)
65
+ if key not in seen:
66
+ seen.add(key)
67
+ candidates.append(candidate)
68
+ return candidates
69
+
70
+ def launcher_version(self, path: Path, timeout: float = 5.0) -> str:
71
+ environ = dict(self.settings.environ)
72
+ environ.update(
73
+ CIEL_RUNTIME_SKIP_INSTALL_DIAGNOSTIC="1",
74
+ CIEL_RUNTIME_SKIP_SELF_UPDATE="1",
75
+ CIEL_RUNTIME_SELF_UPDATE_CHECK="off",
76
+ )
77
+ try:
78
+ process = subprocess.run(
79
+ [str(path), "--version"],
80
+ text=True,
81
+ stdout=subprocess.PIPE,
82
+ stderr=subprocess.STDOUT,
83
+ env=environ,
84
+ timeout=timeout,
85
+ )
86
+ except Exception:
87
+ return ""
88
+ if process.returncode != 0:
89
+ return ""
90
+ output = process.stdout or ""
91
+ match = re.search(r"ciel-runtime\s+(.+)", output, re.IGNORECASE)
92
+ return match.group(1).strip() if match else output.strip().splitlines()[-1].strip()
93
+
94
+ def diagnostics(self) -> list[dict[str, str]]:
95
+ rows: list[dict[str, str]] = []
96
+ for launcher in self.candidates():
97
+ root = self.ports.package_root(launcher)
98
+ rows.append(
99
+ {
100
+ "launcher": str(launcher),
101
+ "resolved": str(launcher.resolve(strict=False)),
102
+ "package_root": str(root) if root else "",
103
+ "version": self.launcher_version(launcher),
104
+ }
105
+ )
106
+ return rows
107
+
108
+ def warn_if_multiple(self) -> None:
109
+ if self.settings.environ.get("CIEL_RUNTIME_SKIP_INSTALL_DIAGNOSTIC") == "1":
110
+ return
111
+ if not (self.ports.stdin_isatty() and self.ports.stdout_isatty()):
112
+ return
113
+ rows = self.ports.diagnostics()
114
+ roots = {row["package_root"] for row in rows if row.get("package_root")}
115
+ if len(roots) <= 1:
116
+ return
117
+ current_root = str(self.ports.current_root() or "")
118
+ first = rows[0] if rows else {}
119
+ newest = max(
120
+ (row for row in rows if row.get("version")),
121
+ key=lambda row: self.ports.parse_version(row["version"]),
122
+ default=None,
123
+ )
124
+ self.ports.write_error(
125
+ "Ciel Runtime warning: multiple ciel-runtime npm installs are visible."
126
+ )
127
+ if first:
128
+ self.ports.write_error(
129
+ f" shell resolves ciel-runtime to: {first.get('launcher')} "
130
+ f"({first.get('version') or 'unknown version'})"
131
+ )
132
+ if current_root:
133
+ self.ports.write_error(f" current package root: {current_root}")
134
+ if newest and newest is not first:
135
+ self.ports.write_error(
136
+ f" newer visible install: {newest.get('launcher')} "
137
+ f"({newest.get('version')})"
138
+ )
139
+ self.ports.write_error(
140
+ " Fix by keeping one install prefix: update or uninstall the stale "
141
+ "higher-priority install."
142
+ )
143
+
144
+
145
+ __all__ = [
146
+ "InstallDiagnosticsPorts",
147
+ "InstallDiagnosticsService",
148
+ "InstallDiagnosticsSettings",
149
+ ]
@@ -0,0 +1,123 @@
1
+ """Official Kimi Code credential and request identity compatibility."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import platform
8
+ import re
9
+ import shutil
10
+ import socket
11
+ import subprocess
12
+ import time
13
+ import uuid
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+
18
+ def code_home(home: Path, environ: dict[str, str] | None = None) -> Path:
19
+ values = os.environ if environ is None else environ
20
+ return Path(values.get("KIMI_CODE_HOME") or (home / ".kimi-code"))
21
+
22
+
23
+ def oauth_token_record(home: Path) -> dict[str, Any] | None:
24
+ try:
25
+ value = json.loads(
26
+ (code_home(home) / "credentials" / "kimi-code.json").read_text(
27
+ encoding="utf-8"
28
+ )
29
+ )
30
+ except (OSError, TypeError, ValueError):
31
+ return None
32
+ return value if isinstance(value, dict) else None
33
+
34
+
35
+ def oauth_access_token(home: Path) -> str | None:
36
+ record = oauth_token_record(home)
37
+ if record is None:
38
+ return None
39
+ token = str(record.get("access_token") or "").strip()
40
+ try:
41
+ expires_at = float(record.get("expires_at") or 0)
42
+ except (TypeError, ValueError):
43
+ expires_at = 0
44
+ if not token or (expires_at > 0 and expires_at <= time.time() + 30):
45
+ return None
46
+ return token
47
+
48
+
49
+ def oauth_configured(home: Path) -> bool:
50
+ if oauth_access_token(home) is None:
51
+ return False
52
+ try:
53
+ text = (code_home(home) / "config.toml").read_text(encoding="utf-8")
54
+ except OSError:
55
+ return False
56
+ return "managed:kimi-code" in text and ("oauth" in text or 'api_key = ""' in text)
57
+
58
+
59
+ def code_version() -> str:
60
+ cached = getattr(code_version, "_cached", None)
61
+ if isinstance(cached, str) and cached:
62
+ return cached
63
+ executable = shutil.which("kimi")
64
+ version = "unknown"
65
+ if executable:
66
+ result = subprocess.run(
67
+ [executable, "--version"], text=True, capture_output=True, check=False
68
+ )
69
+ match = re.search(
70
+ r"\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?",
71
+ result.stdout or result.stderr,
72
+ )
73
+ if match:
74
+ version = match.group(0)
75
+ setattr(code_version, "_cached", version)
76
+ return version
77
+
78
+
79
+ def device_id(home: Path) -> str:
80
+ path = code_home(home) / "device_id"
81
+ try:
82
+ existing = path.read_text(encoding="utf-8").strip()
83
+ if existing:
84
+ return existing
85
+ except OSError:
86
+ pass
87
+ value = str(uuid.uuid4())
88
+ path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
89
+ try:
90
+ descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
91
+ with os.fdopen(descriptor, "w", encoding="utf-8") as stream:
92
+ stream.write(value)
93
+ except FileExistsError:
94
+ return path.read_text(encoding="utf-8").strip()
95
+ return value
96
+
97
+
98
+ def identity_headers(home: Path) -> dict[str, str]:
99
+ version = code_version()
100
+ system = platform.system()
101
+ os_name = "Windows_NT" if system == "Windows" else "Darwin" if system == "Darwin" else system
102
+ release = platform.release()
103
+ architecture = platform.machine()
104
+ return {
105
+ "User-Agent": f"kimi-code-cli/{version}",
106
+ "X-Msh-Platform": "kimi_code_cli",
107
+ "X-Msh-Version": version,
108
+ "X-Msh-Device-Name": socket.gethostname(),
109
+ "X-Msh-Device-Model": f"{os_name} {release} {architecture}".strip(),
110
+ "X-Msh-Os-Version": release,
111
+ "X-Msh-Device-Id": device_id(home),
112
+ }
113
+
114
+
115
+ __all__ = [
116
+ "code_home",
117
+ "code_version",
118
+ "device_id",
119
+ "identity_headers",
120
+ "oauth_access_token",
121
+ "oauth_configured",
122
+ "oauth_token_record",
123
+ ]