@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,195 @@
1
+ """Codex MCP discovery, capability filtering, and proxy configuration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from collections.abc import Callable, Iterable
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+
12
+ @dataclass(frozen=True, slots=True)
13
+ class CodexMcpConfigPorts:
14
+ discover: Callable[..., dict[str, dict[str, Any]]]
15
+ log: Callable[[str, str], None]
16
+
17
+
18
+ @dataclass(frozen=True, slots=True)
19
+ class CodexMcpArtifactPorts:
20
+ config_path: Callable[[], Path]
21
+ save_json: Callable[[Path, dict[str, Any], str], None]
22
+ unlink: Callable[[Path], None]
23
+ load_json: Callable[[Path], Any]
24
+
25
+
26
+ @dataclass(frozen=True, slots=True)
27
+ class CodexMcpCapabilityPorts:
28
+ ensure_probe_cache: Callable[..., Any]
29
+ read_servers: Callable[[Path, Path], list[dict[str, Any]]]
30
+ cached_probe_servers: Callable[[], list[dict[str, Any]]]
31
+ path_key: Callable[[Path], str]
32
+ cwd: Callable[[], Path]
33
+
34
+
35
+ @dataclass(frozen=True, slots=True)
36
+ class CodexMcpProjectionPorts:
37
+ dedupe_strings: Callable[[Iterable[str]], list[str]]
38
+ public_name: Callable[[str], str]
39
+ is_streamable_http: Callable[[dict[str, Any]], bool]
40
+ split_proxy_url: Callable[[str], str]
41
+ toml_string: Callable[[str], str]
42
+
43
+
44
+ @dataclass(frozen=True, slots=True)
45
+ class CodexMcpPolicy:
46
+ native_channel_names: frozenset[str]
47
+ builtin_channel_url: Callable[[], str]
48
+
49
+
50
+ @dataclass(frozen=True, slots=True)
51
+ class CodexMcpIntegrationService:
52
+ config: CodexMcpConfigPorts
53
+ artifact: CodexMcpArtifactPorts
54
+ capability: CodexMcpCapabilityPorts
55
+ projection: CodexMcpProjectionPorts
56
+ policy: CodexMcpPolicy
57
+
58
+ def discovered_servers(
59
+ self,
60
+ passthrough: list[str] | None = None,
61
+ env: dict[str, str] | None = None,
62
+ cwd: Path | None = None,
63
+ ) -> dict[str, dict[str, Any]]:
64
+ return self.config.discover(passthrough, env, cwd, log=self.config.log)
65
+
66
+ def write_discovery_config(
67
+ self,
68
+ passthrough: list[str] | None = None,
69
+ env: dict[str, str] | None = None,
70
+ cwd: Path | None = None,
71
+ ) -> Path | None:
72
+ servers = self.discovered_servers(passthrough or [], env=env, cwd=cwd)
73
+ path = self.artifact.config_path()
74
+ if not servers:
75
+ try:
76
+ self.artifact.unlink(path)
77
+ except FileNotFoundError:
78
+ pass
79
+ except Exception as exc:
80
+ self.config.log(
81
+ "WARN",
82
+ "codex_mcp_config_remove_failed "
83
+ f"error={type(exc).__name__}: {exc}",
84
+ )
85
+ return None
86
+ self.artifact.save_json(path, {"mcpServers": servers}, "codex_mcp_config")
87
+ self.config.log(
88
+ "INFO", f"codex_mcp_config_written servers={','.join(sorted(servers))}"
89
+ )
90
+ return path
91
+
92
+ @staticmethod
93
+ def config_bare_key(name: str) -> str | None:
94
+ text = str(name or "").strip()
95
+ return text if re.fullmatch(r"[A-Za-z0-9_-]+", text) else None
96
+
97
+ def channel_capable_server_names(
98
+ self, cfg: dict[str, Any], config_path: Path | None
99
+ ) -> list[str]:
100
+ if not config_path or not config_path.exists() or not config_path.is_file():
101
+ return []
102
+ self.capability.ensure_probe_cache(
103
+ cfg, [], extra_config_paths=[config_path]
104
+ )
105
+ candidate_names = [
106
+ str(server.get("channel") or "").strip()
107
+ for server in self.capability.read_servers(
108
+ config_path, self.capability.cwd()
109
+ )
110
+ if str(server.get("channel") or "").strip()
111
+ ]
112
+ source_key = self.capability.path_key(config_path)
113
+ capable = {
114
+ str(record.get("name") or "").strip()
115
+ for record in self.capability.cached_probe_servers()
116
+ if record.get("capable")
117
+ and str(record.get("name") or "").strip()
118
+ and self.capability.path_key(
119
+ Path(str(record.get("source_path") or ""))
120
+ )
121
+ == source_key
122
+ }
123
+ return self.projection.dedupe_strings(
124
+ name
125
+ for name in candidate_names
126
+ if name in capable and name.casefold() not in self.policy.native_channel_names
127
+ )
128
+
129
+ def streamable_http_servers(
130
+ self, config_path: Path | None
131
+ ) -> dict[str, dict[str, Any]]:
132
+ if not config_path or not config_path.exists() or not config_path.is_file():
133
+ return {}
134
+ try:
135
+ data = self.artifact.load_json(config_path)
136
+ except Exception as exc:
137
+ self.config.log(
138
+ "WARN",
139
+ "codex_mcp_compat_source_read_failed "
140
+ f"error={type(exc).__name__}: {exc}",
141
+ )
142
+ return {}
143
+ servers = data.get("mcpServers") if isinstance(data, dict) else None
144
+ if not isinstance(servers, dict):
145
+ return {}
146
+ return {
147
+ name: raw_server
148
+ for raw_name, raw_server in servers.items()
149
+ if (name := str(raw_name).strip())
150
+ and isinstance(raw_server, dict)
151
+ and self.projection.is_streamable_http(raw_server)
152
+ }
153
+
154
+ def native_http_compat_args(
155
+ self,
156
+ config_path: Path | None,
157
+ *,
158
+ split_http_proxy: bool = False,
159
+ channel_owned_server_names: Iterable[str] | None = None,
160
+ include_builtin_channel: bool = False,
161
+ ) -> list[str]:
162
+ servers = self.streamable_http_servers(config_path)
163
+ args: list[str] = []
164
+ active: list[str] = []
165
+ if include_builtin_channel:
166
+ key = "ciel-runtime-router"
167
+ url = self.projection.toml_string(self.policy.builtin_channel_url())
168
+ args.extend(["-c", f"mcp_servers.{key}.url={url}"])
169
+ active.append(key)
170
+ channel_owned = {
171
+ self.projection.public_name(str(name or "").strip())
172
+ for name in channel_owned_server_names or []
173
+ if str(name or "").strip()
174
+ }
175
+ for name in sorted(servers):
176
+ key = self.config_bare_key(name)
177
+ if not key:
178
+ self.config.log(
179
+ "WARN", f"codex_mcp_compat_skipped_unsafe_name server={name}"
180
+ )
181
+ continue
182
+ if split_http_proxy or self.projection.public_name(name) in channel_owned:
183
+ url = self.projection.toml_string(
184
+ self.projection.split_proxy_url(name)
185
+ )
186
+ args.extend(["-c", f"mcp_servers.{key}.url={url}"])
187
+ if name not in active:
188
+ active.append(name)
189
+ if active:
190
+ self.config.log(
191
+ "INFO",
192
+ "codex_mcp_native_http_compat servers=%s split_http_proxy=%s"
193
+ % (",".join(active), str(bool(split_http_proxy)).lower()),
194
+ )
195
+ return args
@@ -0,0 +1,304 @@
1
+ """Conservative restoration of missing native Codex MCP configuration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import math
7
+ import os
8
+ import time
9
+ from collections.abc import Callable
10
+ from dataclasses import dataclass
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ from .mcp_inventory import McpInventoryService
15
+
16
+
17
+ _SCALAR_KEYS = (
18
+ "command",
19
+ "cwd",
20
+ "url",
21
+ "bearer_token_env_var",
22
+ "enabled",
23
+ "experimental_environment",
24
+ "oauth_resource",
25
+ "required",
26
+ "startup_timeout_ms",
27
+ "startup_timeout_sec",
28
+ "tool_timeout_sec",
29
+ )
30
+ _LIST_KEYS = (
31
+ "args",
32
+ "env_vars",
33
+ "enabled_tools",
34
+ "disabled_tools",
35
+ "scopes",
36
+ )
37
+ _TABLE_KEYS = ("env", "http_headers", "env_http_headers")
38
+
39
+
40
+ @dataclass(frozen=True, slots=True)
41
+ class CodexMcpRestorePorts:
42
+ config_paths: Callable[..., list[Path]]
43
+ discover_managed: Callable[[Path], dict[str, dict[str, Any]]]
44
+ log: Callable[[str, str], None]
45
+
46
+
47
+ @dataclass(frozen=True, slots=True)
48
+ class CodexMcpRestoreService:
49
+ ports: CodexMcpRestorePorts
50
+
51
+ def restore(
52
+ self,
53
+ passthrough: list[str] | None = None,
54
+ env: dict[str, str] | None = None,
55
+ cwd: Path | None = None,
56
+ ) -> list[str]:
57
+ working_directory = (cwd or Path.cwd()).resolve()
58
+ paths = self.ports.config_paths(
59
+ passthrough or [], env=env, cwd=working_directory
60
+ )
61
+ if not paths:
62
+ return []
63
+ config_path = paths[0]
64
+ reserved: set[str] = set()
65
+ for path in paths[1:]:
66
+ if not path.is_file():
67
+ continue
68
+ try:
69
+ reserved.update(
70
+ self._existing_names(path.read_text(encoding="utf-8"))
71
+ )
72
+ except (OSError, UnicodeError, ValueError, TypeError) as exc:
73
+ self.ports.log(
74
+ "WARN",
75
+ "codex_mcp_restore_config_read_failed "
76
+ f"path={path} error={type(exc).__name__}: {exc}",
77
+ )
78
+ return []
79
+ managed = self.ports.discover_managed(working_directory)
80
+ return self._restore_missing(config_path, managed, reserved=reserved)
81
+
82
+ def _restore_missing(
83
+ self,
84
+ config_path: Path,
85
+ managed: dict[str, dict[str, Any]],
86
+ *,
87
+ reserved: set[str] | None = None,
88
+ ) -> list[str]:
89
+ try:
90
+ before = self._signature(config_path)
91
+ original = config_path.read_text(encoding="utf-8") if before else ""
92
+ existing = self._existing_names(original)
93
+ except (OSError, UnicodeError, ValueError, TypeError, RuntimeError) as exc:
94
+ self.ports.log(
95
+ "WARN",
96
+ "codex_mcp_restore_config_read_failed "
97
+ f"path={config_path} error={type(exc).__name__}: {exc}",
98
+ )
99
+ return []
100
+
101
+ projected: dict[str, dict[str, Any]] = {}
102
+ for raw_name, raw_server in managed.items():
103
+ name = str(raw_name or "").strip()
104
+ if not name or not isinstance(raw_server, dict):
105
+ continue
106
+ server = self._project_server(raw_server)
107
+ if server is None:
108
+ self.ports.log(
109
+ "WARN", f"codex_mcp_restore_skipped_unsupported server={name}"
110
+ )
111
+ continue
112
+ projected[name] = server
113
+ merged = McpInventoryService.merge(
114
+ {name: {} for name in existing}, projected, reserved=reserved
115
+ )
116
+ additions = [(name, projected[name]) for name in merged.added]
117
+
118
+ if not additions:
119
+ return []
120
+
121
+ suffix = "" if not original or original.endswith("\n") else "\n"
122
+ blocks = "\n".join(
123
+ self._server_toml(name, server) for name, server in additions
124
+ )
125
+ restored = original + suffix + ("\n" if original else "") + blocks
126
+ if not restored.endswith("\n"):
127
+ restored += "\n"
128
+
129
+ try:
130
+ self._validate(restored)
131
+ self._atomic_replace(config_path, restored, before)
132
+ except (OSError, UnicodeError, ValueError, TypeError) as exc:
133
+ self.ports.log(
134
+ "WARN",
135
+ "codex_mcp_restore_write_failed "
136
+ f"path={config_path} error={type(exc).__name__}: {exc}",
137
+ )
138
+ return []
139
+
140
+ names = [name for name, _server in additions]
141
+ self.ports.log(
142
+ "INFO",
143
+ "codex_mcp_config_restored "
144
+ f"path={config_path} servers={','.join(sorted(names))}",
145
+ )
146
+ return names
147
+
148
+ @staticmethod
149
+ def _signature(path: Path) -> tuple[int, int, int] | None:
150
+ try:
151
+ stat = path.stat()
152
+ except FileNotFoundError:
153
+ return None
154
+ return stat.st_mtime_ns, stat.st_size, getattr(stat, "st_ino", 0)
155
+
156
+ @staticmethod
157
+ def _validate(text: str) -> dict[str, Any]:
158
+ import tomllib
159
+
160
+ data = tomllib.loads(text)
161
+ if not isinstance(data, dict):
162
+ raise ValueError("Codex config must be a TOML table")
163
+ return data
164
+
165
+ @classmethod
166
+ def _existing_names(cls, text: str) -> set[str]:
167
+ if not text.strip():
168
+ return set()
169
+ data = cls._validate(text)
170
+ servers = data.get("mcp_servers")
171
+ if not isinstance(servers, dict):
172
+ return set()
173
+ return {str(name).strip() for name in servers if str(name).strip()}
174
+
175
+ @classmethod
176
+ def _project_server(cls, server: dict[str, Any]) -> dict[str, Any] | None:
177
+ projected: dict[str, Any] = {}
178
+ command = str(server.get("command") or "").strip()
179
+ url = str(server.get("url") or server.get("endpoint") or "").strip()
180
+ raw_args = server.get("args")
181
+ arguments = [str(item) for item in raw_args] if isinstance(raw_args, list) else []
182
+ if "mcp-proxy" in arguments and "--server-config" in arguments:
183
+ return None
184
+ transport = str(server.get("type") or server.get("transport") or "").strip().casefold()
185
+ if command:
186
+ projected["command"] = command
187
+ elif url.startswith(("http://", "https://")) and transport != "sse":
188
+ projected["url"] = url
189
+ else:
190
+ return None
191
+
192
+ aliases = {
193
+ "token_env_var": "bearer_token_env_var",
194
+ "headers": "http_headers",
195
+ }
196
+ normalized = dict(server)
197
+ for source, target in aliases.items():
198
+ if target not in normalized and source in normalized:
199
+ normalized[target] = normalized[source]
200
+ for key in _SCALAR_KEYS:
201
+ if key in {"command", "url"} or normalized.get(key) is None:
202
+ continue
203
+ value = normalized[key]
204
+ if isinstance(value, (str, bool, int)) or (
205
+ isinstance(value, float) and math.isfinite(value)
206
+ ):
207
+ projected[key] = value
208
+ for key in _LIST_KEYS:
209
+ value = normalized.get(key)
210
+ if isinstance(value, list) and all(
211
+ cls._supported_list_item(item) for item in value
212
+ ):
213
+ projected[key] = list(value)
214
+ for key in _TABLE_KEYS:
215
+ value = normalized.get(key)
216
+ if isinstance(value, dict):
217
+ projected[key] = {
218
+ str(item_key): str(item_value)
219
+ for item_key, item_value in value.items()
220
+ if str(item_key).strip()
221
+ }
222
+ return projected
223
+
224
+ @staticmethod
225
+ def _supported_list_item(value: Any) -> bool:
226
+ if isinstance(value, (str, bool, int)):
227
+ return True
228
+ if isinstance(value, float):
229
+ return math.isfinite(value)
230
+ if isinstance(value, dict):
231
+ return all(
232
+ str(key).strip()
233
+ and CodexMcpRestoreService._supported_list_item(item)
234
+ and not isinstance(item, (list, dict))
235
+ for key, item in value.items()
236
+ )
237
+ return False
238
+
239
+ @staticmethod
240
+ def _toml_value(value: Any) -> str:
241
+ if isinstance(value, bool):
242
+ return "true" if value else "false"
243
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
244
+ return repr(value)
245
+ if isinstance(value, dict):
246
+ entries = (
247
+ f"{json.dumps(str(key), ensure_ascii=False)} = {CodexMcpRestoreService._toml_value(item)}"
248
+ for key, item in value.items()
249
+ )
250
+ return "{ " + ", ".join(entries) + " }"
251
+ if isinstance(value, list):
252
+ return "[" + ", ".join(CodexMcpRestoreService._toml_value(item) for item in value) + "]"
253
+ return json.dumps(str(value), ensure_ascii=False)
254
+
255
+ @classmethod
256
+ def _server_toml(cls, name: str, server: dict[str, Any]) -> str:
257
+ table_name = json.dumps(name, ensure_ascii=False)
258
+ lines = [
259
+ "# Restored by Ciel Runtime from managed MCP state; existing Codex entries take precedence.",
260
+ f"[mcp_servers.{table_name}]",
261
+ ]
262
+ for key in _SCALAR_KEYS:
263
+ if key in server:
264
+ lines.append(f"{key} = {cls._toml_value(server[key])}")
265
+ for key in _LIST_KEYS:
266
+ if key in server:
267
+ lines.append(f"{key} = {cls._toml_value(server[key])}")
268
+ for key in _TABLE_KEYS:
269
+ values = server.get(key)
270
+ if not isinstance(values, dict) or not values:
271
+ continue
272
+ lines.extend(("", f"[mcp_servers.{table_name}.{key}]"))
273
+ for item_key, item_value in sorted(values.items()):
274
+ encoded_key = json.dumps(str(item_key), ensure_ascii=False)
275
+ lines.append(f"{encoded_key} = {cls._toml_value(item_value)}")
276
+ return "\n".join(lines) + "\n"
277
+
278
+ @classmethod
279
+ def _atomic_replace(
280
+ cls,
281
+ path: Path,
282
+ text: str,
283
+ expected: tuple[int, int, int] | None,
284
+ ) -> None:
285
+ path.parent.mkdir(parents=True, exist_ok=True)
286
+ if cls._signature(path) != expected:
287
+ raise RuntimeError("Codex config changed while MCP restoration was running")
288
+ temporary = path.with_name(
289
+ f"{path.name}.{os.getpid()}.{time.time_ns()}.tmp"
290
+ )
291
+ try:
292
+ temporary.write_text(text, encoding="utf-8")
293
+ os.chmod(temporary, 0o600)
294
+ if cls._signature(path) != expected:
295
+ raise RuntimeError("Codex config changed while MCP restoration was running")
296
+ temporary.replace(path)
297
+ finally:
298
+ try:
299
+ temporary.unlink()
300
+ except FileNotFoundError:
301
+ pass
302
+
303
+
304
+ __all__ = ["CodexMcpRestorePorts", "CodexMcpRestoreService"]
@@ -0,0 +1,133 @@
1
+ """Codex bundled-model catalog projection and atomic persistence."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from collections.abc import Callable
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+
12
+ @dataclass(frozen=True, slots=True)
13
+ class CodexModelCatalogSpec:
14
+ alias: str
15
+ provider_label: str
16
+ context_window: int
17
+ effort: str = ""
18
+ auto_compact_token_limit: int | None = None
19
+
20
+
21
+ class CodexModelCatalogService:
22
+ def __init__(
23
+ self,
24
+ config_dir: Path,
25
+ run: Callable[..., Any],
26
+ log: Callable[[str, str], None],
27
+ ) -> None:
28
+ self.config_dir = config_dir
29
+ self.run = run
30
+ self.log = log
31
+
32
+ def write(
33
+ self,
34
+ codex: str,
35
+ spec: CodexModelCatalogSpec,
36
+ environment: dict[str, str],
37
+ ) -> Path | None:
38
+ try:
39
+ result = self.run(
40
+ [codex, "debug", "models", "--bundled"],
41
+ capture_output=True,
42
+ text=True,
43
+ encoding="utf-8",
44
+ errors="replace",
45
+ timeout=15,
46
+ check=False,
47
+ env=environment,
48
+ )
49
+ if result.returncode != 0:
50
+ detail = result.stderr or result.stdout or f"exit {result.returncode}"
51
+ raise RuntimeError(detail.strip())
52
+ catalog = json.loads(result.stdout)
53
+ models = catalog.get("models") if isinstance(catalog, dict) else None
54
+ if not isinstance(models, list) or not models:
55
+ raise ValueError("bundled catalog contains no models")
56
+ template = next(
57
+ (
58
+ item
59
+ for item in models
60
+ if isinstance(item, dict) and item.get("slug") == "gpt-5.2"
61
+ ),
62
+ None,
63
+ )
64
+ if template is None:
65
+ template = next((item for item in models if isinstance(item, dict)), None)
66
+ if template is None:
67
+ raise ValueError("bundled catalog contains no model metadata")
68
+ routed = self._routed_model(template, spec)
69
+ catalog["models"] = [
70
+ item
71
+ for item in models
72
+ if not (isinstance(item, dict) and item.get("slug") == spec.alias)
73
+ ] + [routed]
74
+ return self._save(catalog)
75
+ except Exception as exc:
76
+ self.log(
77
+ "WARN",
78
+ f"codex_model_catalog_generation_failed "
79
+ f"error={type(exc).__name__}: {exc}",
80
+ )
81
+ return None
82
+
83
+ @staticmethod
84
+ def _routed_model(
85
+ template: dict[str, Any], spec: CodexModelCatalogSpec
86
+ ) -> dict[str, Any]:
87
+ routed = json.loads(json.dumps(template))
88
+ auto_compact_token_limit = (
89
+ spec.auto_compact_token_limit
90
+ if spec.auto_compact_token_limit is not None
91
+ else max(1, (spec.context_window * 9) // 10)
92
+ )
93
+ routed.update(
94
+ {
95
+ "slug": spec.alias,
96
+ "display_name": f"Ciel Runtime {spec.provider_label}",
97
+ "description": f"{spec.provider_label} routed through Ciel Runtime.",
98
+ "visibility": "none",
99
+ "supported_in_api": True,
100
+ "priority": 99,
101
+ "context_window": spec.context_window,
102
+ "max_context_window": spec.context_window,
103
+ "auto_compact_token_limit": min(
104
+ spec.context_window,
105
+ max(1, auto_compact_token_limit),
106
+ ),
107
+ }
108
+ )
109
+ if spec.effort:
110
+ routed["default_reasoning_level"] = spec.effort
111
+ supported = routed.get("supported_reasoning_levels")
112
+ if not isinstance(supported, list):
113
+ supported = []
114
+ if not any(
115
+ isinstance(item, dict) and item.get("effort") == spec.effort
116
+ for item in supported
117
+ ):
118
+ supported.append(
119
+ {
120
+ "effort": spec.effort,
121
+ "description": f"{spec.effort.title()} reasoning effort",
122
+ }
123
+ )
124
+ routed["supported_reasoning_levels"] = supported
125
+ return routed
126
+
127
+ def _save(self, catalog: dict[str, Any]) -> Path:
128
+ self.config_dir.mkdir(parents=True, exist_ok=True)
129
+ path = self.config_dir / "codex-model-catalog.json"
130
+ temporary = path.with_suffix(".json.tmp")
131
+ temporary.write_text(json.dumps(catalog, ensure_ascii=False), encoding="utf-8")
132
+ temporary.replace(path)
133
+ return path