@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,261 @@
|
|
|
1
|
+
"""Build and decode provider-specific Advisor requests without performing I/O."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
ADVISOR_REVIEW_PROMPT = (
|
|
11
|
+
"You are ciel-runtime Advisor, a stronger reviewer model. Review the current task state and provide "
|
|
12
|
+
"concise, actionable guidance for the executor model. Review now; do not say that you will review later. "
|
|
13
|
+
"Do not write code unless a small exact patch is the clearest advice. Use this exact structure: "
|
|
14
|
+
"Verdict: approve, revise, or continue. Key findings: concrete gaps or risks. Required next action: "
|
|
15
|
+
"the next action or Claude Code tool call. Validation: the check that proves the work. "
|
|
16
|
+
"If the executor is stuck after progress announcements, tell it the exact next Claude Code tool to call."
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True, slots=True)
|
|
21
|
+
class AdvisorProjectionPorts:
|
|
22
|
+
provider_kind: Callable[..., str]
|
|
23
|
+
anthropic_messages: Callable[..., tuple[list[dict[str, Any]], list[str]]]
|
|
24
|
+
openai_messages: Callable[..., list[dict[str, Any]]]
|
|
25
|
+
ollama_messages: Callable[[dict[str, Any]], list[dict[str, Any]]]
|
|
26
|
+
focus_from_body: Callable[[dict[str, Any]], str]
|
|
27
|
+
compact_text: Callable[[str], str]
|
|
28
|
+
anthropic_system: Callable[[Any, list[str]], Any]
|
|
29
|
+
anthropic_text: Callable[[Any], str]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True, slots=True)
|
|
33
|
+
class AdvisorBudgetPorts:
|
|
34
|
+
ollama_context: Callable[[dict[str, Any]], int]
|
|
35
|
+
provider_context: Callable[[str, dict[str, Any]], int]
|
|
36
|
+
openai_context: Callable[[str, dict[str, Any]], int]
|
|
37
|
+
reserve: Callable[[dict[str, Any], int], int]
|
|
38
|
+
compact_messages: Callable[..., list[dict[str, Any]]]
|
|
39
|
+
configured_output: Callable[..., int]
|
|
40
|
+
ollama_options: Callable[[dict[str, Any]], dict[str, Any]]
|
|
41
|
+
positive_int: Callable[[Any], int]
|
|
42
|
+
ollama_num_ctx: Callable[..., int]
|
|
43
|
+
think_enabled: Callable[[str | None, dict[str, Any]], bool]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True, slots=True)
|
|
47
|
+
class AdvisorEndpointPorts:
|
|
48
|
+
join_url: Callable[[str, str], str]
|
|
49
|
+
upstream_query: Callable[[dict[str, Any], str, str], str]
|
|
50
|
+
provider_request_base: Callable[[str, dict[str, Any]], str]
|
|
51
|
+
upstream_model: Callable[[str], str]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True, slots=True)
|
|
55
|
+
class AdvisorAnthropicSystemPolicy:
|
|
56
|
+
review_prompt: str
|
|
57
|
+
content_to_text: Callable[[Any], str]
|
|
58
|
+
|
|
59
|
+
def project(
|
|
60
|
+
self,
|
|
61
|
+
system: Any,
|
|
62
|
+
extra_system_texts: list[str] | None = None,
|
|
63
|
+
) -> list[dict[str, Any]]:
|
|
64
|
+
blocks: list[dict[str, Any]] = []
|
|
65
|
+
original_text = ""
|
|
66
|
+
if isinstance(system, str):
|
|
67
|
+
clean = system.strip()
|
|
68
|
+
if clean:
|
|
69
|
+
blocks.append({"type": "text", "text": clean})
|
|
70
|
+
elif isinstance(system, list) and system:
|
|
71
|
+
first = system[0]
|
|
72
|
+
if (
|
|
73
|
+
isinstance(first, dict)
|
|
74
|
+
and str(first.get("type") or "") == "text"
|
|
75
|
+
and str(first.get("text") or "").strip()
|
|
76
|
+
):
|
|
77
|
+
blocks.append(dict(first))
|
|
78
|
+
original_text = self.content_to_text(system[1:]).strip()
|
|
79
|
+
else:
|
|
80
|
+
original_text = self.content_to_text(system).strip()
|
|
81
|
+
elif system:
|
|
82
|
+
original_text = self.content_to_text(system).strip()
|
|
83
|
+
blocks.append({"type": "text", "text": self.review_prompt})
|
|
84
|
+
if original_text:
|
|
85
|
+
blocks.append(
|
|
86
|
+
{
|
|
87
|
+
"type": "text",
|
|
88
|
+
"text": (
|
|
89
|
+
"Original session system context:\n"
|
|
90
|
+
+ original_text
|
|
91
|
+
),
|
|
92
|
+
}
|
|
93
|
+
)
|
|
94
|
+
for text in extra_system_texts or []:
|
|
95
|
+
clean = str(text or "").strip()
|
|
96
|
+
if clean:
|
|
97
|
+
blocks.append(
|
|
98
|
+
{
|
|
99
|
+
"type": "text",
|
|
100
|
+
"text": (
|
|
101
|
+
"Additional system context from message "
|
|
102
|
+
"history:\n" + clean
|
|
103
|
+
),
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
return blocks
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class AdvisorRequestBuilder:
|
|
110
|
+
def __init__(
|
|
111
|
+
self,
|
|
112
|
+
review_prompt: str,
|
|
113
|
+
projection: AdvisorProjectionPorts,
|
|
114
|
+
budget: AdvisorBudgetPorts,
|
|
115
|
+
endpoint: AdvisorEndpointPorts,
|
|
116
|
+
) -> None:
|
|
117
|
+
self.review_prompt = review_prompt
|
|
118
|
+
self.projection = projection
|
|
119
|
+
self.budget = budget
|
|
120
|
+
self.endpoint = endpoint
|
|
121
|
+
|
|
122
|
+
def messages(
|
|
123
|
+
self, provider: str, body: dict[str, Any], focus_override: str = ""
|
|
124
|
+
) -> list[dict[str, Any]]:
|
|
125
|
+
kind = self.projection.provider_kind(provider)
|
|
126
|
+
if kind == "anthropic":
|
|
127
|
+
messages, _ = self.projection.anthropic_messages(body)
|
|
128
|
+
elif kind == "openai-compatible":
|
|
129
|
+
messages = self.projection.openai_messages(body)
|
|
130
|
+
else:
|
|
131
|
+
messages = self.projection.ollama_messages(body)
|
|
132
|
+
focus = focus_override or self.projection.focus_from_body(body)
|
|
133
|
+
if kind != "anthropic":
|
|
134
|
+
messages.insert(0, {"role": "system", "content": self.review_prompt})
|
|
135
|
+
if focus:
|
|
136
|
+
focus_text = f"Advisor focus:\n{self.projection.compact_text(focus)}"
|
|
137
|
+
content: Any = (
|
|
138
|
+
[{"type": "text", "text": focus_text}]
|
|
139
|
+
if kind == "anthropic"
|
|
140
|
+
else focus_text
|
|
141
|
+
)
|
|
142
|
+
messages.append({"role": "user", "content": content})
|
|
143
|
+
return messages
|
|
144
|
+
|
|
145
|
+
def input_budget(self, provider: str, config: dict[str, Any]) -> int:
|
|
146
|
+
kind = self.projection.provider_kind(provider)
|
|
147
|
+
if kind == "ollama":
|
|
148
|
+
context_limit = self.budget.ollama_context(config)
|
|
149
|
+
elif kind == "anthropic":
|
|
150
|
+
context_limit = self.budget.provider_context(provider, config) or 200000
|
|
151
|
+
else:
|
|
152
|
+
context_limit = self.budget.openai_context(provider, config)
|
|
153
|
+
return max(
|
|
154
|
+
8192,
|
|
155
|
+
context_limit - 4096 - self.budget.reserve(config, context_limit),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
def model(self, provider: str, model: str) -> str:
|
|
159
|
+
return self.endpoint.upstream_model(model) if provider == "nvidia-hosted" else model
|
|
160
|
+
|
|
161
|
+
def request(
|
|
162
|
+
self,
|
|
163
|
+
provider: str,
|
|
164
|
+
model: str,
|
|
165
|
+
body: dict[str, Any],
|
|
166
|
+
config: dict[str, Any],
|
|
167
|
+
focus_override: str = "",
|
|
168
|
+
) -> dict[str, Any]:
|
|
169
|
+
kind = self.projection.provider_kind(provider)
|
|
170
|
+
messages = self.messages(provider, body, focus_override)
|
|
171
|
+
if kind != "anthropic":
|
|
172
|
+
messages = self.budget.compact_messages(
|
|
173
|
+
messages,
|
|
174
|
+
[],
|
|
175
|
+
self.input_budget(provider, config),
|
|
176
|
+
provider=provider,
|
|
177
|
+
model=model,
|
|
178
|
+
)
|
|
179
|
+
upstream_model = self.model(provider, model)
|
|
180
|
+
if kind == "anthropic":
|
|
181
|
+
_, extra_system = self.projection.anthropic_messages(body)
|
|
182
|
+
request = {
|
|
183
|
+
"model": upstream_model,
|
|
184
|
+
"system": self.projection.anthropic_system(
|
|
185
|
+
body.get("system"), extra_system
|
|
186
|
+
),
|
|
187
|
+
"messages": messages,
|
|
188
|
+
"stream": False,
|
|
189
|
+
"max_tokens": min(
|
|
190
|
+
4096, self.budget.configured_output(config, body) or 4096
|
|
191
|
+
),
|
|
192
|
+
}
|
|
193
|
+
return self._sampling_options(request, config)
|
|
194
|
+
if kind == "openai-compatible":
|
|
195
|
+
request = {
|
|
196
|
+
"model": upstream_model,
|
|
197
|
+
"messages": messages,
|
|
198
|
+
"stream": False,
|
|
199
|
+
"max_tokens": min(
|
|
200
|
+
4096, self.budget.configured_output(config, body) or 4096
|
|
201
|
+
),
|
|
202
|
+
}
|
|
203
|
+
return self._sampling_options(request, config)
|
|
204
|
+
request = {
|
|
205
|
+
"model": upstream_model,
|
|
206
|
+
"messages": messages,
|
|
207
|
+
"stream": False,
|
|
208
|
+
"think": self.budget.think_enabled(upstream_model, config),
|
|
209
|
+
}
|
|
210
|
+
options = self.budget.ollama_options(config)
|
|
211
|
+
options.setdefault(
|
|
212
|
+
"num_predict",
|
|
213
|
+
min(4096, self.budget.positive_int(options.get("num_predict")) or 4096),
|
|
214
|
+
)
|
|
215
|
+
num_ctx = self.budget.ollama_num_ctx(
|
|
216
|
+
config, {"messages": messages, "tools": []}
|
|
217
|
+
)
|
|
218
|
+
if num_ctx:
|
|
219
|
+
options.setdefault("num_ctx", num_ctx)
|
|
220
|
+
if options:
|
|
221
|
+
request["options"] = options
|
|
222
|
+
return request
|
|
223
|
+
|
|
224
|
+
@staticmethod
|
|
225
|
+
def _sampling_options(
|
|
226
|
+
request: dict[str, Any], config: dict[str, Any]
|
|
227
|
+
) -> dict[str, Any]:
|
|
228
|
+
for key in ("temperature", "top_p"):
|
|
229
|
+
if config.get(key) is not None:
|
|
230
|
+
request[key] = config[key]
|
|
231
|
+
return request
|
|
232
|
+
|
|
233
|
+
def response_text(self, provider: str, data: Any) -> str:
|
|
234
|
+
if not isinstance(data, dict):
|
|
235
|
+
return ""
|
|
236
|
+
kind = self.projection.provider_kind(provider)
|
|
237
|
+
if kind == "anthropic":
|
|
238
|
+
return self.projection.anthropic_text(data.get("content")).strip()
|
|
239
|
+
if kind == "openai-compatible":
|
|
240
|
+
choices = data.get("choices")
|
|
241
|
+
if isinstance(choices, list) and choices:
|
|
242
|
+
message = choices[0].get("message") if isinstance(choices[0], dict) else {}
|
|
243
|
+
if isinstance(message, dict):
|
|
244
|
+
return str(message.get("content") or "").strip()
|
|
245
|
+
return ""
|
|
246
|
+
message = data.get("message")
|
|
247
|
+
return str((message or {}).get("content") or "").strip()
|
|
248
|
+
|
|
249
|
+
def endpoint_url(self, provider: str, config: dict[str, Any]) -> str:
|
|
250
|
+
base = str(config.get("base_url") or "").rstrip("/")
|
|
251
|
+
kind = self.projection.provider_kind(provider)
|
|
252
|
+
if kind == "anthropic":
|
|
253
|
+
url = self.endpoint.join_url(base, "/v1/messages")
|
|
254
|
+
query = self.endpoint.upstream_query(config, "", provider)
|
|
255
|
+
return f"{url}?{query}" if query else url
|
|
256
|
+
if kind == "openai-compatible":
|
|
257
|
+
return self.endpoint.join_url(
|
|
258
|
+
self.endpoint.provider_request_base(provider, config),
|
|
259
|
+
"/v1/chat/completions",
|
|
260
|
+
)
|
|
261
|
+
return self.endpoint.join_url(base, "/api/chat")
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Official manifest-backed AGY installation and update lifecycle."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import platform
|
|
9
|
+
import re
|
|
10
|
+
import shutil
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
import tarfile
|
|
14
|
+
import tempfile
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any, Callable
|
|
17
|
+
import urllib.request
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True, slots=True)
|
|
21
|
+
class AgyInstallerPorts:
|
|
22
|
+
user_bin_dir: Callable[[], Path]
|
|
23
|
+
forced_environment: Callable[[], dict[str, str]]
|
|
24
|
+
find_executable: Callable[[str], str | None]
|
|
25
|
+
version_newer: Callable[[str, str], bool]
|
|
26
|
+
run_upgrade: Callable[..., tuple[int, str]]
|
|
27
|
+
output: Callable[..., None] = print
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True, slots=True)
|
|
31
|
+
class AgyInstaller:
|
|
32
|
+
manifest_base_url: str
|
|
33
|
+
ports: AgyInstallerPorts
|
|
34
|
+
|
|
35
|
+
def manifest_name(self) -> str:
|
|
36
|
+
arch = "arm64" if platform.machine().lower() in ("arm64", "aarch64") else "amd64"
|
|
37
|
+
platform_name = "windows" if os.name == "nt" else ("darwin" if sys.platform == "darwin" else "linux")
|
|
38
|
+
return f"{platform_name}_{arch}.json"
|
|
39
|
+
|
|
40
|
+
def manifest_url(self) -> str:
|
|
41
|
+
override = str(os.environ.get("CIEL_RUNTIME_AGY_MANIFEST_URL") or "").strip()
|
|
42
|
+
return override or f"{self.manifest_base_url}/manifests/{self.manifest_name()}"
|
|
43
|
+
|
|
44
|
+
@staticmethod
|
|
45
|
+
def download_file(url: str, target: Path, timeout: float = 120.0) -> None:
|
|
46
|
+
with urllib.request.urlopen(url, timeout=timeout) as response, target.open("wb") as output:
|
|
47
|
+
shutil.copyfileobj(response, output)
|
|
48
|
+
|
|
49
|
+
def latest_manifest(self, timeout: float = 15.0) -> dict[str, Any] | None:
|
|
50
|
+
try:
|
|
51
|
+
with urllib.request.urlopen(self.manifest_url(), timeout=timeout) as response:
|
|
52
|
+
data = json.loads(response.read(65536).decode("utf-8", errors="replace"))
|
|
53
|
+
if isinstance(data, dict) and data.get("url") and data.get("version"):
|
|
54
|
+
return data
|
|
55
|
+
except Exception as exc:
|
|
56
|
+
self._print(f"AGY manifest check failed ({type(exc).__name__}); continuing.")
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
def current_version(executable: str) -> str:
|
|
61
|
+
try:
|
|
62
|
+
process = subprocess.run([executable, "--version"], text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=8)
|
|
63
|
+
except Exception:
|
|
64
|
+
return ""
|
|
65
|
+
if process.returncode != 0:
|
|
66
|
+
return ""
|
|
67
|
+
match = re.search(r"\d+(?:\.\d+)+(?:[-+][A-Za-z0-9_.-]+)?", process.stdout or "")
|
|
68
|
+
return match.group(0) if match else (process.stdout or "").strip()
|
|
69
|
+
|
|
70
|
+
@staticmethod
|
|
71
|
+
def verify_sha512(path: Path, expected: str) -> bool:
|
|
72
|
+
expected = str(expected or "").strip().lower()
|
|
73
|
+
if not expected:
|
|
74
|
+
return True
|
|
75
|
+
digest = hashlib.sha512()
|
|
76
|
+
with path.open("rb") as stream:
|
|
77
|
+
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
|
78
|
+
digest.update(chunk)
|
|
79
|
+
return digest.hexdigest().lower() == expected
|
|
80
|
+
|
|
81
|
+
def install_from_manifest(self, manifest: dict[str, Any]) -> str | None:
|
|
82
|
+
url = str(manifest.get("url") or "").strip()
|
|
83
|
+
version = str(manifest.get("version") or "").strip()
|
|
84
|
+
checksum = str(manifest.get("sha512") or "").strip()
|
|
85
|
+
if not url:
|
|
86
|
+
self._print("AGY install failed: manifest did not include a download URL.")
|
|
87
|
+
return None
|
|
88
|
+
install_dir = self.ports.user_bin_dir()
|
|
89
|
+
install_dir.mkdir(parents=True, exist_ok=True)
|
|
90
|
+
target = install_dir / ("agy.exe" if os.name == "nt" else "agy")
|
|
91
|
+
suffix = ".exe" if os.name == "nt" else ".tar.gz"
|
|
92
|
+
with tempfile.TemporaryDirectory(prefix="ciel-runtime-agy-") as temporary_dir:
|
|
93
|
+
download = Path(temporary_dir) / f"agy{suffix}"
|
|
94
|
+
self._print(f"Downloading AGY {version or 'latest'} from Google official manifest...")
|
|
95
|
+
self.download_file(url, download)
|
|
96
|
+
if not self.verify_sha512(download, checksum):
|
|
97
|
+
self._print("AGY install failed: sha512 verification did not match manifest.")
|
|
98
|
+
return None
|
|
99
|
+
if os.name == "nt":
|
|
100
|
+
shutil.copy2(download, target)
|
|
101
|
+
elif not self._extract_executable(download, target):
|
|
102
|
+
return None
|
|
103
|
+
try:
|
|
104
|
+
subprocess.run([str(target), "install"], input="y\n", text=True, env=self.ports.forced_environment(), timeout=120, check=False)
|
|
105
|
+
except Exception as exc:
|
|
106
|
+
self._print(f"AGY post-install setup skipped ({type(exc).__name__}); continuing.")
|
|
107
|
+
self._print(f"AGY installed: {target}")
|
|
108
|
+
return str(target)
|
|
109
|
+
|
|
110
|
+
def install_if_missing(self) -> str | None:
|
|
111
|
+
executable = self.ports.find_executable("agy")
|
|
112
|
+
if executable:
|
|
113
|
+
return executable
|
|
114
|
+
if os.environ.get("CIEL_RUNTIME_SKIP_AGY_INSTALL") == "1":
|
|
115
|
+
return None
|
|
116
|
+
manifest = self.latest_manifest()
|
|
117
|
+
if not manifest:
|
|
118
|
+
self._print("AGY executable was not found, and the official AGY manifest could not be read.")
|
|
119
|
+
return None
|
|
120
|
+
return self.install_from_manifest(manifest)
|
|
121
|
+
|
|
122
|
+
def update_check(self, executable: str, enabled: bool = True) -> str:
|
|
123
|
+
if not enabled or os.environ.get("CIEL_RUNTIME_SKIP_AGY_UPDATE") == "1":
|
|
124
|
+
return executable
|
|
125
|
+
self._print("Checking AGY update before launch...")
|
|
126
|
+
current = self.current_version(executable)
|
|
127
|
+
if current:
|
|
128
|
+
self._print(f"Current AGY version: {current}")
|
|
129
|
+
manifest = self.latest_manifest()
|
|
130
|
+
latest = str((manifest or {}).get("version") or "").strip()
|
|
131
|
+
if current and latest and not self.ports.version_newer(latest, current):
|
|
132
|
+
self._print(f"AGY is up to date ({current}).")
|
|
133
|
+
return executable
|
|
134
|
+
self._print(f"AGY update available: {current or 'unknown'} -> {latest}; upgrading automatically." if latest else "AGY update version could not be confirmed; running native updater.")
|
|
135
|
+
return_code, output = self.ports.run_upgrade([executable, "update"], timeout=300)
|
|
136
|
+
if output:
|
|
137
|
+
self._print(output)
|
|
138
|
+
if return_code != 0:
|
|
139
|
+
if manifest and latest and (not current or self.ports.version_newer(latest, current)):
|
|
140
|
+
return self.install_from_manifest(manifest) or executable
|
|
141
|
+
self._print(f"AGY update failed ({return_code}); continuing with current version.")
|
|
142
|
+
return executable
|
|
143
|
+
updated = self.ports.find_executable("agy") or executable
|
|
144
|
+
new_version = self.current_version(updated)
|
|
145
|
+
if new_version:
|
|
146
|
+
self._print(f"AGY version after update: {new_version}")
|
|
147
|
+
return updated
|
|
148
|
+
|
|
149
|
+
def _extract_executable(self, archive_path: Path, target: Path) -> bool:
|
|
150
|
+
with tarfile.open(archive_path, "r:gz") as archive:
|
|
151
|
+
member = next((item for item in archive.getmembers() if Path(item.name).name == "agy" and item.isfile()), None)
|
|
152
|
+
member = member or next((item for item in archive.getmembers() if item.isfile()), None)
|
|
153
|
+
if member is None:
|
|
154
|
+
self._print("AGY install failed: archive did not contain an executable file.")
|
|
155
|
+
return False
|
|
156
|
+
extracted = archive.extractfile(member)
|
|
157
|
+
if extracted is None:
|
|
158
|
+
self._print("AGY install failed: could not extract executable from archive.")
|
|
159
|
+
return False
|
|
160
|
+
with target.open("wb") as output:
|
|
161
|
+
shutil.copyfileobj(extracted, output)
|
|
162
|
+
target.chmod(target.stat().st_mode | 0o755)
|
|
163
|
+
return True
|
|
164
|
+
|
|
165
|
+
def _print(self, message: str) -> None:
|
|
166
|
+
self.ports.output(message, flush=True)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
__all__ = ["AgyInstaller", "AgyInstallerPorts"]
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Restore missing managed MCP servers into AGY's native JSON configuration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import time
|
|
8
|
+
from collections.abc import Callable, Mapping
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from .mcp_inventory import McpInventoryService
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True, slots=True)
|
|
17
|
+
class AgyMcpRestorePorts:
|
|
18
|
+
discover_managed: Callable[[Path], dict[str, dict[str, Any]]]
|
|
19
|
+
log: Callable[[str, str], None]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True, slots=True)
|
|
23
|
+
class AgyMcpRestoreService:
|
|
24
|
+
ports: AgyMcpRestorePorts
|
|
25
|
+
|
|
26
|
+
def restore(
|
|
27
|
+
self,
|
|
28
|
+
env: Mapping[str, str] | None = None,
|
|
29
|
+
cwd: Path | None = None,
|
|
30
|
+
) -> list[str]:
|
|
31
|
+
environment = os.environ if env is None else env
|
|
32
|
+
working_directory = (cwd or Path.cwd()).resolve()
|
|
33
|
+
home = self._home(environment)
|
|
34
|
+
global_path = home / ".gemini" / "antigravity-cli" / "mcp_config.json"
|
|
35
|
+
workspace_path = working_directory / ".agents" / "mcp_config.json"
|
|
36
|
+
try:
|
|
37
|
+
before = self._signature(global_path)
|
|
38
|
+
global_data = self._load(global_path)
|
|
39
|
+
workspace_data = self._load(workspace_path)
|
|
40
|
+
except (OSError, UnicodeError, json.JSONDecodeError, TypeError, ValueError) as exc:
|
|
41
|
+
self.ports.log(
|
|
42
|
+
"WARN",
|
|
43
|
+
"agy_mcp_restore_config_read_failed "
|
|
44
|
+
f"error={type(exc).__name__}: {exc}",
|
|
45
|
+
)
|
|
46
|
+
return []
|
|
47
|
+
|
|
48
|
+
global_servers = self._servers(global_data)
|
|
49
|
+
reserved = set(self._servers(workspace_data))
|
|
50
|
+
projected: dict[str, dict[str, Any]] = {}
|
|
51
|
+
for name, server in self.ports.discover_managed(working_directory).items():
|
|
52
|
+
normalized = self._project_server(server)
|
|
53
|
+
if normalized is None:
|
|
54
|
+
self.ports.log(
|
|
55
|
+
"WARN", f"agy_mcp_restore_skipped_unsupported server={name}"
|
|
56
|
+
)
|
|
57
|
+
continue
|
|
58
|
+
projected[name] = normalized
|
|
59
|
+
merged = McpInventoryService.merge(
|
|
60
|
+
global_servers, projected, reserved=reserved
|
|
61
|
+
)
|
|
62
|
+
if not merged.added:
|
|
63
|
+
return []
|
|
64
|
+
restored = dict(global_data)
|
|
65
|
+
restored["mcpServers"] = merged.servers
|
|
66
|
+
try:
|
|
67
|
+
self._atomic_replace(global_path, restored, before)
|
|
68
|
+
except (OSError, UnicodeError, TypeError, ValueError, RuntimeError) as exc:
|
|
69
|
+
self.ports.log(
|
|
70
|
+
"WARN",
|
|
71
|
+
"agy_mcp_restore_write_failed "
|
|
72
|
+
f"path={global_path} error={type(exc).__name__}: {exc}",
|
|
73
|
+
)
|
|
74
|
+
return []
|
|
75
|
+
names = list(merged.added)
|
|
76
|
+
self.ports.log(
|
|
77
|
+
"INFO",
|
|
78
|
+
"agy_mcp_config_restored "
|
|
79
|
+
f"path={global_path} servers={','.join(sorted(names))}",
|
|
80
|
+
)
|
|
81
|
+
return names
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
def _home(env: Mapping[str, str]) -> Path:
|
|
85
|
+
configured = str(env.get("HOME") or env.get("USERPROFILE") or "").strip()
|
|
86
|
+
return Path(configured).expanduser() if configured else Path.home()
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
def _signature(path: Path) -> tuple[int, int, int] | None:
|
|
90
|
+
try:
|
|
91
|
+
stat = path.stat()
|
|
92
|
+
except FileNotFoundError:
|
|
93
|
+
return None
|
|
94
|
+
return stat.st_mtime_ns, stat.st_size, getattr(stat, "st_ino", 0)
|
|
95
|
+
|
|
96
|
+
@staticmethod
|
|
97
|
+
def _load(path: Path) -> dict[str, Any]:
|
|
98
|
+
if not path.is_file():
|
|
99
|
+
return {}
|
|
100
|
+
value = json.loads(path.read_text(encoding="utf-8"))
|
|
101
|
+
if not isinstance(value, dict):
|
|
102
|
+
raise ValueError(f"AGY MCP config must be a JSON object: {path}")
|
|
103
|
+
servers = value.get("mcpServers", {})
|
|
104
|
+
if not isinstance(servers, dict):
|
|
105
|
+
raise ValueError(f"AGY mcpServers must be a JSON object: {path}")
|
|
106
|
+
return value
|
|
107
|
+
|
|
108
|
+
@staticmethod
|
|
109
|
+
def _servers(data: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
|
110
|
+
value = data.get("mcpServers", {})
|
|
111
|
+
return {
|
|
112
|
+
str(name): dict(server)
|
|
113
|
+
for name, server in value.items()
|
|
114
|
+
if str(name).strip() and isinstance(server, dict)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
@staticmethod
|
|
118
|
+
def _project_server(server: dict[str, Any]) -> dict[str, Any] | None:
|
|
119
|
+
command = str(server.get("command") or "").strip()
|
|
120
|
+
url = str(
|
|
121
|
+
server.get("serverUrl")
|
|
122
|
+
or server.get("serverURL")
|
|
123
|
+
or server.get("url")
|
|
124
|
+
or server.get("endpoint")
|
|
125
|
+
or ""
|
|
126
|
+
).strip()
|
|
127
|
+
args = server.get("args")
|
|
128
|
+
arguments = [str(item) for item in args] if isinstance(args, list) else []
|
|
129
|
+
if "mcp-proxy" in arguments and "--server-config" in arguments:
|
|
130
|
+
return None
|
|
131
|
+
if command:
|
|
132
|
+
projected: dict[str, Any] = {"command": command}
|
|
133
|
+
elif url.startswith(("http://", "https://")):
|
|
134
|
+
projected = {"serverUrl": url}
|
|
135
|
+
else:
|
|
136
|
+
return None
|
|
137
|
+
aliases = {
|
|
138
|
+
"http_headers": "headers",
|
|
139
|
+
"env_http_headers": "headers",
|
|
140
|
+
"type": "transport",
|
|
141
|
+
}
|
|
142
|
+
supported = (
|
|
143
|
+
"args", "env", "headers", "transport", "authProviderType",
|
|
144
|
+
)
|
|
145
|
+
normalized = dict(server)
|
|
146
|
+
for source, target in aliases.items():
|
|
147
|
+
if target not in normalized and source in normalized:
|
|
148
|
+
normalized[target] = normalized[source]
|
|
149
|
+
for key in supported:
|
|
150
|
+
value = normalized.get(key)
|
|
151
|
+
if isinstance(value, (str, bool, int, float, list, dict)):
|
|
152
|
+
projected[key] = value
|
|
153
|
+
return projected
|
|
154
|
+
|
|
155
|
+
@classmethod
|
|
156
|
+
def _atomic_replace(
|
|
157
|
+
cls,
|
|
158
|
+
path: Path,
|
|
159
|
+
value: dict[str, Any],
|
|
160
|
+
expected: tuple[int, int, int] | None,
|
|
161
|
+
) -> None:
|
|
162
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
163
|
+
if cls._signature(path) != expected:
|
|
164
|
+
raise RuntimeError("AGY config changed while MCP restoration was running")
|
|
165
|
+
temporary = path.with_name(f"{path.name}.{os.getpid()}.{time.time_ns()}.tmp")
|
|
166
|
+
try:
|
|
167
|
+
temporary.write_text(
|
|
168
|
+
json.dumps(value, indent=2, ensure_ascii=False) + "\n",
|
|
169
|
+
encoding="utf-8",
|
|
170
|
+
)
|
|
171
|
+
os.chmod(temporary, 0o600)
|
|
172
|
+
if cls._signature(path) != expected:
|
|
173
|
+
raise RuntimeError("AGY config changed while MCP restoration was running")
|
|
174
|
+
temporary.replace(path)
|
|
175
|
+
finally:
|
|
176
|
+
try:
|
|
177
|
+
temporary.unlink()
|
|
178
|
+
except FileNotFoundError:
|
|
179
|
+
pass
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
__all__ = ["AgyMcpRestorePorts", "AgyMcpRestoreService"]
|