@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.
- package/README.md +132 -0
- package/ciel-runtime-menu.py +56 -6
- package/ciel_runtime.py +9379 -35909
- package/ciel_runtime_support/advisor_client.py +193 -0
- package/ciel_runtime_support/advisor_policy.py +320 -0
- package/ciel_runtime_support/advisor_refinement.py +160 -0
- package/ciel_runtime_support/advisor_request_builder.py +261 -0
- package/ciel_runtime_support/agy_installer.py +169 -0
- package/ciel_runtime_support/agy_mcp_restore.py +182 -0
- package/ciel_runtime_support/anthropic_model_policy.py +186 -0
- package/ciel_runtime_support/anthropic_response_writer.py +255 -0
- package/ciel_runtime_support/anthropic_tool_turns.py +130 -0
- package/ciel_runtime_support/api_key_cooldown.py +159 -0
- package/ciel_runtime_support/architecture.py +488 -1
- package/ciel_runtime_support/architecture_budget.py +42 -0
- package/ciel_runtime_support/channel_backlog.py +90 -0
- package/ciel_runtime_support/channel_cli.py +119 -0
- package/ciel_runtime_support/channel_compact_injection.py +82 -0
- package/ciel_runtime_support/channel_compact_poll.py +67 -0
- package/ciel_runtime_support/channel_compact_request_repository.py +113 -0
- package/ciel_runtime_support/channel_config_service.py +281 -0
- package/ciel_runtime_support/channel_connection_lifecycle.py +180 -0
- package/ciel_runtime_support/channel_connection_registry.py +128 -0
- package/ciel_runtime_support/channel_connection_worker.py +284 -0
- package/ciel_runtime_support/channel_cursor_recovery.py +92 -0
- package/ciel_runtime_support/channel_cursor_repository.py +89 -0
- package/ciel_runtime_support/channel_cursor_service.py +178 -0
- package/ciel_runtime_support/channel_event_identity.py +212 -0
- package/ciel_runtime_support/channel_event_projection.py +315 -0
- package/ciel_runtime_support/channel_inflight.py +127 -0
- package/ciel_runtime_support/channel_injection.py +115 -0
- package/ciel_runtime_support/channel_launch_guard_repository.py +58 -0
- package/ciel_runtime_support/channel_launch_policy.py +180 -0
- package/ciel_runtime_support/channel_llm_context.py +156 -0
- package/ciel_runtime_support/channel_mcp_discovery.py +186 -0
- package/ciel_runtime_support/channel_mcp_http_controller.py +239 -0
- package/ciel_runtime_support/channel_mcp_ownership.py +148 -0
- package/ciel_runtime_support/channel_mcp_tools.py +240 -0
- package/ciel_runtime_support/channel_mcp_transport.py +394 -0
- package/ciel_runtime_support/channel_message_dedupe.py +65 -0
- package/ciel_runtime_support/channel_message_policy.py +256 -0
- package/ciel_runtime_support/channel_message_prompt.py +305 -0
- package/ciel_runtime_support/channel_message_repository.py +234 -0
- package/ciel_runtime_support/channel_notification_projection.py +217 -0
- package/ciel_runtime_support/channel_panel.py +162 -0
- package/ciel_runtime_support/channel_pending_injection.py +209 -0
- package/ciel_runtime_support/channel_pending_poll.py +109 -0
- package/ciel_runtime_support/channel_probe_cache.py +433 -0
- package/ciel_runtime_support/channel_probe_report.py +101 -0
- package/ciel_runtime_support/channel_runtime_environment.py +181 -0
- package/ciel_runtime_support/channel_session_lifecycle.py +114 -0
- package/ciel_runtime_support/channel_session_repository.py +90 -0
- package/ciel_runtime_support/channel_terminal_dispatch.py +115 -0
- package/ciel_runtime_support/channel_terminal_input.py +277 -0
- package/ciel_runtime_support/channel_terminal_proxy.py +447 -0
- package/ciel_runtime_support/channel_tool_context.py +166 -0
- package/ciel_runtime_support/channel_transcript.py +414 -0
- package/ciel_runtime_support/channel_transcript_repository.py +96 -0
- package/ciel_runtime_support/channel_wake_claim_repository.py +126 -0
- package/ciel_runtime_support/channel_wake_delivery_repository.py +88 -0
- package/ciel_runtime_support/chat_files.py +138 -0
- package/ciel_runtime_support/chat_http_controller.py +235 -0
- package/ciel_runtime_support/claude_environment.py +375 -0
- package/ciel_runtime_support/claude_router.py +247 -193
- package/ciel_runtime_support/cli_dispatch.py +792 -0
- package/ciel_runtime_support/cli_parser.py +165 -0
- package/ciel_runtime_support/cli_usage.py +100 -0
- package/ciel_runtime_support/codex_app_server.py +20 -5
- package/ciel_runtime_support/codex_channel_sse_launch.py +87 -0
- package/ciel_runtime_support/codex_cli.py +42 -6
- package/ciel_runtime_support/codex_config.py +323 -0
- package/ciel_runtime_support/codex_launch_configuration.py +240 -0
- package/ciel_runtime_support/codex_launch_policy.py +66 -0
- package/ciel_runtime_support/codex_mcp_integration.py +195 -0
- package/ciel_runtime_support/codex_mcp_restore.py +304 -0
- package/ciel_runtime_support/codex_model_catalog.py +133 -0
- package/ciel_runtime_support/codex_process_lifecycle.py +271 -0
- package/ciel_runtime_support/codex_router.py +147 -1
- package/ciel_runtime_support/codex_session_repository.py +115 -0
- package/ciel_runtime_support/codex_session_selection.py +114 -0
- package/ciel_runtime_support/command_asset_installer.py +103 -0
- package/ciel_runtime_support/compatibility_probe.py +295 -0
- package/ciel_runtime_support/compatibility_protocol.py +251 -0
- package/ciel_runtime_support/compatibility_runtime.py +166 -0
- package/ciel_runtime_support/compatibility_test.py +370 -0
- package/ciel_runtime_support/config_migrations.py +307 -0
- package/ciel_runtime_support/config_repository.py +175 -0
- package/ciel_runtime_support/config_value_codec.py +64 -0
- package/ciel_runtime_support/configuration_cli.py +374 -0
- package/ciel_runtime_support/context_compaction.py +280 -0
- package/ciel_runtime_support/context_setup.py +208 -0
- package/ciel_runtime_support/context_summary_policy.py +392 -0
- package/ciel_runtime_support/credential_cli.py +104 -0
- package/ciel_runtime_support/credential_management.py +261 -0
- package/ciel_runtime_support/credentials.py +269 -0
- package/ciel_runtime_support/executable_discovery.py +141 -0
- package/ciel_runtime_support/github_copilot_oauth.py +335 -0
- package/ciel_runtime_support/github_copilot_oauth_runtime.py +213 -0
- package/ciel_runtime_support/header_forwarding.py +73 -0
- package/ciel_runtime_support/headless_config.py +221 -0
- package/ciel_runtime_support/http_response.py +129 -0
- package/ciel_runtime_support/install_diagnostics.py +149 -0
- package/ciel_runtime_support/kimi_identity.py +123 -0
- package/ciel_runtime_support/launch_diagnostics.py +204 -0
- package/ciel_runtime_support/launch_state.py +127 -0
- package/ciel_runtime_support/live_api_key_controller.py +58 -0
- package/ciel_runtime_support/llm_config_http.py +148 -0
- package/ciel_runtime_support/llm_option_config.py +259 -0
- package/ciel_runtime_support/llm_presentation_data.py +447 -0
- package/ciel_runtime_support/llm_presets.py +773 -0
- package/ciel_runtime_support/lm_studio_runtime.py +401 -0
- package/ciel_runtime_support/managed_mcp_config.py +144 -0
- package/ciel_runtime_support/managed_mcp_discovery.py +151 -0
- package/ciel_runtime_support/managed_service_cleanup.py +89 -0
- package/ciel_runtime_support/mcp_config_reader.py +230 -0
- package/ciel_runtime_support/mcp_http_proxy.py +607 -0
- package/ciel_runtime_support/mcp_inventory.py +59 -0
- package/ciel_runtime_support/mcp_notification_wait_policy.py +159 -0
- package/ciel_runtime_support/mcp_probe_codec.py +136 -0
- package/ciel_runtime_support/mcp_probe_transport.py +328 -0
- package/ciel_runtime_support/mcp_proxy_codec.py +345 -0
- package/ciel_runtime_support/mcp_proxy_config.py +107 -0
- package/ciel_runtime_support/mcp_proxy_notifications.py +261 -0
- package/ciel_runtime_support/mcp_proxy_process.py +560 -0
- package/ciel_runtime_support/mcp_split_proxy_http.py +165 -0
- package/ciel_runtime_support/mcp_stdio_probe.py +240 -0
- package/ciel_runtime_support/mcp_transport.py +146 -0
- package/ciel_runtime_support/model_cache_lifecycle.py +78 -0
- package/ciel_runtime_support/model_catalog_projection.py +61 -0
- package/ciel_runtime_support/model_context_hints.py +109 -0
- package/ciel_runtime_support/model_panel.py +147 -0
- package/ciel_runtime_support/model_registry_repository.py +231 -0
- package/ciel_runtime_support/npm_runtime.py +191 -0
- package/ciel_runtime_support/ollama_catalog.py +462 -0
- package/ciel_runtime_support/ollama_catalog_cli.py +41 -0
- package/ciel_runtime_support/ollama_catalog_repository.py +75 -0
- package/ciel_runtime_support/ollama_context_sync.py +87 -0
- package/ciel_runtime_support/ollama_forwarding.py +449 -0
- package/ciel_runtime_support/openai_chat_passthrough.py +67 -0
- package/ciel_runtime_support/openai_chat_router.py +64 -0
- package/ciel_runtime_support/openai_forwarding.py +194 -0
- package/ciel_runtime_support/openai_responses_router.py +291 -0
- package/ciel_runtime_support/openai_responses_stream.py +135 -0
- package/ciel_runtime_support/output_budget.py +89 -0
- package/ciel_runtime_support/package_lifecycle.py +217 -0
- package/ciel_runtime_support/plan_artifact_controller.py +104 -0
- package/ciel_runtime_support/prelaunch.py +959 -0
- package/ciel_runtime_support/prelaunch_launch_preference.py +75 -0
- package/ciel_runtime_support/prelaunch_panel_projection.py +396 -0
- package/ciel_runtime_support/prelaunch_terminal.py +764 -0
- package/ciel_runtime_support/process_control.py +708 -0
- package/ciel_runtime_support/prompt_compaction.py +322 -0
- package/ciel_runtime_support/prompt_injection.py +176 -0
- package/ciel_runtime_support/protocols/__init__.py +24 -0
- package/ciel_runtime_support/protocols/anthropic_content.py +29 -0
- package/ciel_runtime_support/protocols/anthropic_thinking_policy.py +336 -0
- package/ciel_runtime_support/protocols/chat_projection.py +315 -0
- package/ciel_runtime_support/protocols/conversation_policy.py +217 -0
- package/ciel_runtime_support/protocols/conversation_turn_policy.py +972 -0
- package/ciel_runtime_support/protocols/ollama_chat.py +111 -0
- package/ciel_runtime_support/protocols/ollama_response.py +231 -0
- package/ciel_runtime_support/protocols/openai_reasoning.py +75 -0
- package/ciel_runtime_support/protocols/openai_responses.py +271 -0
- package/ciel_runtime_support/protocols/pseudo_tool_history.py +248 -0
- package/ciel_runtime_support/protocols/tool_result_projection.py +112 -0
- package/ciel_runtime_support/provider_adapters.py +160 -0
- package/ciel_runtime_support/provider_catalog_sources.py +316 -0
- package/ciel_runtime_support/provider_choice.py +201 -0
- package/ciel_runtime_support/provider_compatibility.py +165 -0
- package/ciel_runtime_support/provider_config_mutations.py +361 -0
- package/ciel_runtime_support/provider_configuration_service.py +162 -0
- package/ciel_runtime_support/provider_context.py +308 -0
- package/ciel_runtime_support/provider_contract_projection.py +73 -0
- package/ciel_runtime_support/provider_descriptor.py +82 -0
- package/ciel_runtime_support/provider_endpoint_policy.py +130 -0
- package/ciel_runtime_support/provider_endpoint_probe.py +165 -0
- package/ciel_runtime_support/provider_launch_endpoint.py +109 -0
- package/ciel_runtime_support/provider_limits.py +457 -0
- package/ciel_runtime_support/provider_model_identity.py +139 -0
- package/ciel_runtime_support/provider_model_selection.py +431 -0
- package/ciel_runtime_support/provider_model_specs.py +142 -0
- package/ciel_runtime_support/provider_models.py +263 -0
- package/ciel_runtime_support/provider_network.py +176 -0
- package/ciel_runtime_support/provider_option_cli.py +238 -0
- package/ciel_runtime_support/provider_option_panel.py +275 -0
- package/ciel_runtime_support/provider_option_status.py +192 -0
- package/ciel_runtime_support/provider_policy.py +101 -0
- package/ciel_runtime_support/provider_query_policy.py +67 -0
- package/ciel_runtime_support/provider_readiness.py +112 -0
- package/ciel_runtime_support/provider_request_access.py +131 -0
- package/ciel_runtime_support/provider_request_builder.py +250 -0
- package/ciel_runtime_support/provider_responses_passthrough.py +81 -0
- package/ciel_runtime_support/provider_runtime_info.py +113 -0
- package/ciel_runtime_support/provider_runtime_modes.py +150 -0
- package/ciel_runtime_support/provider_sampling_policy.py +46 -0
- package/ciel_runtime_support/provider_status.py +145 -0
- package/ciel_runtime_support/provider_timeout_policy.py +184 -0
- package/ciel_runtime_support/provider_tool_policy.py +145 -0
- package/ciel_runtime_support/providers/__init__.py +65 -0
- package/ciel_runtime_support/providers/anthropic.py +160 -0
- package/ciel_runtime_support/providers/anthropic_catalog.py +119 -0
- package/ciel_runtime_support/providers/base.py +232 -0
- package/ciel_runtime_support/providers/catalog.py +326 -0
- package/ciel_runtime_support/providers/cloud.py +194 -0
- package/ciel_runtime_support/providers/constants.py +54 -0
- package/ciel_runtime_support/providers/deepseek.py +115 -0
- package/ciel_runtime_support/providers/fireworks.py +158 -0
- package/ciel_runtime_support/providers/github_copilot_oauth.py +199 -0
- package/ciel_runtime_support/providers/kimi.py +297 -0
- package/ciel_runtime_support/providers/lm_studio.py +76 -0
- package/ciel_runtime_support/providers/meta.py +257 -0
- package/ciel_runtime_support/providers/native.py +194 -0
- package/ciel_runtime_support/providers/nim.py +69 -0
- package/ciel_runtime_support/providers/nvidia.py +158 -0
- package/ciel_runtime_support/providers/nvidia_runtime.py +285 -0
- package/ciel_runtime_support/providers/ollama.py +181 -0
- package/ciel_runtime_support/providers/ollama_context.py +195 -0
- package/ciel_runtime_support/providers/ollama_runtime.py +213 -0
- package/ciel_runtime_support/providers/opencode.py +220 -0
- package/ciel_runtime_support/providers/opencode_go.py +37 -0
- package/ciel_runtime_support/providers/openrouter.py +57 -0
- package/ciel_runtime_support/providers/vllm.py +65 -0
- package/ciel_runtime_support/providers/zai.py +119 -0
- package/ciel_runtime_support/pseudo_tool_parser.py +115 -0
- package/ciel_runtime_support/rate_limit_policy.py +117 -0
- package/ciel_runtime_support/rate_limit_repository.py +154 -0
- package/ciel_runtime_support/registry.py +46 -0
- package/ciel_runtime_support/request_shortcuts.py +253 -0
- package/ciel_runtime_support/request_trace.py +323 -0
- package/ciel_runtime_support/response_collection.py +209 -0
- package/ciel_runtime_support/router_access.py +238 -0
- package/ciel_runtime_support/router_client_lifecycle.py +366 -0
- package/ciel_runtime_support/router_health_policy.py +101 -0
- package/ciel_runtime_support/router_http.py +513 -0
- package/ciel_runtime_support/router_process_lifecycle.py +401 -0
- package/ciel_runtime_support/router_rate_limit_service.py +285 -0
- package/ciel_runtime_support/router_server_runtime.py +103 -0
- package/ciel_runtime_support/router_shortcuts.py +201 -0
- package/ciel_runtime_support/routing_fallback.py +73 -0
- package/ciel_runtime_support/runtime_activity_repository.py +143 -0
- package/ciel_runtime_support/runtime_adapters.py +104 -0
- package/ciel_runtime_support/runtime_command_factory.py +73 -0
- package/ciel_runtime_support/runtime_compatibility.py +50 -0
- package/ciel_runtime_support/runtime_constants.py +178 -0
- package/ciel_runtime_support/runtime_launch.py +1602 -0
- package/ciel_runtime_support/runtime_llm_options.py +312 -0
- package/ciel_runtime_support/runtime_logging.py +161 -0
- package/ciel_runtime_support/runtime_paths.py +157 -0
- package/ciel_runtime_support/runtime_restart.py +84 -0
- package/ciel_runtime_support/runtime_upgrade.py +149 -0
- package/ciel_runtime_support/secure_json_repository.py +55 -0
- package/ciel_runtime_support/session_import.py +356 -0
- package/ciel_runtime_support/settings_repository.py +8 -0
- package/ciel_runtime_support/slash_command_assets.py +211 -0
- package/ciel_runtime_support/sse_stream.py +57 -0
- package/ciel_runtime_support/sse_trace.py +225 -0
- package/ciel_runtime_support/statusline_script.py +593 -0
- package/ciel_runtime_support/statusline_settings.py +53 -0
- package/ciel_runtime_support/stream_chunk_policy.py +18 -0
- package/ciel_runtime_support/streaming_anthropic.py +1955 -0
- package/ciel_runtime_support/synthetic_tool_policy.py +105 -0
- package/ciel_runtime_support/terminal_platform_io.py +127 -0
- package/ciel_runtime_support/timeout_profile.py +196 -0
- package/ciel_runtime_support/tool_dialects.py +85 -0
- package/ciel_runtime_support/tool_exposure_policy.py +63 -0
- package/ciel_runtime_support/tool_guard_hooks.py +218 -0
- package/ciel_runtime_support/tool_request_projection.py +96 -0
- package/ciel_runtime_support/tool_schema.py +483 -0
- package/ciel_runtime_support/tool_side_effect_dedupe.py +95 -0
- package/ciel_runtime_support/ui_text.py +266 -0
- package/ciel_runtime_support/upstream_error_policy.py +104 -0
- package/ciel_runtime_support/upstream_retry.py +419 -0
- package/ciel_runtime_support/upstream_stream_io.py +106 -0
- package/ciel_runtime_support/usage_events.py +96 -0
- package/ciel_runtime_support/visible_stream_filters.py +130 -0
- package/ciel_runtime_support/web_endpoints.py +447 -0
- package/ciel_runtime_support/web_ui.py +915 -0
- package/ciel_runtime_support/web_ui_controller.py +189 -0
- package/ciel_runtime_support/windows_console_input.py +137 -0
- package/ciel_runtime_support/windows_console_mode.py +112 -0
- package/docs/Architecture.md +54 -0
- package/docs/Configuration.md +17 -0
- package/docs/Module-Map.md +1093 -26
- package/docs/Providers.md +46 -1
- package/docs/adr/0001-runtime-bounded-contexts.md +295 -0
- package/npm-bin/run-ciel-runtime.js +22 -2
- package/package.json +9 -2
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Transparent end-to-end request header forwarding policies."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
HOP_BY_HOP_REQUEST_HEADERS = frozenset(
|
|
9
|
+
{
|
|
10
|
+
"accept-encoding",
|
|
11
|
+
"connection",
|
|
12
|
+
"content-length",
|
|
13
|
+
"expect",
|
|
14
|
+
"forwarded",
|
|
15
|
+
"host",
|
|
16
|
+
"keep-alive",
|
|
17
|
+
"proxy-authenticate",
|
|
18
|
+
"proxy-authorization",
|
|
19
|
+
"te",
|
|
20
|
+
"trailer",
|
|
21
|
+
"transfer-encoding",
|
|
22
|
+
"upgrade",
|
|
23
|
+
"via",
|
|
24
|
+
"x-forwarded-for",
|
|
25
|
+
"x-forwarded-host",
|
|
26
|
+
"x-forwarded-proto",
|
|
27
|
+
}
|
|
28
|
+
)
|
|
29
|
+
CONFIGURED_PROVIDER_CREDENTIAL_HEADERS = frozenset(
|
|
30
|
+
{"authorization", "cookie", "set-cookie", "x-api-key"}
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def project_end_to_end_request_headers(
|
|
35
|
+
inbound_headers: Any | None,
|
|
36
|
+
*,
|
|
37
|
+
replace_credentials: bool,
|
|
38
|
+
transport_headers: frozenset[str] = HOP_BY_HOP_REQUEST_HEADERS,
|
|
39
|
+
) -> dict[str, str]:
|
|
40
|
+
"""Copy client headers verbatim except transport-owned connection headers.
|
|
41
|
+
|
|
42
|
+
Header names and values are not interpreted or reconstructed. Configured
|
|
43
|
+
provider routes additionally remove client credentials before the provider
|
|
44
|
+
adapter installs the selected provider credential.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
if inbound_headers is None:
|
|
48
|
+
return {}
|
|
49
|
+
try:
|
|
50
|
+
items = inbound_headers.items()
|
|
51
|
+
except Exception:
|
|
52
|
+
return {}
|
|
53
|
+
excluded = transport_headers
|
|
54
|
+
if replace_credentials:
|
|
55
|
+
excluded = excluded | CONFIGURED_PROVIDER_CREDENTIAL_HEADERS
|
|
56
|
+
forwarded: dict[str, str] = {}
|
|
57
|
+
for raw_name, raw_value in items:
|
|
58
|
+
original_name = str(raw_name)
|
|
59
|
+
normalized_name = original_name.casefold()
|
|
60
|
+
if (
|
|
61
|
+
normalized_name not in excluded
|
|
62
|
+
and not normalized_name.startswith("x-ciel-runtime-")
|
|
63
|
+
and raw_value is not None
|
|
64
|
+
):
|
|
65
|
+
forwarded[original_name] = str(raw_value)
|
|
66
|
+
return forwarded
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
__all__ = [
|
|
70
|
+
"CONFIGURED_PROVIDER_CREDENTIAL_HEADERS",
|
|
71
|
+
"HOP_BY_HOP_REQUEST_HEADERS",
|
|
72
|
+
"project_end_to_end_request_headers",
|
|
73
|
+
]
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""Application service for applying headless environment configuration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Callable, Mapping
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True, slots=True)
|
|
12
|
+
class HeadlessConfigCommands:
|
|
13
|
+
set_language: Callable[[str], None]
|
|
14
|
+
set_web_fetch: Callable[[bool], None]
|
|
15
|
+
set_provider: Callable[[str], None]
|
|
16
|
+
set_api_keys: Callable[[str, list[str]], None]
|
|
17
|
+
set_api_key: Callable[[str, str], None]
|
|
18
|
+
set_base_url: Callable[[str, str], None]
|
|
19
|
+
set_model: Callable[[str], None]
|
|
20
|
+
set_advisor_model: Callable[[str], None]
|
|
21
|
+
set_provider_options: Callable[[list[str]], None]
|
|
22
|
+
set_ollama_options: Callable[[list[str]], None]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True, slots=True)
|
|
26
|
+
class HeadlessChannelCommands:
|
|
27
|
+
add_channel: Callable[[str], None]
|
|
28
|
+
set_delivery: Callable[[str], None]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True, slots=True)
|
|
32
|
+
class HeadlessConfigServices:
|
|
33
|
+
environ: Mapping[str, str]
|
|
34
|
+
parse_bool: Callable[[str | None, bool | None], bool | None]
|
|
35
|
+
current_provider: Callable[[], str]
|
|
36
|
+
commands: HeadlessConfigCommands
|
|
37
|
+
channels: HeadlessChannelCommands
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True, slots=True)
|
|
41
|
+
class HeadlessConfigResult:
|
|
42
|
+
skip_menu: bool
|
|
43
|
+
web_search_override: bool | None
|
|
44
|
+
update_check_override: bool | None
|
|
45
|
+
self_update_check_override: bool | None
|
|
46
|
+
force_menu: bool
|
|
47
|
+
|
|
48
|
+
def as_tuple(self) -> tuple[bool, bool | None, bool | None, bool | None, bool]:
|
|
49
|
+
return (
|
|
50
|
+
self.skip_menu,
|
|
51
|
+
self.web_search_override,
|
|
52
|
+
self.update_check_override,
|
|
53
|
+
self.self_update_check_override,
|
|
54
|
+
self.force_menu,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True, slots=True)
|
|
59
|
+
class HeadlessEnvFileLoader:
|
|
60
|
+
load: Callable[..., None]
|
|
61
|
+
|
|
62
|
+
def pop_args(self, argv: list[str]) -> list[str]:
|
|
63
|
+
cleaned: list[str] = []
|
|
64
|
+
index = 0
|
|
65
|
+
while index < len(argv):
|
|
66
|
+
argument = argv[index]
|
|
67
|
+
if (
|
|
68
|
+
argument == "--ca-env-file"
|
|
69
|
+
or argument.startswith("--ca-env-file=")
|
|
70
|
+
):
|
|
71
|
+
value = (
|
|
72
|
+
argument.split("=", 1)[1]
|
|
73
|
+
if "=" in argument
|
|
74
|
+
else None
|
|
75
|
+
)
|
|
76
|
+
if value is None:
|
|
77
|
+
if index + 1 >= len(argv):
|
|
78
|
+
raise SystemExit(
|
|
79
|
+
"Missing path for --ca-env-file"
|
|
80
|
+
)
|
|
81
|
+
value = argv[index + 1]
|
|
82
|
+
index += 2
|
|
83
|
+
else:
|
|
84
|
+
index += 1
|
|
85
|
+
path = Path(value).expanduser()
|
|
86
|
+
if not path.exists():
|
|
87
|
+
raise SystemExit(
|
|
88
|
+
f"--ca-env-file not found: {path}"
|
|
89
|
+
)
|
|
90
|
+
self.load(path, override=True)
|
|
91
|
+
continue
|
|
92
|
+
cleaned.append(argument)
|
|
93
|
+
index += 1
|
|
94
|
+
return cleaned
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
PROVIDER_OPTION_ENV = {
|
|
98
|
+
"CIEL_RUNTIME_MAX_OUTPUT_TOKENS": "max_output_tokens",
|
|
99
|
+
"CIEL_RUNTIME_CONTEXT_WINDOW": "context_window",
|
|
100
|
+
"CIEL_RUNTIME_REQUEST_TIMEOUT_MS": "request_timeout_ms",
|
|
101
|
+
"CIEL_RUNTIME_STREAM_IDLE_TIMEOUT_MS": "stream_idle_timeout_ms",
|
|
102
|
+
"CIEL_RUNTIME_RATE_LIMIT_RPM": "rate_limit_rpm",
|
|
103
|
+
"CIEL_RUNTIME_RATE_LIMIT_STATUS": "rate_limit_status",
|
|
104
|
+
"CIEL_RUNTIME_STREAM": "stream_enabled",
|
|
105
|
+
"CIEL_RUNTIME_STREAM_WORD_CHUNKING": "stream_word_chunking",
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def apply_headless_config(services: HeadlessConfigServices) -> HeadlessConfigResult:
|
|
110
|
+
env = services.environ
|
|
111
|
+
commands = services.commands
|
|
112
|
+
skip_menu = env.get("CIEL_RUNTIME_SKIP_MENU") == "1"
|
|
113
|
+
force_menu = bool(services.parse_bool(env.get("CIEL_RUNTIME_FORCE_MENU"), False))
|
|
114
|
+
web_search_override = services.parse_bool(env.get("CIEL_RUNTIME_WEB_SEARCH"), None)
|
|
115
|
+
update_check_override = services.parse_bool(env.get("CIEL_RUNTIME_UPDATE_CHECK"), None)
|
|
116
|
+
self_update_check_override = services.parse_bool(env.get("CIEL_RUNTIME_SELF_UPDATE_CHECK"), None)
|
|
117
|
+
|
|
118
|
+
language = env.get("CIEL_RUNTIME_LANGUAGE", "").strip()
|
|
119
|
+
if language:
|
|
120
|
+
commands.set_language(language)
|
|
121
|
+
skip_menu = True
|
|
122
|
+
web_fetch = services.parse_bool(env.get("CIEL_RUNTIME_WEB_FETCH"), None)
|
|
123
|
+
if web_fetch is not None:
|
|
124
|
+
commands.set_web_fetch(web_fetch)
|
|
125
|
+
skip_menu = True
|
|
126
|
+
provider = env.get("CIEL_RUNTIME_PROVIDER", "").strip()
|
|
127
|
+
if provider:
|
|
128
|
+
commands.set_provider(provider)
|
|
129
|
+
skip_menu = True
|
|
130
|
+
|
|
131
|
+
current_provider = services.current_provider()
|
|
132
|
+
skip_menu = _apply_api_key_config(env, current_provider, commands) or skip_menu
|
|
133
|
+
|
|
134
|
+
base_url = env.get("CIEL_RUNTIME_BASE_URL", "").strip()
|
|
135
|
+
if base_url:
|
|
136
|
+
commands.set_base_url(services.current_provider(), base_url)
|
|
137
|
+
skip_menu = True
|
|
138
|
+
model = env.get("CIEL_RUNTIME_MODEL", "").strip()
|
|
139
|
+
if model:
|
|
140
|
+
commands.set_model(model)
|
|
141
|
+
skip_menu = True
|
|
142
|
+
advisor_model = env.get("CIEL_RUNTIME_ADVISOR_MODEL", "").strip()
|
|
143
|
+
if advisor_model:
|
|
144
|
+
commands.set_advisor_model(advisor_model)
|
|
145
|
+
skip_menu = True
|
|
146
|
+
|
|
147
|
+
provider_values = [
|
|
148
|
+
f"{option_key}={env[env_key].strip()}"
|
|
149
|
+
for env_key, option_key in PROVIDER_OPTION_ENV.items()
|
|
150
|
+
if env.get(env_key, "").strip()
|
|
151
|
+
]
|
|
152
|
+
if provider_values:
|
|
153
|
+
commands.set_provider_options(provider_values)
|
|
154
|
+
skip_menu = True
|
|
155
|
+
|
|
156
|
+
ollama_values = _ollama_option_values(env)
|
|
157
|
+
if ollama_values:
|
|
158
|
+
commands.set_ollama_options(ollama_values)
|
|
159
|
+
skip_menu = True
|
|
160
|
+
|
|
161
|
+
for channel in _split_values(env.get("CIEL_RUNTIME_CHANNELS", "")):
|
|
162
|
+
services.channels.add_channel(channel)
|
|
163
|
+
skip_menu = True
|
|
164
|
+
for channel in _split_values(env.get("CIEL_RUNTIME_DEV_CHANNELS", "")):
|
|
165
|
+
services.channels.add_channel(channel)
|
|
166
|
+
skip_menu = True
|
|
167
|
+
channel_delivery = env.get("CIEL_RUNTIME_CHANNEL_DELIVERY", "").strip()
|
|
168
|
+
if channel_delivery:
|
|
169
|
+
services.channels.set_delivery(channel_delivery)
|
|
170
|
+
skip_menu = True
|
|
171
|
+
|
|
172
|
+
return HeadlessConfigResult(
|
|
173
|
+
skip_menu=skip_menu,
|
|
174
|
+
web_search_override=web_search_override,
|
|
175
|
+
update_check_override=update_check_override,
|
|
176
|
+
self_update_check_override=self_update_check_override,
|
|
177
|
+
force_menu=force_menu,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _apply_api_key_config(
|
|
182
|
+
env: Mapping[str, str],
|
|
183
|
+
provider: str,
|
|
184
|
+
commands: HeadlessConfigCommands,
|
|
185
|
+
) -> bool:
|
|
186
|
+
api_keys_env = env.get("CIEL_RUNTIME_API_KEYS_ENV", "").strip()
|
|
187
|
+
api_keys = env.get("CIEL_RUNTIME_API_KEYS", "").strip()
|
|
188
|
+
api_key_env = env.get("CIEL_RUNTIME_API_KEY_ENV", "").strip()
|
|
189
|
+
api_key = env.get("CIEL_RUNTIME_API_KEY", "").strip()
|
|
190
|
+
if api_keys_env:
|
|
191
|
+
value = env.get(api_keys_env, "")
|
|
192
|
+
if not value:
|
|
193
|
+
raise SystemExit(f"Environment variable {api_keys_env} is empty or not set")
|
|
194
|
+
commands.set_api_keys(provider, [value])
|
|
195
|
+
return True
|
|
196
|
+
if api_keys:
|
|
197
|
+
commands.set_api_keys(provider, [api_keys])
|
|
198
|
+
return True
|
|
199
|
+
if api_key_env:
|
|
200
|
+
value = env.get(api_key_env, "")
|
|
201
|
+
if not value:
|
|
202
|
+
raise SystemExit(f"Environment variable {api_key_env} is empty or not set")
|
|
203
|
+
commands.set_api_key(provider, value)
|
|
204
|
+
return True
|
|
205
|
+
if api_key:
|
|
206
|
+
commands.set_api_key(provider, api_key)
|
|
207
|
+
return True
|
|
208
|
+
return False
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _ollama_option_values(env: Mapping[str, str]) -> list[str]:
|
|
212
|
+
values: list[str] = []
|
|
213
|
+
num_ctx = env.get("CIEL_RUNTIME_OLLAMA_NUM_CTX", "").strip()
|
|
214
|
+
if num_ctx:
|
|
215
|
+
values.append(f"num_ctx={num_ctx}")
|
|
216
|
+
values.extend(item for item in env.get("CIEL_RUNTIME_OLLAMA_OPTIONS", "").replace(",", " ").split() if item)
|
|
217
|
+
return values
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _split_values(value: str) -> list[str]:
|
|
221
|
+
return [item.strip() for item in re.split(r"[\s,]+", value.strip()) if item.strip()]
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""HTTP response writer and pending channel-delivery guard."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import errno
|
|
6
|
+
import json
|
|
7
|
+
from typing import Any, Callable
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
CLIENT_DISCONNECT_ERRNOS = {
|
|
11
|
+
errno.EPIPE,
|
|
12
|
+
errno.ECONNRESET,
|
|
13
|
+
getattr(errno, "ECONNABORTED", errno.ECONNRESET),
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class HttpResponseAdapter:
|
|
18
|
+
def __init__(self, log: Callable[[str, str], None]) -> None:
|
|
19
|
+
self.log = log
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def write_json(handler: Any, value: Any, status: int = 200) -> None:
|
|
23
|
+
body = json.dumps(value).encode("utf-8")
|
|
24
|
+
handler.send_response(status)
|
|
25
|
+
handler.send_header("content-type", "application/json")
|
|
26
|
+
handler.send_header("content-length", str(len(body)))
|
|
27
|
+
handler.end_headers()
|
|
28
|
+
handler.wfile.write(body)
|
|
29
|
+
|
|
30
|
+
@staticmethod
|
|
31
|
+
def is_client_disconnect(error: BaseException) -> bool:
|
|
32
|
+
if isinstance(error, (BrokenPipeError, ConnectionResetError, ConnectionAbortedError)):
|
|
33
|
+
return True
|
|
34
|
+
return isinstance(error, OSError) and getattr(error, "errno", None) in CLIENT_DISCONNECT_ERRNOS
|
|
35
|
+
|
|
36
|
+
def try_write_json(self, handler: Any, value: Any, status: int = 200) -> bool:
|
|
37
|
+
try:
|
|
38
|
+
self.write_json(handler, value, status)
|
|
39
|
+
return True
|
|
40
|
+
except Exception as exc:
|
|
41
|
+
if self.is_client_disconnect(exc):
|
|
42
|
+
self.log(
|
|
43
|
+
"WARN",
|
|
44
|
+
f"write_json_client_disconnected status={status} "
|
|
45
|
+
f"error={type(exc).__name__}: {exc}",
|
|
46
|
+
)
|
|
47
|
+
return False
|
|
48
|
+
raise
|
|
49
|
+
|
|
50
|
+
@staticmethod
|
|
51
|
+
def response_status(handler: Any) -> int | None:
|
|
52
|
+
try:
|
|
53
|
+
return int(getattr(handler, "_ciel_runtime_response_status", None))
|
|
54
|
+
except Exception:
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
@staticmethod
|
|
58
|
+
def write_empty(handler: Any, status: int = 202) -> None:
|
|
59
|
+
handler.send_response(status)
|
|
60
|
+
handler.send_header("content-length", "0")
|
|
61
|
+
handler.end_headers()
|
|
62
|
+
|
|
63
|
+
@staticmethod
|
|
64
|
+
def write_accepted(handler: Any) -> None:
|
|
65
|
+
body = b"Accepted"
|
|
66
|
+
handler.send_response(202)
|
|
67
|
+
handler.send_header("content-type", "text/plain")
|
|
68
|
+
handler.send_header("content-length", str(len(body)))
|
|
69
|
+
handler.end_headers()
|
|
70
|
+
handler.wfile.write(body)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class ChannelDeliveryGuard:
|
|
74
|
+
def __init__(self, log: Callable[[str, str], None]) -> None:
|
|
75
|
+
self.log = log
|
|
76
|
+
|
|
77
|
+
@staticmethod
|
|
78
|
+
def metadata_enabled(metadata: dict[str, Any] | None) -> bool:
|
|
79
|
+
return isinstance(metadata, dict) and bool(
|
|
80
|
+
metadata.get("ciel_runtime_channel_cursor_last_id")
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def begin(self, handler: Any | None, body: dict[str, Any]) -> None:
|
|
84
|
+
if handler is None:
|
|
85
|
+
return
|
|
86
|
+
metadata = body.get("metadata") if isinstance(body.get("metadata"), dict) else {}
|
|
87
|
+
if not self.metadata_enabled(metadata):
|
|
88
|
+
return
|
|
89
|
+
try:
|
|
90
|
+
setattr(handler, "_ciel_runtime_channel_delivery_guard", True)
|
|
91
|
+
setattr(handler, "_ciel_runtime_channel_delivery_ok", False)
|
|
92
|
+
setattr(handler, "_ciel_runtime_channel_delivery_reason", "pending")
|
|
93
|
+
except Exception as exc:
|
|
94
|
+
self.log(
|
|
95
|
+
"WARN",
|
|
96
|
+
f"channel_delivery_guard_begin_failed error={type(exc).__name__}: {exc}",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def success(self, handler: Any | None, reason: str = "response_complete") -> None:
|
|
100
|
+
if handler is None or not getattr(handler, "_ciel_runtime_channel_delivery_guard", False):
|
|
101
|
+
return
|
|
102
|
+
try:
|
|
103
|
+
setattr(handler, "_ciel_runtime_channel_delivery_ok", True)
|
|
104
|
+
setattr(handler, "_ciel_runtime_channel_delivery_reason", reason)
|
|
105
|
+
except Exception as exc:
|
|
106
|
+
self.log(
|
|
107
|
+
"WARN",
|
|
108
|
+
f"channel_delivery_guard_success_failed reason={reason} "
|
|
109
|
+
f"error={type(exc).__name__}: {exc}",
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def failed(self, handler: Any | None, reason: str = "response_failed") -> None:
|
|
113
|
+
if handler is None or not getattr(handler, "_ciel_runtime_channel_delivery_guard", False):
|
|
114
|
+
return
|
|
115
|
+
try:
|
|
116
|
+
setattr(handler, "_ciel_runtime_channel_delivery_ok", False)
|
|
117
|
+
setattr(handler, "_ciel_runtime_channel_delivery_reason", reason)
|
|
118
|
+
except Exception as exc:
|
|
119
|
+
self.log(
|
|
120
|
+
"WARN",
|
|
121
|
+
f"channel_delivery_guard_failure_failed reason={reason} "
|
|
122
|
+
f"error={type(exc).__name__}: {exc}",
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
@staticmethod
|
|
126
|
+
def confirmed(handler: Any | None) -> bool:
|
|
127
|
+
if handler is None or not getattr(handler, "_ciel_runtime_channel_delivery_guard", False):
|
|
128
|
+
return True
|
|
129
|
+
return bool(getattr(handler, "_ciel_runtime_channel_delivery_ok", False))
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Discovery and presentation of duplicate Ciel Runtime installations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import subprocess
|
|
7
|
+
from collections.abc import Callable, Mapping
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True, slots=True)
|
|
13
|
+
class InstallDiagnosticsSettings:
|
|
14
|
+
home: Path
|
|
15
|
+
environ: Mapping[str, str]
|
|
16
|
+
windows: bool
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True, slots=True)
|
|
20
|
+
class InstallDiagnosticsPorts:
|
|
21
|
+
extra_dirs: Callable[[], list[Path]]
|
|
22
|
+
package_root: Callable[[Path], Path | None]
|
|
23
|
+
current_root: Callable[[], Path | None]
|
|
24
|
+
parse_version: Callable[[str], tuple[int, ...]]
|
|
25
|
+
diagnostics: Callable[[], list[dict[str, str]]]
|
|
26
|
+
stdin_isatty: Callable[[], bool]
|
|
27
|
+
stdout_isatty: Callable[[], bool]
|
|
28
|
+
write_error: Callable[[str], None]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True, slots=True)
|
|
32
|
+
class InstallDiagnosticsService:
|
|
33
|
+
settings: InstallDiagnosticsSettings
|
|
34
|
+
ports: InstallDiagnosticsPorts
|
|
35
|
+
|
|
36
|
+
def candidate_dirs(self) -> list[Path]:
|
|
37
|
+
raw_dirs = [
|
|
38
|
+
Path(entry)
|
|
39
|
+
for entry in self.settings.environ.get("PATH", "").split(";" if self.settings.windows else ":")
|
|
40
|
+
if entry
|
|
41
|
+
]
|
|
42
|
+
raw_dirs.extend(self.ports.extra_dirs())
|
|
43
|
+
raw_dirs.extend(
|
|
44
|
+
[self.settings.home / ".npm-global" / "bin", self.settings.home / "bin"]
|
|
45
|
+
)
|
|
46
|
+
if not self.settings.windows:
|
|
47
|
+
raw_dirs.extend([Path("/usr/local/bin"), Path("/usr/bin")])
|
|
48
|
+
return list(dict.fromkeys(raw_dirs))
|
|
49
|
+
|
|
50
|
+
def candidates(self) -> list[Path]:
|
|
51
|
+
names = ["ciel-runtime"]
|
|
52
|
+
if self.settings.windows:
|
|
53
|
+
names.extend(["ciel-runtime.cmd", "ciel-runtime.exe"])
|
|
54
|
+
candidates: list[Path] = []
|
|
55
|
+
seen: set[str] = set()
|
|
56
|
+
for directory in self.candidate_dirs():
|
|
57
|
+
for name in names:
|
|
58
|
+
candidate = directory / name
|
|
59
|
+
if not candidate.exists():
|
|
60
|
+
continue
|
|
61
|
+
try:
|
|
62
|
+
key = str(candidate.resolve(strict=False))
|
|
63
|
+
except Exception:
|
|
64
|
+
key = str(candidate)
|
|
65
|
+
if key not in seen:
|
|
66
|
+
seen.add(key)
|
|
67
|
+
candidates.append(candidate)
|
|
68
|
+
return candidates
|
|
69
|
+
|
|
70
|
+
def launcher_version(self, path: Path, timeout: float = 5.0) -> str:
|
|
71
|
+
environ = dict(self.settings.environ)
|
|
72
|
+
environ.update(
|
|
73
|
+
CIEL_RUNTIME_SKIP_INSTALL_DIAGNOSTIC="1",
|
|
74
|
+
CIEL_RUNTIME_SKIP_SELF_UPDATE="1",
|
|
75
|
+
CIEL_RUNTIME_SELF_UPDATE_CHECK="off",
|
|
76
|
+
)
|
|
77
|
+
try:
|
|
78
|
+
process = subprocess.run(
|
|
79
|
+
[str(path), "--version"],
|
|
80
|
+
text=True,
|
|
81
|
+
stdout=subprocess.PIPE,
|
|
82
|
+
stderr=subprocess.STDOUT,
|
|
83
|
+
env=environ,
|
|
84
|
+
timeout=timeout,
|
|
85
|
+
)
|
|
86
|
+
except Exception:
|
|
87
|
+
return ""
|
|
88
|
+
if process.returncode != 0:
|
|
89
|
+
return ""
|
|
90
|
+
output = process.stdout or ""
|
|
91
|
+
match = re.search(r"ciel-runtime\s+(.+)", output, re.IGNORECASE)
|
|
92
|
+
return match.group(1).strip() if match else output.strip().splitlines()[-1].strip()
|
|
93
|
+
|
|
94
|
+
def diagnostics(self) -> list[dict[str, str]]:
|
|
95
|
+
rows: list[dict[str, str]] = []
|
|
96
|
+
for launcher in self.candidates():
|
|
97
|
+
root = self.ports.package_root(launcher)
|
|
98
|
+
rows.append(
|
|
99
|
+
{
|
|
100
|
+
"launcher": str(launcher),
|
|
101
|
+
"resolved": str(launcher.resolve(strict=False)),
|
|
102
|
+
"package_root": str(root) if root else "",
|
|
103
|
+
"version": self.launcher_version(launcher),
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
return rows
|
|
107
|
+
|
|
108
|
+
def warn_if_multiple(self) -> None:
|
|
109
|
+
if self.settings.environ.get("CIEL_RUNTIME_SKIP_INSTALL_DIAGNOSTIC") == "1":
|
|
110
|
+
return
|
|
111
|
+
if not (self.ports.stdin_isatty() and self.ports.stdout_isatty()):
|
|
112
|
+
return
|
|
113
|
+
rows = self.ports.diagnostics()
|
|
114
|
+
roots = {row["package_root"] for row in rows if row.get("package_root")}
|
|
115
|
+
if len(roots) <= 1:
|
|
116
|
+
return
|
|
117
|
+
current_root = str(self.ports.current_root() or "")
|
|
118
|
+
first = rows[0] if rows else {}
|
|
119
|
+
newest = max(
|
|
120
|
+
(row for row in rows if row.get("version")),
|
|
121
|
+
key=lambda row: self.ports.parse_version(row["version"]),
|
|
122
|
+
default=None,
|
|
123
|
+
)
|
|
124
|
+
self.ports.write_error(
|
|
125
|
+
"Ciel Runtime warning: multiple ciel-runtime npm installs are visible."
|
|
126
|
+
)
|
|
127
|
+
if first:
|
|
128
|
+
self.ports.write_error(
|
|
129
|
+
f" shell resolves ciel-runtime to: {first.get('launcher')} "
|
|
130
|
+
f"({first.get('version') or 'unknown version'})"
|
|
131
|
+
)
|
|
132
|
+
if current_root:
|
|
133
|
+
self.ports.write_error(f" current package root: {current_root}")
|
|
134
|
+
if newest and newest is not first:
|
|
135
|
+
self.ports.write_error(
|
|
136
|
+
f" newer visible install: {newest.get('launcher')} "
|
|
137
|
+
f"({newest.get('version')})"
|
|
138
|
+
)
|
|
139
|
+
self.ports.write_error(
|
|
140
|
+
" Fix by keeping one install prefix: update or uninstall the stale "
|
|
141
|
+
"higher-priority install."
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
__all__ = [
|
|
146
|
+
"InstallDiagnosticsPorts",
|
|
147
|
+
"InstallDiagnosticsService",
|
|
148
|
+
"InstallDiagnosticsSettings",
|
|
149
|
+
]
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Official Kimi Code credential and request identity compatibility."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import platform
|
|
8
|
+
import re
|
|
9
|
+
import shutil
|
|
10
|
+
import socket
|
|
11
|
+
import subprocess
|
|
12
|
+
import time
|
|
13
|
+
import uuid
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def code_home(home: Path, environ: dict[str, str] | None = None) -> Path:
|
|
19
|
+
values = os.environ if environ is None else environ
|
|
20
|
+
return Path(values.get("KIMI_CODE_HOME") or (home / ".kimi-code"))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def oauth_token_record(home: Path) -> dict[str, Any] | None:
|
|
24
|
+
try:
|
|
25
|
+
value = json.loads(
|
|
26
|
+
(code_home(home) / "credentials" / "kimi-code.json").read_text(
|
|
27
|
+
encoding="utf-8"
|
|
28
|
+
)
|
|
29
|
+
)
|
|
30
|
+
except (OSError, TypeError, ValueError):
|
|
31
|
+
return None
|
|
32
|
+
return value if isinstance(value, dict) else None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def oauth_access_token(home: Path) -> str | None:
|
|
36
|
+
record = oauth_token_record(home)
|
|
37
|
+
if record is None:
|
|
38
|
+
return None
|
|
39
|
+
token = str(record.get("access_token") or "").strip()
|
|
40
|
+
try:
|
|
41
|
+
expires_at = float(record.get("expires_at") or 0)
|
|
42
|
+
except (TypeError, ValueError):
|
|
43
|
+
expires_at = 0
|
|
44
|
+
if not token or (expires_at > 0 and expires_at <= time.time() + 30):
|
|
45
|
+
return None
|
|
46
|
+
return token
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def oauth_configured(home: Path) -> bool:
|
|
50
|
+
if oauth_access_token(home) is None:
|
|
51
|
+
return False
|
|
52
|
+
try:
|
|
53
|
+
text = (code_home(home) / "config.toml").read_text(encoding="utf-8")
|
|
54
|
+
except OSError:
|
|
55
|
+
return False
|
|
56
|
+
return "managed:kimi-code" in text and ("oauth" in text or 'api_key = ""' in text)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def code_version() -> str:
|
|
60
|
+
cached = getattr(code_version, "_cached", None)
|
|
61
|
+
if isinstance(cached, str) and cached:
|
|
62
|
+
return cached
|
|
63
|
+
executable = shutil.which("kimi")
|
|
64
|
+
version = "unknown"
|
|
65
|
+
if executable:
|
|
66
|
+
result = subprocess.run(
|
|
67
|
+
[executable, "--version"], text=True, capture_output=True, check=False
|
|
68
|
+
)
|
|
69
|
+
match = re.search(
|
|
70
|
+
r"\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?",
|
|
71
|
+
result.stdout or result.stderr,
|
|
72
|
+
)
|
|
73
|
+
if match:
|
|
74
|
+
version = match.group(0)
|
|
75
|
+
setattr(code_version, "_cached", version)
|
|
76
|
+
return version
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def device_id(home: Path) -> str:
|
|
80
|
+
path = code_home(home) / "device_id"
|
|
81
|
+
try:
|
|
82
|
+
existing = path.read_text(encoding="utf-8").strip()
|
|
83
|
+
if existing:
|
|
84
|
+
return existing
|
|
85
|
+
except OSError:
|
|
86
|
+
pass
|
|
87
|
+
value = str(uuid.uuid4())
|
|
88
|
+
path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
89
|
+
try:
|
|
90
|
+
descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
91
|
+
with os.fdopen(descriptor, "w", encoding="utf-8") as stream:
|
|
92
|
+
stream.write(value)
|
|
93
|
+
except FileExistsError:
|
|
94
|
+
return path.read_text(encoding="utf-8").strip()
|
|
95
|
+
return value
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def identity_headers(home: Path) -> dict[str, str]:
|
|
99
|
+
version = code_version()
|
|
100
|
+
system = platform.system()
|
|
101
|
+
os_name = "Windows_NT" if system == "Windows" else "Darwin" if system == "Darwin" else system
|
|
102
|
+
release = platform.release()
|
|
103
|
+
architecture = platform.machine()
|
|
104
|
+
return {
|
|
105
|
+
"User-Agent": f"kimi-code-cli/{version}",
|
|
106
|
+
"X-Msh-Platform": "kimi_code_cli",
|
|
107
|
+
"X-Msh-Version": version,
|
|
108
|
+
"X-Msh-Device-Name": socket.gethostname(),
|
|
109
|
+
"X-Msh-Device-Model": f"{os_name} {release} {architecture}".strip(),
|
|
110
|
+
"X-Msh-Os-Version": release,
|
|
111
|
+
"X-Msh-Device-Id": device_id(home),
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
__all__ = [
|
|
116
|
+
"code_home",
|
|
117
|
+
"code_version",
|
|
118
|
+
"device_id",
|
|
119
|
+
"identity_headers",
|
|
120
|
+
"oauth_access_token",
|
|
121
|
+
"oauth_configured",
|
|
122
|
+
"oauth_token_record",
|
|
123
|
+
]
|