@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,130 @@
1
+ """Stateful removal of provider-visible thinking and tool-call artifacts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import Any
7
+
8
+
9
+ VISIBLE_THINKING_MARKUP_TAG_RE = re.compile(r"</?think(?:ing)?\b[^>]*>", re.I)
10
+ VISIBLE_THINKING_MARKUP_PREFIXES = ("<think", "</think", "<thinking", "</thinking")
11
+ VISIBLE_TOOL_CALL_ARTIFACT_SUFFIX_RE = re.compile(
12
+ r"(?s)(?:^|(?:\r?\n){1,3})[ \t]*call[ \t]*(?:\r?\n)"
13
+ r"[ \t]*ignore[ \t]*(?:\r?\n)?[ \t]*$"
14
+ )
15
+ VISIBLE_TOOL_CALL_ARTIFACT_HOLD_CHARS = 96
16
+
17
+
18
+ def visible_thinking_markup_partial_start(text: str) -> int:
19
+ less_than = text.rfind("<")
20
+ if less_than < 0 or less_than <= text.rfind(">"):
21
+ return -1
22
+ suffix = text[less_than:].lower()
23
+ if any(
24
+ prefix.startswith(suffix) or suffix.startswith(prefix)
25
+ for prefix in VISIBLE_THINKING_MARKUP_PREFIXES
26
+ ):
27
+ return less_than
28
+ return -1
29
+
30
+
31
+ class VisibleThinkingMarkupFilter:
32
+ def __init__(self) -> None:
33
+ self.in_thinking = False
34
+ self.pending = ""
35
+
36
+ def feed(self, text: Any) -> str:
37
+ raw = str(text or "")
38
+ if not raw:
39
+ return ""
40
+ value = self.pending + raw
41
+ self.pending = ""
42
+ partial_start = visible_thinking_markup_partial_start(value)
43
+ if partial_start >= 0:
44
+ self.pending = value[partial_start:]
45
+ value = value[:partial_start]
46
+ return self._strip_complete_tags(value)
47
+
48
+ def finish(self) -> str:
49
+ pending = self.pending
50
+ self.pending = ""
51
+ if (
52
+ not pending
53
+ or self.in_thinking
54
+ or visible_thinking_markup_partial_start(pending) == 0
55
+ ):
56
+ self.in_thinking = False
57
+ return ""
58
+ return self._strip_complete_tags(pending)
59
+
60
+ def _strip_complete_tags(self, text: str) -> str:
61
+ output: list[str] = []
62
+ position = 0
63
+ for match in VISIBLE_THINKING_MARKUP_TAG_RE.finditer(text):
64
+ closing = match.group(0).lstrip().lower().startswith("</")
65
+ if self.in_thinking:
66
+ if closing:
67
+ self.in_thinking = False
68
+ position = match.end()
69
+ continue
70
+ output.append(text[position : match.start()])
71
+ position = match.end()
72
+ if not closing:
73
+ self.in_thinking = True
74
+ if not self.in_thinking:
75
+ output.append(text[position:])
76
+ return "".join(output)
77
+
78
+
79
+ def strip_visible_thinking_markup(text: Any) -> str:
80
+ raw = str(text or "")
81
+ if "<think" not in raw.lower() and "</think" not in raw.lower():
82
+ return raw
83
+ filter_state = VisibleThinkingMarkupFilter()
84
+ return (filter_state.feed(raw) + filter_state.finish()).lstrip()
85
+
86
+
87
+ def strip_visible_tool_call_artifact_suffix(text: Any) -> str:
88
+ raw = str(text or "")
89
+ if "call" not in raw or "ignore" not in raw:
90
+ return raw
91
+ return VISIBLE_TOOL_CALL_ARTIFACT_SUFFIX_RE.sub("", raw)
92
+
93
+
94
+ class VisibleToolCallArtifactFilter:
95
+ def __init__(self, hold_chars: int = VISIBLE_TOOL_CALL_ARTIFACT_HOLD_CHARS) -> None:
96
+ self.hold_chars = max(16, int(hold_chars))
97
+ self.pending = ""
98
+ self.stripped = False
99
+
100
+ def feed(self, text: Any) -> str:
101
+ raw = str(text or "")
102
+ if not raw:
103
+ return ""
104
+ value = self.pending + raw
105
+ if len(value) <= self.hold_chars:
106
+ self.pending = value
107
+ return ""
108
+ emit_length = len(value) - self.hold_chars
109
+ self.pending = value[emit_length:]
110
+ return value[:emit_length]
111
+
112
+ def finish(self) -> str:
113
+ pending = self.pending
114
+ self.pending = ""
115
+ stripped = strip_visible_tool_call_artifact_suffix(pending)
116
+ self.stripped = self.stripped or stripped != pending
117
+ return stripped
118
+
119
+
120
+ __all__ = [
121
+ "VISIBLE_THINKING_MARKUP_PREFIXES",
122
+ "VISIBLE_THINKING_MARKUP_TAG_RE",
123
+ "VISIBLE_TOOL_CALL_ARTIFACT_HOLD_CHARS",
124
+ "VISIBLE_TOOL_CALL_ARTIFACT_SUFFIX_RE",
125
+ "VisibleThinkingMarkupFilter",
126
+ "VisibleToolCallArtifactFilter",
127
+ "strip_visible_thinking_markup",
128
+ "strip_visible_tool_call_artifact_suffix",
129
+ "visible_thinking_markup_partial_start",
130
+ ]
@@ -0,0 +1,447 @@
1
+ """Startup web endpoint options and optional Tailscale Serve integration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ipaddress
6
+ import json
7
+ import os
8
+ import shutil
9
+ import subprocess
10
+ import urllib.parse
11
+ from collections.abc import MutableMapping, Sequence
12
+ from dataclasses import dataclass
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+
17
+ _TRUE = {"1", "true", "yes", "on"}
18
+ _WILDCARD_HOSTS = {"0.0.0.0", "::", "[::]"}
19
+
20
+
21
+ @dataclass(frozen=True, slots=True)
22
+ class TailscaleNode:
23
+ ipv4: str
24
+ dns_name: str
25
+ online: bool = True
26
+
27
+
28
+ @dataclass(frozen=True, slots=True)
29
+ class WebEndpointReport:
30
+ local_url: str
31
+ tailscale_ip_url: str = ""
32
+ tailscale_https_url: str = ""
33
+ tailscale_https_command: str = ""
34
+
35
+ def status_lines(self) -> list[str]:
36
+ lines = [f"web: {self.local_url}"]
37
+ if self.tailscale_ip_url:
38
+ lines.append(f"web_tailscale_http: {self.tailscale_ip_url}")
39
+ if self.tailscale_https_url:
40
+ lines.append(f"web_tailscale_https: {self.tailscale_https_url}")
41
+ elif self.tailscale_https_command:
42
+ lines.append(f"web_tailscale_https_recommended: {self.tailscale_https_command}")
43
+ return lines
44
+
45
+
46
+ @dataclass(frozen=True, slots=True)
47
+ class WebBackendSettings:
48
+ enabled: bool = False
49
+ host: str = "127.0.0.1"
50
+ port: int = 0
51
+ tailscale_https: bool = False
52
+
53
+ @property
54
+ def client_host(self) -> str:
55
+ return "127.0.0.1" if self.host in _WILDCARD_HOSTS else self.host
56
+
57
+
58
+ def web_backend_settings(config: dict[str, Any] | None) -> WebBackendSettings:
59
+ raw = config.get("web_backend") if isinstance(config, dict) else {}
60
+ values = raw if isinstance(raw, dict) else {}
61
+ host = str(values.get("host") or "127.0.0.1").strip()
62
+ if not host or any(character.isspace() for character in host):
63
+ host = "127.0.0.1"
64
+ try:
65
+ port = int(values.get("port") or 0)
66
+ except (TypeError, ValueError):
67
+ port = 0
68
+ if not 1 <= port <= 65535:
69
+ port = 0
70
+ tailscale_https = bool(values.get("tailscale_https", False))
71
+ enabled_value = values.get("enabled")
72
+ enabled = (
73
+ bool(enabled_value)
74
+ if enabled_value is not None
75
+ else bool(tailscale_https or host != "127.0.0.1" or port)
76
+ )
77
+ return WebBackendSettings(enabled, host, port, tailscale_https)
78
+
79
+
80
+ def load_saved_web_backend(path: os.PathLike[str] | str) -> WebBackendSettings:
81
+ try:
82
+ payload = json.loads(Path(path).read_text(encoding="utf-8"))
83
+ except (OSError, ValueError, TypeError):
84
+ payload = {}
85
+ return web_backend_settings(payload if isinstance(payload, dict) else {})
86
+
87
+
88
+ def web_backend_summary(config: dict[str, Any], effective_port: int) -> str:
89
+ settings = web_backend_settings(config)
90
+ port = settings.port or effective_port or int(config.get("_effective_web_port") or 0)
91
+ mode = " + Tailscale HTTPS" if settings.tailscale_https else ""
92
+ state = "on" if settings.enabled else "off"
93
+ return f"{state} · {settings.host}:{port or 'auto'}{mode}"
94
+
95
+
96
+ def web_backend_panel_rows(
97
+ config: dict[str, Any], effective_port: int
98
+ ) -> tuple[list[str], list[str]]:
99
+ settings = web_backend_settings(config)
100
+ port = settings.port or effective_port
101
+ node = discover_tailscale_node()
102
+ tailscale = "on" if settings.tailscale_https else "off"
103
+ if node and node.dns_name:
104
+ tailscale += f" · {node.dns_name}:{port}"
105
+ else:
106
+ tailscale += " · not detected"
107
+ return (
108
+ [
109
+ f"Web backend [{'on' if settings.enabled else 'off'}]",
110
+ f"Web bind address [{settings.host}]",
111
+ f"Web port [{port}]",
112
+ f"Tailscale HTTPS [{tailscale}]",
113
+ "Back",
114
+ ],
115
+ ["enabled", "host", "port", "tailscale", "back"],
116
+ )
117
+
118
+
119
+ def update_web_backend_config(
120
+ config: dict[str, Any], key: str, value: Any, effective_port: int
121
+ ) -> list[str]:
122
+ current = web_backend_settings(config)
123
+ enabled = current.enabled
124
+ host = current.host
125
+ port = current.port
126
+ tailscale_https = current.tailscale_https
127
+ if key == "enabled":
128
+ enabled = bool(value)
129
+ elif key == "host":
130
+ host, embedded_port = _normalize_web_address(str(value))
131
+ enabled = True
132
+ if embedded_port is not None:
133
+ port = embedded_port
134
+ elif key == "port":
135
+ port = _valid_port(str(value), "web port")
136
+ enabled = True
137
+ elif key == "tailscale":
138
+ tailscale_https = bool(value)
139
+ enabled = True
140
+ else:
141
+ raise ValueError(f"unknown web backend setting: {key}")
142
+ config["web_backend"] = {
143
+ "enabled": enabled,
144
+ "host": host,
145
+ "port": port or effective_port,
146
+ "tailscale_https": tailscale_https,
147
+ }
148
+ external = host in _WILDCARD_HOSTS or not _is_loopback(host)
149
+ config["router_debug_external_access"] = external
150
+ config["router_debug_external_access_confirmed"] = external
151
+ scheme = "https" if tailscale_https else "http"
152
+ return [
153
+ f"Web backend: {'on' if enabled else 'off'} · {host}:{port or effective_port}.",
154
+ f"Tailscale HTTPS: {'on' if tailscale_https else 'off'}.",
155
+ f"Runtime is restarting so {scheme} endpoint settings apply now.",
156
+ ]
157
+
158
+
159
+ def apply_startup_web_options(
160
+ argv: list[str],
161
+ environ: MutableMapping[str, str],
162
+ ) -> list[str]:
163
+ """Consume Ciel-owned web flags before runtime path constants are imported."""
164
+ output = [argv[0]] if argv else []
165
+ index = 1
166
+ passthrough = False
167
+ while index < len(argv):
168
+ argument = argv[index]
169
+ if passthrough:
170
+ output.append(argument)
171
+ index += 1
172
+ continue
173
+ if argument == "--":
174
+ passthrough = True
175
+ output.append(argument)
176
+ index += 1
177
+ continue
178
+ name, separator, inline_value = argument.partition("=")
179
+ if name in {"--ca-web-address", "--ca-web-host"}:
180
+ value, index = _option_value(argv, index, inline_value if separator else "", name)
181
+ host, embedded_port = _normalize_web_address(value)
182
+ _apply_web_host(host, environ)
183
+ environ["CIEL_RUNTIME_WEB_START_REQUESTED"] = "1"
184
+ if embedded_port is not None:
185
+ environ["CIEL_RUNTIME_ROUTER_PORT"] = str(embedded_port)
186
+ continue
187
+ if name == "--ca-web-port":
188
+ value, index = _option_value(argv, index, inline_value if separator else "", name)
189
+ environ["CIEL_RUNTIME_ROUTER_PORT"] = str(_valid_port(value, name))
190
+ environ["CIEL_RUNTIME_WEB_START_REQUESTED"] = "1"
191
+ continue
192
+ if name == "--ca-tailscale-https":
193
+ environ["CIEL_RUNTIME_TAILSCALE_HTTPS"] = "1"
194
+ environ["CIEL_RUNTIME_WEB_START_REQUESTED"] = "1"
195
+ if separator and inline_value:
196
+ environ["CIEL_RUNTIME_TAILSCALE_HTTPS_PORT"] = str(
197
+ _valid_port(inline_value, name)
198
+ )
199
+ index += 1
200
+ continue
201
+ output.append(argument)
202
+ index += 1
203
+ return output
204
+
205
+
206
+ def _option_value(
207
+ argv: Sequence[str], index: int, inline_value: str, option: str
208
+ ) -> tuple[str, int]:
209
+ if inline_value:
210
+ return inline_value, index + 1
211
+ if index + 1 >= len(argv) or argv[index + 1] == "--":
212
+ raise SystemExit(f"{option} requires a value")
213
+ return argv[index + 1], index + 2
214
+
215
+
216
+ def _normalize_web_address(value: str) -> tuple[str, int | None]:
217
+ text = value.strip()
218
+ if not text:
219
+ raise SystemExit("--ca-web-address requires a host or URL")
220
+ parsed = urllib.parse.urlparse(text if "://" in text else f"//{text}")
221
+ if parsed.scheme and parsed.scheme not in {"http", "https"}:
222
+ raise SystemExit("--ca-web-address supports only http:// or https:// URLs")
223
+ if parsed.path not in {"", "/"} or parsed.query or parsed.fragment:
224
+ raise SystemExit("--ca-web-address must not include a path, query, or fragment")
225
+ try:
226
+ port = parsed.port
227
+ except ValueError as exc:
228
+ raise SystemExit(f"invalid web address port: {exc}") from exc
229
+ host = str(parsed.hostname or "").strip()
230
+ if not host or any(character.isspace() for character in host):
231
+ raise SystemExit("--ca-web-address contains an invalid host")
232
+ return host, port
233
+
234
+
235
+ def _valid_port(value: str, option: str) -> int:
236
+ try:
237
+ port = int(value)
238
+ except (TypeError, ValueError) as exc:
239
+ raise SystemExit(f"{option} requires a numeric port") from exc
240
+ if not 1 <= port <= 65535:
241
+ raise SystemExit(f"{option} port must be between 1 and 65535")
242
+ return port
243
+
244
+
245
+ def _apply_web_host(host: str, environ: MutableMapping[str, str]) -> None:
246
+ environ["CIEL_RUNTIME_ROUTER_BIND_HOST"] = host
247
+ environ["CIEL_RUNTIME_ROUTER_CLIENT_HOST"] = (
248
+ "127.0.0.1" if host in _WILDCARD_HOSTS else host
249
+ )
250
+ if host in _WILDCARD_HOSTS or not _is_loopback(host):
251
+ environ["CIEL_RUNTIME_ROUTER_DEBUG_EXTERNAL"] = "1"
252
+
253
+
254
+ def _is_loopback(host: str) -> bool:
255
+ if host.lower() == "localhost":
256
+ return True
257
+ try:
258
+ return ipaddress.ip_address(host).is_loopback
259
+ except ValueError:
260
+ return False
261
+
262
+
263
+ def discover_tailscale_node(*, timeout: float = 2.0) -> TailscaleNode | None:
264
+ executable = shutil.which("tailscale")
265
+ if not executable:
266
+ return None
267
+ try:
268
+ completed = subprocess.run(
269
+ [executable, "status", "--json"],
270
+ capture_output=True,
271
+ text=True,
272
+ timeout=timeout,
273
+ check=False,
274
+ )
275
+ if completed.returncode != 0:
276
+ return None
277
+ payload = json.loads(completed.stdout)
278
+ except (OSError, subprocess.SubprocessError, json.JSONDecodeError):
279
+ return None
280
+ if str(payload.get("BackendState") or "").lower() != "running":
281
+ return None
282
+ self_node = payload.get("Self") if isinstance(payload.get("Self"), dict) else {}
283
+ addresses = self_node.get("TailscaleIPs") or payload.get("TailscaleIPs") or []
284
+ ipv4 = next((str(item) for item in addresses if _is_ipv4(item)), "")
285
+ dns_name = str(self_node.get("DNSName") or "").strip().rstrip(".")
286
+ if not ipv4 and not dns_name:
287
+ return None
288
+ return TailscaleNode(ipv4=ipv4, dns_name=dns_name, online=bool(self_node.get("Online", True)))
289
+
290
+
291
+ def _is_ipv4(value: Any) -> bool:
292
+ try:
293
+ return ipaddress.ip_address(str(value)).version == 4
294
+ except ValueError:
295
+ return False
296
+
297
+
298
+ def tailscale_https_url_for_target(
299
+ node: TailscaleNode,
300
+ router_port: int,
301
+ *,
302
+ timeout: float = 2.0,
303
+ ) -> str:
304
+ if not node.dns_name:
305
+ return ""
306
+ executable = shutil.which("tailscale")
307
+ if not executable:
308
+ return ""
309
+ try:
310
+ completed = subprocess.run(
311
+ [executable, "serve", "status", "--json"],
312
+ capture_output=True,
313
+ text=True,
314
+ timeout=timeout,
315
+ check=False,
316
+ )
317
+ if completed.returncode != 0:
318
+ return ""
319
+ payload = json.loads(completed.stdout)
320
+ except (OSError, subprocess.SubprocessError, json.JSONDecodeError):
321
+ return ""
322
+ target = f"http://127.0.0.1:{router_port}"
323
+ web = payload.get("Web") if isinstance(payload.get("Web"), dict) else {}
324
+ tcp = payload.get("TCP") if isinstance(payload.get("TCP"), dict) else {}
325
+ for authority, config in web.items():
326
+ handlers = config.get("Handlers") if isinstance(config, dict) else {}
327
+ if not isinstance(handlers, dict):
328
+ continue
329
+ if not any(
330
+ isinstance(handler, dict) and str(handler.get("Proxy") or "").rstrip("/") == target
331
+ for handler in handlers.values()
332
+ ):
333
+ continue
334
+ port_text = str(authority).rsplit(":", 1)[-1]
335
+ port_config = tcp.get(port_text) if isinstance(tcp, dict) else None
336
+ if not isinstance(port_config, dict) or not port_config.get("HTTPS"):
337
+ continue
338
+ port = int(port_text) if port_text.isdigit() else 443
339
+ suffix = "" if port == 443 else f":{port}"
340
+ return f"https://{node.dns_name}{suffix}"
341
+ return ""
342
+
343
+
344
+ def build_web_endpoint_report(
345
+ client_host: str,
346
+ bind_host: str,
347
+ router_port: int,
348
+ ) -> WebEndpointReport:
349
+ display_host = _url_host(client_host or "127.0.0.1")
350
+ local_url = f"http://{display_host}:{router_port}/"
351
+ node = discover_tailscale_node()
352
+ if node is None:
353
+ return WebEndpointReport(local_url=local_url)
354
+ exposed = bind_host in _WILDCARD_HOSTS or bind_host in {node.ipv4, node.dns_name}
355
+ tailscale_ip_url = f"http://{node.ipv4}:{router_port}/" if exposed and node.ipv4 else ""
356
+ https_url = tailscale_https_url_for_target(node, router_port)
357
+ command = ""
358
+ if not https_url and node.dns_name:
359
+ command = (
360
+ f"ciel-runtime --ca-web-port {router_port} --ca-tailscale-https={router_port}"
361
+ )
362
+ return WebEndpointReport(
363
+ local_url=local_url,
364
+ tailscale_ip_url=tailscale_ip_url,
365
+ tailscale_https_url=f"{https_url}/" if https_url else "",
366
+ tailscale_https_command=command,
367
+ )
368
+
369
+
370
+ def configure_tailscale_https(router_port: int, https_port: int | None = None) -> list[str]:
371
+ node = discover_tailscale_node()
372
+ if node is None or not node.dns_name:
373
+ return ["Tailscale HTTPS was requested, but no online MagicDNS node was detected."]
374
+ executable = shutil.which("tailscale")
375
+ if not executable:
376
+ return ["Tailscale HTTPS was requested, but the tailscale CLI was not found."]
377
+ public_port = https_port or router_port
378
+ command = [
379
+ executable,
380
+ "serve",
381
+ f"--https={public_port}",
382
+ "--bg",
383
+ "--yes",
384
+ f"http://127.0.0.1:{router_port}",
385
+ ]
386
+ try:
387
+ completed = subprocess.run(
388
+ command,
389
+ capture_output=True,
390
+ text=True,
391
+ timeout=20.0,
392
+ check=False,
393
+ )
394
+ except (OSError, subprocess.SubprocessError) as exc:
395
+ return [f"Tailscale HTTPS setup failed: {type(exc).__name__}: {exc}"]
396
+ if completed.returncode != 0:
397
+ detail = (completed.stderr or completed.stdout or "unknown error").strip()
398
+ return [f"Tailscale HTTPS setup failed: {detail}"]
399
+ suffix = "" if public_port == 443 else f":{public_port}"
400
+ return [
401
+ f"Tailscale HTTPS: https://{node.dns_name}{suffix}/",
402
+ f"Tailscale Serve target: http://127.0.0.1:{router_port}",
403
+ ]
404
+
405
+
406
+ def configure_requested_web_endpoints(
407
+ router_port: int,
408
+ client_host: str,
409
+ bind_host: str,
410
+ environ: MutableMapping[str, str] | None = None,
411
+ config: dict[str, Any] | None = None,
412
+ ) -> list[str]:
413
+ environment = os.environ if environ is None else environ
414
+ lines: list[str] = []
415
+ saved = web_backend_settings(config)
416
+ requested = (
417
+ str(environment.get("CIEL_RUNTIME_TAILSCALE_HTTPS") or "").lower() in _TRUE
418
+ or (saved.enabled and saved.tailscale_https)
419
+ )
420
+ if requested:
421
+ configured_port = str(environment.get("CIEL_RUNTIME_TAILSCALE_HTTPS_PORT") or "").strip()
422
+ https_port = _valid_port(configured_port, "CIEL_RUNTIME_TAILSCALE_HTTPS_PORT") if configured_port else None
423
+ lines.extend(configure_tailscale_https(router_port, https_port))
424
+ lines.extend(build_web_endpoint_report(client_host, bind_host, router_port).status_lines())
425
+ return lines
426
+
427
+
428
+ def _url_host(host: str) -> str:
429
+ return f"[{host}]" if ":" in host and not host.startswith("[") else host
430
+
431
+
432
+ __all__ = [
433
+ "TailscaleNode",
434
+ "WebBackendSettings",
435
+ "WebEndpointReport",
436
+ "apply_startup_web_options",
437
+ "build_web_endpoint_report",
438
+ "configure_requested_web_endpoints",
439
+ "configure_tailscale_https",
440
+ "discover_tailscale_node",
441
+ "load_saved_web_backend",
442
+ "tailscale_https_url_for_target",
443
+ "update_web_backend_config",
444
+ "web_backend_panel_rows",
445
+ "web_backend_settings",
446
+ "web_backend_summary",
447
+ ]