@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,89 @@
1
+ """Provider request output-token budgeting policy."""
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
+ @dataclass(frozen=True, slots=True)
11
+ class OpenAIContextBudgetPolicy:
12
+ """Resolve context capacity for providers using OpenAI-compatible payloads."""
13
+
14
+ positive_int: Callable[[Any], int | None]
15
+ runtime_capacity: Callable[[str, dict[str, Any]], int]
16
+ nvidia_default: Callable[[str], int]
17
+
18
+ def context_limit(self, provider: str, config: dict[str, Any]) -> int:
19
+ configured = self.positive_int(config.get("context_window")) or self.positive_int(
20
+ config.get("max_model_len")
21
+ )
22
+ if provider in ("vllm", "self-hosted-nim"):
23
+ runtime = self.runtime_capacity(provider, config)
24
+ if configured and str(config.get("llm_preset") or "").strip():
25
+ return min(configured, runtime) if runtime else configured
26
+ if runtime:
27
+ return runtime
28
+ if configured:
29
+ return configured
30
+ if provider == "nvidia-hosted":
31
+ return self.nvidia_default(str(config.get("current_model") or ""))
32
+ return 65536
33
+
34
+
35
+ @dataclass(frozen=True, slots=True)
36
+ class OutputBudgetPolicy:
37
+ positive_int: Callable[[Any], int | None]
38
+ estimate_tokens: Callable[[Any, dict[int, int] | None], int]
39
+ provider_options: Callable[[dict[str, Any]], dict[str, Any]]
40
+
41
+ def configured_tokens(
42
+ self,
43
+ config: dict[str, Any],
44
+ body: dict[str, Any],
45
+ option_key: str | None = None,
46
+ ) -> int | None:
47
+ configured = self.positive_int(config.get("max_output_tokens"))
48
+ if option_key:
49
+ configured = (
50
+ self.positive_int(self.provider_options(config).get(option_key))
51
+ or configured
52
+ )
53
+ requested = self.positive_int(body.get("max_tokens"))
54
+ if configured and requested:
55
+ return min(configured, requested)
56
+ return configured or requested
57
+
58
+ def reserve_tokens(
59
+ self, config: dict[str, Any], context_limit: int | None
60
+ ) -> int:
61
+ configured = self.positive_int(config.get("context_reserve_tokens"))
62
+ if configured:
63
+ return configured
64
+ if not context_limit:
65
+ return 1024
66
+ return max(1024, min(32768, int(context_limit) // 32))
67
+
68
+ def cap_tokens_for_context(
69
+ self,
70
+ config: dict[str, Any],
71
+ body: dict[str, Any],
72
+ payload: Any,
73
+ context_limit: int | None,
74
+ configured: int | None,
75
+ _token_cache: dict[int, int] | None = None,
76
+ ) -> int | None:
77
+ del body
78
+ if not configured:
79
+ return None
80
+ if not context_limit:
81
+ return configured
82
+ reserve = self.reserve_tokens(config, context_limit)
83
+ available = context_limit - self.estimate_tokens(payload, _token_cache) - reserve
84
+ if available <= 0:
85
+ return min(configured, 256)
86
+ return max(1, min(configured, available))
87
+
88
+
89
+ __all__ = ["OpenAIContextBudgetPolicy", "OutputBudgetPolicy"]
@@ -0,0 +1,217 @@
1
+ """Reusable npm-backed runtime package installation and update lifecycle."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ import os
7
+ from pathlib import Path
8
+ import subprocess
9
+ from typing import Callable
10
+
11
+
12
+ @dataclass(frozen=True, slots=True)
13
+ class NpmPackageLifecyclePorts:
14
+ find_executable: Callable[[str], str | None]
15
+ install_prefix: Callable[[], Path | None]
16
+ install_command: Callable[[str, str, Path | None], list[str]]
17
+ run_upgrade: Callable[..., tuple[int, str]]
18
+ add_prefix_bin: Callable[[Path | None], None]
19
+ latest_version: Callable[[str, str], str]
20
+ version_newer: Callable[[str, str], bool]
21
+ output: Callable[..., None] = print
22
+
23
+
24
+ class NpmPackageLifecycle:
25
+ def __init__(self, ports: NpmPackageLifecyclePorts) -> None:
26
+ self._ports = ports
27
+
28
+ def install_if_missing(
29
+ self,
30
+ *,
31
+ executable_name: str,
32
+ label: str,
33
+ package_spec: str,
34
+ skip_env: str,
35
+ ) -> str | None:
36
+ executable = self._ports.find_executable(executable_name)
37
+ if executable:
38
+ return executable
39
+ if os.environ.get(skip_env) == "1":
40
+ return None
41
+ npm = self._ports.find_executable("npm")
42
+ if not npm:
43
+ self._print(
44
+ f"{label} executable was not found, and npm is not available to install {package_spec}."
45
+ )
46
+ return None
47
+ prefix = self._ports.install_prefix()
48
+ command = self._ports.install_command(npm, package_spec, prefix)
49
+ self._print(f"{label} executable was not found; installing {package_spec}...")
50
+ if prefix is not None:
51
+ self._print(f"Installing {label} into active npm prefix: {prefix}")
52
+ return_code, output = self._ports.run_upgrade(command, timeout=300)
53
+ if output:
54
+ self._print(output)
55
+ if return_code != 0:
56
+ self._print(f"{label} install failed ({return_code}).")
57
+ if prefix is not None:
58
+ self._print(
59
+ f"Install targeted the active install prefix ({prefix}). "
60
+ "If this prefix is not writable, install with the permissions used for that prefix."
61
+ )
62
+ return None
63
+ self._ports.add_prefix_bin(prefix)
64
+ executable = self._ports.find_executable(executable_name)
65
+ if executable:
66
+ self._print(f"{label} installed: {executable}")
67
+ else:
68
+ self._print(
69
+ f"{label} install completed, but the {executable_name} executable is still not visible in PATH."
70
+ )
71
+ return executable
72
+
73
+ def update_check(
74
+ self,
75
+ executable: str,
76
+ *,
77
+ executable_name: str,
78
+ label: str,
79
+ package_spec: str,
80
+ skip_env: str,
81
+ current_version: Callable[[str], str],
82
+ enabled: bool = True,
83
+ ) -> str:
84
+ if not enabled or os.environ.get(skip_env) == "1":
85
+ return executable
86
+ self._print(f"Checking {label} update before launch...")
87
+ current = current_version(executable)
88
+ if current:
89
+ self._print(f"Current {label} version: {current}")
90
+ npm = self._ports.find_executable("npm")
91
+ if not npm:
92
+ self._print(f"{label} update check skipped: npm was not found.")
93
+ return executable
94
+ latest = self._ports.latest_version(npm, package_spec)
95
+ if not latest:
96
+ self._print(f"{label} update check could not read the latest npm version; continuing.")
97
+ return executable
98
+ if current and not self._ports.version_newer(latest, current):
99
+ self._print(f"{label} is up to date ({current}).")
100
+ return executable
101
+ self._print(f"{label} update available: {current or 'unknown'} -> {latest}; upgrading automatically.")
102
+ prefix = self._ports.install_prefix()
103
+ if prefix is not None:
104
+ self._print(f"Updating {label} in active npm prefix: {prefix}")
105
+ return_code, output = self._ports.run_upgrade(
106
+ self._ports.install_command(npm, package_spec, prefix),
107
+ timeout=300,
108
+ )
109
+ if output:
110
+ self._print(output)
111
+ if return_code != 0:
112
+ self._print(f"{label} update failed ({return_code}); continuing with current version.")
113
+ return executable
114
+ self._ports.add_prefix_bin(prefix)
115
+ updated = self._ports.find_executable(executable_name) or executable
116
+ new_version = current_version(updated)
117
+ if new_version:
118
+ self._print(f"{label} version after update: {new_version}")
119
+ return updated
120
+
121
+ def _print(self, message: str) -> None:
122
+ self._ports.output(message, flush=True)
123
+
124
+
125
+ @dataclass(frozen=True, slots=True)
126
+ class SelfUpdatePorts:
127
+ running_from_package: Callable[[], bool]
128
+ find_executable: Callable[[str], str | None]
129
+ latest_version: Callable[[str, str], str]
130
+ version_newer: Callable[[str, str], bool]
131
+ package_root: Callable[[], Path | None]
132
+ prefix_from_root: Callable[[Path], Path | None]
133
+ install_command: Callable[[str, str, Path | None], list[str]]
134
+ forced_environment: Callable[[], dict[str, str]]
135
+ restart: Callable[..., None]
136
+ output: Callable[..., None] = print
137
+
138
+
139
+ class SelfUpdateLifecycle:
140
+ PACKAGE_SPEC = "@oneciel-ai/ciel-runtime@latest"
141
+
142
+ def __init__(self, current_version: str, ports: SelfUpdatePorts) -> None:
143
+ self._current_version = current_version
144
+ self._ports = ports
145
+
146
+ def run(self, enabled: bool = True) -> bool:
147
+ if not enabled or os.environ.get("CIEL_RUNTIME_SKIP_SELF_UPDATE") == "1":
148
+ return False
149
+ raw_check = os.environ.get("CIEL_RUNTIME_SELF_UPDATE_CHECK")
150
+ if raw_check is not None and raw_check.strip().lower() in {"0", "false", "no", "off"}:
151
+ return False
152
+ if not self._ports.running_from_package():
153
+ return False
154
+ npm = self._ports.find_executable("npm")
155
+ if not npm:
156
+ return False
157
+ latest = self._ports.latest_version(npm, self.PACKAGE_SPEC)
158
+ if not latest or not self._ports.version_newer(latest, self._current_version):
159
+ return False
160
+ self._print(
161
+ f"Ciel Runtime update available: {self._current_version} -> {latest}; upgrading automatically."
162
+ )
163
+ package_root = self._ports.package_root()
164
+ prefix = self._ports.prefix_from_root(package_root) if package_root else None
165
+ command = self._ports.install_command(npm, self.PACKAGE_SPEC, prefix)
166
+ if prefix is not None:
167
+ self._print(f"Updating current Ciel Runtime install prefix: {prefix}")
168
+ try:
169
+ update = subprocess.run(
170
+ command,
171
+ text=True,
172
+ input="y\n",
173
+ stdout=subprocess.PIPE,
174
+ stderr=subprocess.STDOUT,
175
+ env=self._ports.forced_environment(),
176
+ timeout=300,
177
+ )
178
+ except subprocess.TimeoutExpired:
179
+ self._print("Ciel Runtime update timed out; continuing with current version.")
180
+ return False
181
+ except (OSError, subprocess.SubprocessError) as exc:
182
+ self._print(f"Ciel Runtime update failed ({type(exc).__name__}); continuing.")
183
+ return False
184
+ output = (update.stdout or "").strip()
185
+ if output:
186
+ self._print(output)
187
+ if update.returncode != 0:
188
+ self._print(
189
+ f"Ciel Runtime update exited with {update.returncode}; continuing with current version."
190
+ )
191
+ if prefix is not None:
192
+ self._print(
193
+ f"Update targeted the active install prefix ({prefix}). "
194
+ "If this prefix is not writable, reinstall or update with the permissions used for that prefix."
195
+ )
196
+ return False
197
+ self._print("Ciel Runtime updated. Restarting with the new version...")
198
+ try:
199
+ self._ports.restart(npm, package_root=package_root)
200
+ except SystemExit:
201
+ raise
202
+ except (OSError, subprocess.SubprocessError) as exc:
203
+ self._print(
204
+ f"Restart failed ({type(exc).__name__}); continuing with the current process."
205
+ )
206
+ return True
207
+
208
+ def _print(self, message: str) -> None:
209
+ self._ports.output(message, flush=True)
210
+
211
+
212
+ __all__ = [
213
+ "NpmPackageLifecycle",
214
+ "NpmPackageLifecyclePorts",
215
+ "SelfUpdateLifecycle",
216
+ "SelfUpdatePorts",
217
+ ]
@@ -0,0 +1,104 @@
1
+ """HTTP controller and repository adapter for shared plan artifacts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ import urllib.parse
7
+ from dataclasses import dataclass
8
+ from http.server import BaseHTTPRequestHandler
9
+ from pathlib import Path
10
+ from typing import Any, Callable
11
+
12
+
13
+ @dataclass(frozen=True, slots=True)
14
+ class PlanArtifactServices:
15
+ directory: Path
16
+ router_base: str
17
+ safe_segment: Callable[[str, str], str]
18
+ write_json: Callable[..., None]
19
+ write_text: Callable[..., None]
20
+ announce: Callable[[dict[str, Any]], dict[str, Any]]
21
+
22
+
23
+ @dataclass(frozen=True, slots=True)
24
+ class PlanArtifactController:
25
+ services: PlanArtifactServices
26
+
27
+ def get(self, handler: BaseHTTPRequestHandler, path: str) -> bool:
28
+ if path == "/ca/plan/artifacts":
29
+ self.services.directory.mkdir(parents=True, exist_ok=True)
30
+ items = [
31
+ {
32
+ "name": item.name,
33
+ "bytes": item.stat().st_size,
34
+ "url": f"{self.services.router_base}/ca/plan/artifacts/{urllib.parse.quote(item.name)}",
35
+ }
36
+ for item in sorted(self.services.directory.glob("*"))
37
+ if item.is_file()
38
+ ]
39
+ self.services.write_json(handler, {"ok": True, "artifacts": items})
40
+ return True
41
+ if not path.startswith("/ca/plan/artifacts/"):
42
+ return False
43
+ name = self.services.safe_segment(
44
+ urllib.parse.unquote(path[len("/ca/plan/artifacts/") :]), "plan.md"
45
+ )
46
+ target = self.services.directory / name
47
+ if not target.exists() or not target.is_file():
48
+ self.services.write_json(handler, {"ok": False, "error": "not_found"}, 404)
49
+ return True
50
+ content_type = (
51
+ "text/markdown; charset=utf-8"
52
+ if target.suffix.lower() in (".md", ".markdown")
53
+ else "text/plain; charset=utf-8"
54
+ )
55
+ self.services.write_text(
56
+ handler,
57
+ target.read_text(encoding="utf-8", errors="replace"),
58
+ content_type=content_type,
59
+ )
60
+ return True
61
+
62
+ def post(
63
+ self,
64
+ handler: BaseHTTPRequestHandler,
65
+ path: str,
66
+ body: dict[str, Any],
67
+ ) -> bool:
68
+ if path != "/ca/plan/artifacts":
69
+ return False
70
+ self.services.directory.mkdir(parents=True, exist_ok=True)
71
+ title = str(body.get("title") or "plan")
72
+ content = str(body.get("content") or body.get("message") or "")
73
+ name = self.services.safe_segment(
74
+ str(body.get("name") or f"{int(time.time())}-{title}.md"), "plan.md"
75
+ )
76
+ if "." not in name:
77
+ name += ".md"
78
+ target = self.services.directory / name
79
+ target.write_text(content, encoding="utf-8")
80
+ latest = self.services.directory / "latest.md"
81
+ if target.name != latest.name:
82
+ latest.write_text(content, encoding="utf-8")
83
+ url = f"{self.services.router_base}/ca/plan/artifacts/{urllib.parse.quote(name)}"
84
+ if body.get("announce", True):
85
+ self.services.announce(
86
+ {
87
+ "channel": body.get("channel", "plan"),
88
+ "sender_id": body.get("sender_id", "plan"),
89
+ "recipients": body.get("recipients", "all"),
90
+ "kind": "plan",
91
+ "message": url,
92
+ "meta": {"title": title, "url": url, "name": name},
93
+ }
94
+ )
95
+ self.services.write_json(
96
+ handler,
97
+ {
98
+ "ok": True,
99
+ "name": name,
100
+ "url": url,
101
+ "latest_url": f"{self.services.router_base}/ca/plan/artifacts/latest.md",
102
+ },
103
+ )
104
+ return True