@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,204 @@
1
+ """Launch command observability and subprocess stderr capture adapters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+ import sys
7
+ import threading
8
+ import time
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+ from typing import BinaryIO, Callable
12
+
13
+
14
+ Log = Callable[[str, str], None]
15
+
16
+
17
+ @dataclass(frozen=True, slots=True)
18
+ class LaunchCommandDiagnostics:
19
+ log: Log
20
+ mask_secret: Callable[[str], str]
21
+ codex_api_key_env: str
22
+
23
+ def agy(self, command: list[str], environment: dict[str, str]) -> None:
24
+ routed = "yes" if "--dangerously-skip-permissions" in command else "no"
25
+ self.log("INFO", f"agy_launch_cmd argv_len={len(command)} routed_flags={routed}")
26
+ summary = [
27
+ f"{key}={environment[key]}"
28
+ for key in ("CIEL_RUNTIME_PROVIDER", "CIEL_RUNTIME_MODEL_ALIAS")
29
+ if key in environment
30
+ ]
31
+ if summary:
32
+ self.log("INFO", "agy_launch_env " + " ".join(summary))
33
+
34
+ def claude(self, command: list[str], environment: dict[str, str]) -> None:
35
+ try:
36
+ mcp_index = command.index("--mcp-config") if "--mcp-config" in command else -1
37
+ mcp_value = command[mcp_index + 1] if 0 <= mcp_index < len(command) - 1 else "-"
38
+ except Exception:
39
+ mcp_value = "-"
40
+ channel_specs: list[str] = []
41
+ if "--dangerously-load-development-channels" in command:
42
+ start = command.index("--dangerously-load-development-channels") + 1
43
+ for argument in command[start:]:
44
+ if argument.startswith("--"):
45
+ break
46
+ channel_specs.append(argument)
47
+ self.log(
48
+ "INFO",
49
+ "claude_launch_cmd mcp_config=%s channels=%s argv_len=%d"
50
+ % (mcp_value, ",".join(channel_specs) or "-", len(command)),
51
+ )
52
+ keys = (
53
+ "ANTHROPIC_BASE_URL",
54
+ "ANTHROPIC_MODEL",
55
+ "ANTHROPIC_API_KEY",
56
+ "ANTHROPIC_AUTH_TOKEN",
57
+ "CIEL_RUNTIME_PROVIDER",
58
+ "CIEL_RUNTIME_MODEL_ALIAS",
59
+ "CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS",
60
+ "CLAUDE_CODE_EFFORT_LEVEL",
61
+ "ANTHROPIC_CUSTOM_MODEL_OPTION_SUPPORTED_CAPABILITIES",
62
+ "ANTHROPIC_DEFAULT_HAIKU_MODEL_SUPPORTS",
63
+ "ANTHROPIC_DEFAULT_OPUS_MODEL_SUPPORTS",
64
+ "ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTS",
65
+ )
66
+ summary = self._environment_summary(environment, keys)
67
+ if summary:
68
+ self.log("INFO", "claude_launch_env " + " ".join(summary))
69
+
70
+ def codex(self, command: list[str], environment: dict[str, str]) -> None:
71
+ provider_args = [
72
+ argument
73
+ for argument in command
74
+ if argument.startswith("model_provider=") or argument.startswith("model_providers.")
75
+ ]
76
+ self.log(
77
+ "INFO",
78
+ f"codex_launch_cmd argv_len={len(command)} provider_overrides={len(provider_args)}",
79
+ )
80
+ summary = self._environment_summary(
81
+ environment,
82
+ ("CIEL_RUNTIME_PROVIDER", "CIEL_RUNTIME_MODEL_ALIAS", self.codex_api_key_env),
83
+ )
84
+ if summary:
85
+ self.log("INFO", "codex_launch_env " + " ".join(summary))
86
+
87
+ def codex_app_server(
88
+ self, command: list[str], environment: dict[str, str]
89
+ ) -> None:
90
+ provider_args = [
91
+ argument
92
+ for argument in command
93
+ if argument.startswith("model_provider=")
94
+ or argument.startswith("model_providers.")
95
+ ]
96
+ listen = ""
97
+ for index, argument in enumerate(command):
98
+ if argument == "--listen" and index + 1 < len(command):
99
+ listen = str(command[index + 1])
100
+ break
101
+ if argument.startswith("--listen="):
102
+ listen = argument.split("=", 1)[1]
103
+ break
104
+ self.log(
105
+ "INFO",
106
+ "codex_app_server_launch_cmd argv_len=%d provider_overrides=%d listen=%s"
107
+ % (len(command), len(provider_args), listen or "stdio/default"),
108
+ )
109
+ summary = self._environment_summary(
110
+ environment,
111
+ (
112
+ "CIEL_RUNTIME_PROVIDER",
113
+ "CIEL_RUNTIME_MODEL_ALIAS",
114
+ self.codex_api_key_env,
115
+ ),
116
+ )
117
+ if summary:
118
+ self.log("INFO", "codex_app_server_launch_env " + " ".join(summary))
119
+
120
+ def _environment_summary(
121
+ self,
122
+ environment: dict[str, str],
123
+ keys: tuple[str, ...],
124
+ ) -> list[str]:
125
+ summary: list[str] = []
126
+ for key in keys:
127
+ if key not in environment:
128
+ continue
129
+ value = environment[key]
130
+ if "KEY" in key or "TOKEN" in key:
131
+ value = self.mask_secret(value)
132
+ summary.append(f"{key}={value}")
133
+ return summary
134
+
135
+
136
+ @dataclass(frozen=True, slots=True)
137
+ class StderrCaptureAdapter:
138
+ config_dir: Path
139
+ stderr_log: Path
140
+ log: Log
141
+ rotation_bytes: int = 2_000_000
142
+
143
+ def call(self, command: list[str], environment: dict[str, str]) -> int:
144
+ self.config_dir.mkdir(parents=True, exist_ok=True)
145
+ self._rotate()
146
+ try:
147
+ log_handle = self.stderr_log.open("ab", buffering=0)
148
+ except Exception as error:
149
+ self.log("WARN", f"claude_stderr_capture_open_failed error={type(error).__name__}: {error}")
150
+ return subprocess.call(command, env=environment)
151
+ self._write_header(log_handle)
152
+ try:
153
+ process = subprocess.Popen(command, env=environment, stderr=subprocess.PIPE)
154
+ except Exception as error:
155
+ self.log("WARN", f"claude_stderr_capture_spawn_failed error={type(error).__name__}: {error}")
156
+ self._close(log_handle)
157
+ return subprocess.call(command, env=environment)
158
+
159
+ def tee_stderr() -> None:
160
+ try:
161
+ if process.stderr is None:
162
+ return
163
+ while chunk := process.stderr.read(4096):
164
+ self._write_console(chunk)
165
+ try:
166
+ log_handle.write(chunk)
167
+ except Exception as error:
168
+ self.log("WARN", f"claude_stderr_log_tee_failed error={type(error).__name__}: {error}")
169
+ finally:
170
+ self._close(log_handle)
171
+
172
+ thread = threading.Thread(target=tee_stderr, daemon=True, name="claude-stderr-tee")
173
+ thread.start()
174
+ return_code = process.wait()
175
+ thread.join(timeout=2.0)
176
+ self.log("INFO", f"claude_exit code={return_code} stderr_log={self.stderr_log}")
177
+ return return_code
178
+
179
+ def _rotate(self) -> None:
180
+ try:
181
+ if self.stderr_log.exists() and self.stderr_log.stat().st_size > self.rotation_bytes:
182
+ self.stderr_log.replace(self.stderr_log.with_suffix(".log.1"))
183
+ except Exception as error:
184
+ self.log("WARN", f"claude_stderr_capture_rotate_failed error={type(error).__name__}: {error}")
185
+
186
+ def _write_header(self, log_handle: BinaryIO) -> None:
187
+ header = f"\n===== claude launch at {time.strftime('%Y-%m-%dT%H:%M:%S')} =====\n".encode()
188
+ try:
189
+ log_handle.write(header)
190
+ except Exception as error:
191
+ self.log("WARN", f"claude_stderr_capture_header_failed error={type(error).__name__}: {error}")
192
+
193
+ def _write_console(self, chunk: bytes) -> None:
194
+ try:
195
+ sys.stderr.buffer.write(chunk)
196
+ sys.stderr.buffer.flush()
197
+ except Exception as error:
198
+ self.log("WARN", f"claude_stderr_console_tee_failed error={type(error).__name__}: {error}")
199
+
200
+ def _close(self, log_handle: BinaryIO) -> None:
201
+ try:
202
+ log_handle.close()
203
+ except Exception as error:
204
+ self.log("WARN", f"claude_stderr_capture_close_failed error={type(error).__name__}: {error}")
@@ -0,0 +1,127 @@
1
+ """Runtime launch history repository and session-switch policy."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ from typing import Any, Callable
9
+
10
+
11
+ @dataclass(frozen=True, slots=True)
12
+ class LaunchStateRepository:
13
+ path: Path
14
+ config_dir: Path
15
+ log: Callable[[str, str], None]
16
+ process_id: Callable[[], int]
17
+ clock: Callable[[], float]
18
+ clock_ns: Callable[[], int]
19
+
20
+ def read(self) -> dict[str, Any]:
21
+ try:
22
+ data = json.loads(self.path.read_text(encoding="utf-8"))
23
+ except Exception:
24
+ return {}
25
+ return data if isinstance(data, dict) else {}
26
+
27
+ def write(self, state: dict[str, Any]) -> None:
28
+ try:
29
+ self.config_dir.mkdir(parents=True, exist_ok=True)
30
+ tmp = self.path.with_name(
31
+ f"{self.path.name}.{self.process_id()}.{self.clock_ns()}.tmp"
32
+ )
33
+ tmp.write_text(json.dumps(state, indent=2, sort_keys=True) + "\n", encoding="utf-8")
34
+ tmp.chmod(0o600)
35
+ tmp.replace(self.path)
36
+ except Exception as exc:
37
+ self.log("WARN", f"launch_state_write_failed error={type(exc).__name__}: {exc}")
38
+
39
+ def previous_for_cwd(self, cwd_key: str) -> dict[str, Any]:
40
+ state = self.read()
41
+ by_cwd = state.get("by_cwd")
42
+ if isinstance(by_cwd, dict) and isinstance(item := by_cwd.get(cwd_key), dict):
43
+ return item
44
+ legacy = state.get("last")
45
+ if isinstance(legacy, dict) and str(legacy.get("cwd") or "") == cwd_key:
46
+ return legacy
47
+ return {}
48
+
49
+ def record(self, cwd_key: str, provider: str, mode: str, model: str) -> None:
50
+ state = self.read()
51
+ by_cwd = state.get("by_cwd")
52
+ if not isinstance(by_cwd, dict):
53
+ by_cwd = {}
54
+ item = {
55
+ "cwd": cwd_key,
56
+ "provider": provider,
57
+ "mode": mode,
58
+ "model": model,
59
+ "pid": self.process_id(),
60
+ "time": self.clock(),
61
+ }
62
+ by_cwd[cwd_key] = item
63
+ state["by_cwd"] = by_cwd
64
+ state["last"] = item
65
+ self.write(state)
66
+
67
+
68
+ def current_launch_cwd_key() -> str:
69
+ try:
70
+ return str(Path.cwd().resolve())
71
+ except Exception:
72
+ return str(Path.cwd())
73
+
74
+
75
+ def launch_mode_name(
76
+ provider: str,
77
+ *,
78
+ use_native_anthropic: bool,
79
+ anthropic_routed: bool,
80
+ ) -> str:
81
+ if use_native_anthropic:
82
+ return "anthropic-native"
83
+ if anthropic_routed:
84
+ return "anthropic-routed"
85
+ return f"router:{provider}"
86
+
87
+
88
+ def last_launch_runtime(repository: LaunchStateRepository, cwd_key: str) -> str:
89
+ state = repository.read()
90
+ item = repository.previous_for_cwd(cwd_key)
91
+ if not item:
92
+ candidate = state.get("last")
93
+ item = candidate if isinstance(candidate, dict) else {}
94
+ mode = str(item.get("mode") or "").strip().lower()
95
+ if mode.startswith("codex"):
96
+ return "codex"
97
+ if mode.startswith("agy"):
98
+ return "agy"
99
+ if mode.startswith("anthropic") or mode.startswith("router:"):
100
+ return "claude"
101
+ return ""
102
+
103
+
104
+ def session_control_requested(passthrough: list[str]) -> bool:
105
+ names = ("-c", "--continue", "-r", "--resume", "--session-id", "--fork-session", "--from-pr")
106
+ return any(
107
+ argument in names or any(argument.startswith(name + "=") for name in names)
108
+ for argument in passthrough
109
+ )
110
+
111
+
112
+ def should_fork_native_session(
113
+ *,
114
+ current_mode: str,
115
+ passthrough: list[str],
116
+ cwd_key: str,
117
+ use_native_anthropic: bool,
118
+ repository: LaunchStateRepository,
119
+ ) -> tuple[bool, str]:
120
+ if not use_native_anthropic:
121
+ return False, ""
122
+ if session_control_requested(passthrough):
123
+ return False, "explicit_session_control"
124
+ previous_mode = str(repository.previous_for_cwd(cwd_key).get("mode") or "")
125
+ if not previous_mode or previous_mode == current_mode:
126
+ return False, previous_mode or "no_previous_mode"
127
+ return True, previous_mode
@@ -0,0 +1,58 @@
1
+ """Live API-key status and mutation command controller."""
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 LiveApiKeyPorts:
12
+ load_config: Callable[[], dict[str, Any]]
13
+ current_provider: Callable[[dict[str, Any]], tuple[str, dict[str, Any]]]
14
+ status_line: Callable[[str, dict[str, Any]], str]
15
+ stored_mask: Callable[[str, dict[str, Any]], str]
16
+ store_input: Callable[[str, str], list[str]]
17
+
18
+
19
+ class LiveApiKeyController:
20
+ def __init__(self, ports: LiveApiKeyPorts) -> None:
21
+ self.ports = ports
22
+
23
+ def status(self, provider: str, provider_config: dict[str, Any]) -> list[str]:
24
+ return [
25
+ f"Live API key status for provider: {provider}",
26
+ self.ports.status_line(provider, provider_config),
27
+ f"Stored: {self.ports.stored_mask(provider, provider_config)}",
28
+ ]
29
+
30
+ def handle(self, value: str) -> tuple[list[str], bool]:
31
+ config = self.ports.load_config()
32
+ provider, provider_config = self.ports.current_provider(config)
33
+ raw = str(value or "").strip()
34
+ normalized = raw.lower()
35
+ if normalized in {"", "status", "state", "show", "current", "now"}:
36
+ return self.status(provider, provider_config), False
37
+ if normalized in {"help", "usage", "list"}:
38
+ return [
39
+ "Use `/api-key status` to show masked key status.",
40
+ "Use `/api-key clear` or `/api-key unset` to remove API keys for only the current provider.",
41
+ "Use `/api-key KEY` to set one key.",
42
+ "Use `/api-key KEY1,KEY2` or `/api-keys KEY1;KEY2` to set multiple round-robin keys.",
43
+ "Raw keys are never echoed; responses show only masked keys and fingerprints.",
44
+ ], False
45
+ try:
46
+ lines = self.ports.store_input(provider, raw)
47
+ except SystemExit as exc:
48
+ message = str(exc).strip() or "No API keys provided; unchanged."
49
+ return [message, "", *self.status(provider, provider_config)], False
50
+ config_after = self.ports.load_config()
51
+ provider_after, provider_config_after = self.ports.current_provider(
52
+ config_after
53
+ )
54
+ return lines + [
55
+ "",
56
+ "Updated live API key settings. The next model request uses these settings.",
57
+ *self.status(provider_after, provider_config_after),
58
+ ], True
@@ -0,0 +1,148 @@
1
+ """HTTP controller and presentation projection for LLM configuration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any, Callable
7
+
8
+
9
+ @dataclass(frozen=True, slots=True)
10
+ class LlmConfigIdentity:
11
+ load_config: Callable[..., dict[str, Any]]
12
+ current_provider: Callable[..., tuple[str, dict[str, Any]]]
13
+ current_alias: Callable[..., str]
14
+ applied_preset: Callable[..., str]
15
+ context_status: Callable[..., str]
16
+ timeout_status: Callable[..., str]
17
+ provider_labels: dict[str, str]
18
+
19
+
20
+ @dataclass(frozen=True, slots=True)
21
+ class LlmConfigPanels:
22
+ option_rows: Callable[..., tuple[list[str], list[str]]]
23
+ option_default: Callable[..., str]
24
+ preset_rows: Callable[..., tuple[list[str], list[str]]]
25
+ context_rows: Callable[..., tuple[list[str], list[str]]]
26
+ timeout_rows: Callable[..., tuple[list[str], list[str]]]
27
+
28
+
29
+ @dataclass(frozen=True, slots=True)
30
+ class LlmConfigMutations:
31
+ set_model: Callable[..., list[str]]
32
+ set_advisor_model: Callable[..., list[str]]
33
+ apply_preset: Callable[..., list[str]]
34
+ apply_context: Callable[..., list[str]]
35
+ apply_timeout: Callable[..., list[str]]
36
+ set_option: Callable[..., list[str]]
37
+
38
+
39
+ @dataclass(frozen=True, slots=True)
40
+ class LlmConfigHttpIO:
41
+ publish_event: Callable[..., Any]
42
+ write_json: Callable[..., Any]
43
+ log: Callable[[str, str], None]
44
+
45
+
46
+ @dataclass(frozen=True, slots=True)
47
+ class LlmConfigHttpController:
48
+ identity: LlmConfigIdentity
49
+ panels: LlmConfigPanels
50
+ mutations: LlmConfigMutations
51
+ io: LlmConfigHttpIO
52
+
53
+ PATH = "/ca/config/llm"
54
+
55
+ def payload(self, messages: list[str] | None = None) -> dict[str, Any]:
56
+ cfg = self.identity.load_config()
57
+ provider, provider_config = self.identity.current_provider(cfg)
58
+ language = cfg.get("language", "en")
59
+ rows, values = self.panels.option_rows(provider, provider_config, language)
60
+ preset_rows, preset_values = self.panels.preset_rows(provider, provider_config, language)
61
+ context_rows, context_values = self.panels.context_rows(provider, provider_config, language)
62
+ timeout_rows, timeout_values = self.panels.timeout_rows(provider_config, language)
63
+ return {
64
+ "ok": True,
65
+ "messages": messages or [],
66
+ "provider": provider,
67
+ "provider_label": self.identity.provider_labels.get(provider, provider),
68
+ "model": str(provider_config.get("current_model") or ""),
69
+ "alias": self.identity.current_alias(cfg),
70
+ "advisor_model": str(provider_config.get("advisor_model") or ""),
71
+ "preset": self.identity.applied_preset(provider, provider_config),
72
+ "context": self.identity.context_status(provider, provider_config),
73
+ "timeout": self.identity.timeout_status(provider_config, language),
74
+ "options": [
75
+ {"label": row, "key": key, "value": self.panels.option_default(provider, provider_config, key)}
76
+ for row, key in zip(rows, values)
77
+ if key not in ("back", "__info__", "preset", "context_setup", "timeout_profile")
78
+ ],
79
+ "presets": self._selection_rows(preset_rows, preset_values),
80
+ "contexts": self._selection_rows(context_rows, context_values),
81
+ "timeouts": self._selection_rows(timeout_rows, timeout_values),
82
+ }
83
+
84
+ @staticmethod
85
+ def _selection_rows(rows: list[str], values: list[str]) -> list[dict[str, str]]:
86
+ return [
87
+ {"label": row, "value": value}
88
+ for row, value in zip(rows, values)
89
+ if value not in ("back", "__info__")
90
+ ]
91
+
92
+ def handle_get(self, handler: Any, path: str) -> bool:
93
+ if path != self.PATH:
94
+ return False
95
+ self.io.write_json(handler, self.payload())
96
+ return True
97
+
98
+ def handle_post(self, handler: Any, path: str, body: dict[str, Any]) -> bool:
99
+ if path != self.PATH:
100
+ return False
101
+ cfg = self.identity.load_config()
102
+ provider, _provider_config = self.identity.current_provider(cfg)
103
+ action = str(body.get("action") or "option").strip()
104
+ value = str(body.get("value") or "").strip()
105
+ key = str(body.get("key") or "").strip()
106
+ try:
107
+ messages = self._mutate(action, provider, key, value)
108
+ self.io.publish_event(
109
+ level="info",
110
+ category="config.llm",
111
+ message=f"updated {action} {key or value}",
112
+ provider=provider,
113
+ data={"action": action, "key": key, "value": value},
114
+ )
115
+ self.io.write_json(handler, self.payload(messages))
116
+ except SystemExit as exc:
117
+ self.io.write_json(handler, {"ok": False, "error": str(exc), "messages": [str(exc)]}, 400)
118
+ except Exception as exc:
119
+ error = f"{type(exc).__name__}: {exc}"
120
+ self.io.log("ERROR", f"llm config update failed: {error}")
121
+ self.io.write_json(handler, {"ok": False, "error": error, "messages": [error]}, 500)
122
+ return True
123
+
124
+ def _mutate(self, action: str, provider: str, key: str, value: str) -> list[str]:
125
+ if action == "model":
126
+ return self.mutations.set_model(value)
127
+ if action == "advisor_model":
128
+ return self.mutations.set_advisor_model(value)
129
+ if action == "preset":
130
+ return self.mutations.apply_preset(provider, value)
131
+ if action == "context_setup":
132
+ return self.mutations.apply_context(provider, value)
133
+ if action == "timeout_profile":
134
+ return self.mutations.apply_timeout(provider, value)
135
+ if action == "option":
136
+ if not key:
137
+ raise SystemExit("Missing option key")
138
+ return self.mutations.set_option(provider, key, value)
139
+ raise SystemExit(f"Unknown LLM config action: {action}")
140
+
141
+
142
+ __all__ = [
143
+ "LlmConfigHttpController",
144
+ "LlmConfigHttpIO",
145
+ "LlmConfigIdentity",
146
+ "LlmConfigMutations",
147
+ "LlmConfigPanels",
148
+ ]