alysis-code 0.13.0__py3-none-any.whl

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 (458) hide show
  1. alysis_code/__init__.py +3 -0
  2. alysis_code/__main__.py +4 -0
  3. alysis_code/_build_info.py +14 -0
  4. alysis_code/account_login.py +468 -0
  5. alysis_code/agent/README.md +35 -0
  6. alysis_code/agent/__init__.py +11 -0
  7. alysis_code/agent/acceptance_contract.py +2217 -0
  8. alysis_code/agent/blast_radius.py +1403 -0
  9. alysis_code/agent/cache_keepalive.py +227 -0
  10. alysis_code/agent/completion_certificate.py +366 -0
  11. alysis_code/agent/completion_gate.py +306 -0
  12. alysis_code/agent/empty_response_stall.py +403 -0
  13. alysis_code/agent/errors.py +28 -0
  14. alysis_code/agent/llm_calls.py +475 -0
  15. alysis_code/agent/mutation_classification.py +227 -0
  16. alysis_code/agent/prompt_context.py +2508 -0
  17. alysis_code/agent/read_ledger.py +253 -0
  18. alysis_code/agent/regression_baseline.py +642 -0
  19. alysis_code/agent/reproduction_first.py +610 -0
  20. alysis_code/agent/sensitive_output.py +629 -0
  21. alysis_code/agent/session.py +3218 -0
  22. alysis_code/agent/steering.py +191 -0
  23. alysis_code/agent/subagent_execution.py +5177 -0
  24. alysis_code/agent/subagent_workspace.py +666 -0
  25. alysis_code/agent/tools_assembly.py +4728 -0
  26. alysis_code/agent/turn/__init__.py +101 -0
  27. alysis_code/agent/turn/core.py +8483 -0
  28. alysis_code/agent/turn/events.py +113 -0
  29. alysis_code/agent/turn/exploration.py +590 -0
  30. alysis_code/agent/turn/interventions.py +65 -0
  31. alysis_code/agent/turn/read_cache.py +420 -0
  32. alysis_code/agent/turn/snapshot.py +179 -0
  33. alysis_code/agent/turn_contract.py +661 -0
  34. alysis_code/agent/turn_path.py +129 -0
  35. alysis_code/agent/verification.py +2885 -0
  36. alysis_code/agent/verification_commands.py +512 -0
  37. alysis_code/agent/verification_evidence.py +738 -0
  38. alysis_code/agent_loop.py +693 -0
  39. alysis_code/agent_runtimes/__init__.py +51 -0
  40. alysis_code/agent_runtimes/base.py +114 -0
  41. alysis_code/agent_runtimes/builtins.py +129 -0
  42. alysis_code/agent_runtimes/codex_cli.py +664 -0
  43. alysis_code/agent_runtimes/host.py +263 -0
  44. alysis_code/agent_runtimes/registry.py +64 -0
  45. alysis_code/agent_runtimes/service.py +150 -0
  46. alysis_code/agentbox_client.py +416 -0
  47. alysis_code/agentbox_integration.py +310 -0
  48. alysis_code/alysis_cloud.py +152 -0
  49. alysis_code/approval_scope.py +276 -0
  50. alysis_code/assets/README.md +33 -0
  51. alysis_code/assets/__init__.py +126 -0
  52. alysis_code/assets/asset_read_core.py +281 -0
  53. alysis_code/assets/budget_allocator.py +456 -0
  54. alysis_code/assets/comprehender.py +759 -0
  55. alysis_code/assets/index.py +654 -0
  56. alysis_code/assets/ingestion.py +275 -0
  57. alysis_code/assets/legacy_migration.py +413 -0
  58. alysis_code/assets/models.py +263 -0
  59. alysis_code/assets/ocr.py +239 -0
  60. alysis_code/assets/owl/ascii/f-000.txt +13 -0
  61. alysis_code/assets/owl/ascii/f-001.txt +13 -0
  62. alysis_code/assets/owl/ascii/f-002.txt +13 -0
  63. alysis_code/assets/owl/ascii/f-003.txt +13 -0
  64. alysis_code/assets/owl/ascii/f-004.txt +13 -0
  65. alysis_code/assets/owl/ascii/f-005.txt +13 -0
  66. alysis_code/assets/owl/ascii/f-006.txt +13 -0
  67. alysis_code/assets/owl/ascii/f-007.txt +13 -0
  68. alysis_code/assets/owl/ascii/f-008.txt +13 -0
  69. alysis_code/assets/owl/ascii/f-009.txt +13 -0
  70. alysis_code/assets/owl/ascii/f-010.txt +13 -0
  71. alysis_code/assets/owl/ascii/f-011.txt +13 -0
  72. alysis_code/assets/owl/ascii/f-012.txt +13 -0
  73. alysis_code/assets/owl/ascii/f-013.txt +13 -0
  74. alysis_code/assets/owl/ascii/f-014.txt +13 -0
  75. alysis_code/assets/owl/ascii/f-015.txt +13 -0
  76. alysis_code/assets/owl/ascii/f-016.txt +13 -0
  77. alysis_code/assets/owl/ascii/f-017.txt +13 -0
  78. alysis_code/assets/owl/ascii/f-018.txt +13 -0
  79. alysis_code/assets/owl/ascii/f-019.txt +13 -0
  80. alysis_code/assets/owl/ascii/f-020.txt +13 -0
  81. alysis_code/assets/owl/index.html +98 -0
  82. alysis_code/assets/owl/show-owl.sh +761 -0
  83. alysis_code/assets/paths.py +49 -0
  84. alysis_code/assets/plan_binding.py +326 -0
  85. alysis_code/assets/planner_context.py +466 -0
  86. alysis_code/assets/planner_tools.py +184 -0
  87. alysis_code/assets/prompts.py +101 -0
  88. alysis_code/assets/replanner_context.py +239 -0
  89. alysis_code/assets/surface.py +521 -0
  90. alysis_code/assets/untrusted_content.py +48 -0
  91. alysis_code/assets/usage_logger.py +94 -0
  92. alysis_code/assets/worker_mirror.py +428 -0
  93. alysis_code/assets/worker_section.py +303 -0
  94. alysis_code/assets/worker_tools.py +468 -0
  95. alysis_code/atomic_io.py +83 -0
  96. alysis_code/auth_diagnostics.py +272 -0
  97. alysis_code/background_runner.py +366 -0
  98. alysis_code/branding.py +270 -0
  99. alysis_code/budget_policy.py +390 -0
  100. alysis_code/build_identity.py +465 -0
  101. alysis_code/builtin_hooks/__init__.py +7 -0
  102. alysis_code/builtin_hooks/notify_done_windows.py +65 -0
  103. alysis_code/bwrap_etc.py +76 -0
  104. alysis_code/cancellation.py +41 -0
  105. alysis_code/capabilities.py +137 -0
  106. alysis_code/chatgpt_codex_static_provider.py +133 -0
  107. alysis_code/cli.py +51 -0
  108. alysis_code/cli_impl/__init__.py +1 -0
  109. alysis_code/cli_impl/assets_cli.py +537 -0
  110. alysis_code/cli_impl/assets_modal.py +412 -0
  111. alysis_code/cli_impl/chat/__init__.py +156 -0
  112. alysis_code/cli_impl/chat/commands.py +2616 -0
  113. alysis_code/cli_impl/chat/loop.py +4508 -0
  114. alysis_code/cli_impl/chat/mid_turn_policy.py +125 -0
  115. alysis_code/cli_impl/chat/rendering.py +444 -0
  116. alysis_code/cli_impl/chat/state.py +124 -0
  117. alysis_code/cli_impl/chat_resume.py +830 -0
  118. alysis_code/cli_impl/chat_slash_completer.py +258 -0
  119. alysis_code/cli_impl/commands/__init__.py +11 -0
  120. alysis_code/cli_impl/commands/_shared.py +89 -0
  121. alysis_code/cli_impl/commands/auth.py +623 -0
  122. alysis_code/cli_impl/commands/chat_resume_helpers.py +1531 -0
  123. alysis_code/cli_impl/commands/chat_state.py +158 -0
  124. alysis_code/cli_impl/commands/chat_status.py +1248 -0
  125. alysis_code/cli_impl/commands/chat_terminal.py +942 -0
  126. alysis_code/cli_impl/commands/chat_tui_panels.py +1018 -0
  127. alysis_code/cli_impl/commands/cli_common.py +1223 -0
  128. alysis_code/cli_impl/commands/cli_surface.py +77 -0
  129. alysis_code/cli_impl/commands/config.py +131 -0
  130. alysis_code/cli_impl/commands/conventions.py +85 -0
  131. alysis_code/cli_impl/commands/execution_helpers.py +350 -0
  132. alysis_code/cli_impl/commands/extensions.py +401 -0
  133. alysis_code/cli_impl/commands/forge.py +1282 -0
  134. alysis_code/cli_impl/commands/forge_asset_view.py +121 -0
  135. alysis_code/cli_impl/commands/forge_helpers.py +1215 -0
  136. alysis_code/cli_impl/commands/hooks.py +737 -0
  137. alysis_code/cli_impl/commands/ide_bridge.py +31 -0
  138. alysis_code/cli_impl/commands/mcp.py +700 -0
  139. alysis_code/cli_impl/commands/profile.py +453 -0
  140. alysis_code/cli_impl/commands/prompt_helpers.py +307 -0
  141. alysis_code/cli_impl/commands/report.py +88 -0
  142. alysis_code/cli_impl/commands/root.py +1118 -0
  143. alysis_code/cli_impl/commands/sandbox.py +184 -0
  144. alysis_code/cli_impl/commands/server.py +54 -0
  145. alysis_code/cli_impl/commands/sessions.py +252 -0
  146. alysis_code/cli_impl/commands/skills.py +404 -0
  147. alysis_code/cli_impl/commands/startup.py +946 -0
  148. alysis_code/cli_impl/commands/tools.py +335 -0
  149. alysis_code/cli_impl/commands/update.py +364 -0
  150. alysis_code/cli_impl/commands/welcome.py +972 -0
  151. alysis_code/cli_impl/config_menu.py +3882 -0
  152. alysis_code/cli_impl/forge.py +4509 -0
  153. alysis_code/cli_impl/forge_recovery.py +485 -0
  154. alysis_code/cli_impl/setup_wizard.py +2409 -0
  155. alysis_code/cli_impl/tui/__init__.py +58 -0
  156. alysis_code/cli_impl/tui/app.py +4551 -0
  157. alysis_code/cli_impl/tui/config.py +32 -0
  158. alysis_code/cli_impl/tui/config_flow.py +2754 -0
  159. alysis_code/cli_impl/tui/config_overlay.py +566 -0
  160. alysis_code/cli_impl/tui/content.py +78 -0
  161. alysis_code/cli_impl/tui/footer.py +218 -0
  162. alysis_code/cli_impl/tui/forge_status.py +136 -0
  163. alysis_code/cli_impl/tui/markdown.py +244 -0
  164. alysis_code/cli_impl/tui/owl.py +109 -0
  165. alysis_code/cli_impl/tui/plan_meta.py +477 -0
  166. alysis_code/cli_impl/tui/setup_app.py +519 -0
  167. alysis_code/cli_impl/tui/setup_flow.py +1622 -0
  168. alysis_code/cli_impl/tui/state.py +101 -0
  169. alysis_code/cli_impl/tui/subagent_identity.py +66 -0
  170. alysis_code/cli_impl/tui/subagent_panel.py +186 -0
  171. alysis_code/cli_impl/tui/surface.py +796 -0
  172. alysis_code/cli_impl/tui/transcript.py +514 -0
  173. alysis_code/cli_impl/tui/update_prompt.py +79 -0
  174. alysis_code/cli_impl/tui/workspace_guard.py +384 -0
  175. alysis_code/clipboard.py +172 -0
  176. alysis_code/code_review.py +1211 -0
  177. alysis_code/compaction/__init__.py +28 -0
  178. alysis_code/compaction/conversation_compactor.py +2932 -0
  179. alysis_code/compaction/importance.py +177 -0
  180. alysis_code/compaction/settings.py +297 -0
  181. alysis_code/compaction/tool_output_offload.py +447 -0
  182. alysis_code/config.py +3509 -0
  183. alysis_code/conflict_auto_resolver.py +895 -0
  184. alysis_code/context/__init__.py +1 -0
  185. alysis_code/context/tool_schema_budgeter.py +220 -0
  186. alysis_code/crash_diagnostics.py +282 -0
  187. alysis_code/custom_tools/README.md +34 -0
  188. alysis_code/custom_tools/__init__.py +43 -0
  189. alysis_code/custom_tools/discovery.py +903 -0
  190. alysis_code/custom_tools/runtime.py +1516 -0
  191. alysis_code/custom_tools/session.py +227 -0
  192. alysis_code/custom_tools/trust.py +232 -0
  193. alysis_code/diff_paths.py +113 -0
  194. alysis_code/direction_change.py +293 -0
  195. alysis_code/dispatch_timing.py +306 -0
  196. alysis_code/durable_service_manager.py +1236 -0
  197. alysis_code/edit_discipline.py +659 -0
  198. alysis_code/error_text.py +73 -0
  199. alysis_code/execution_budget.py +411 -0
  200. alysis_code/execution_context.py +915 -0
  201. alysis_code/execution_deadline.py +1065 -0
  202. alysis_code/execution_shared.py +1904 -0
  203. alysis_code/extensions/README.md +30 -0
  204. alysis_code/extensions/__init__.py +93 -0
  205. alysis_code/extensions/activation.py +138 -0
  206. alysis_code/extensions/install.py +1436 -0
  207. alysis_code/extensions/manifest.py +487 -0
  208. alysis_code/extensions/models.py +74 -0
  209. alysis_code/extensions/paths.py +56 -0
  210. alysis_code/extensions/registry.json +4 -0
  211. alysis_code/extensions/registry.py +52 -0
  212. alysis_code/extensions/state.py +83 -0
  213. alysis_code/extensions/workspace_trust.py +101 -0
  214. alysis_code/failed_task_evidence.py +369 -0
  215. alysis_code/failure_category.py +315 -0
  216. alysis_code/feedback_report.py +1647 -0
  217. alysis_code/file_classification.py +485 -0
  218. alysis_code/forge.py +2064 -0
  219. alysis_code/forge_completion.py +362 -0
  220. alysis_code/forge_events.py +475 -0
  221. alysis_code/frontmatter_utils.py +95 -0
  222. alysis_code/git_evidence.py +1181 -0
  223. alysis_code/git_ops.py +560 -0
  224. alysis_code/git_safe.py +62 -0
  225. alysis_code/git_worktrees.py +190 -0
  226. alysis_code/hooks/README.md +33 -0
  227. alysis_code/hooks/__init__.py +67 -0
  228. alysis_code/hooks/audit.py +171 -0
  229. alysis_code/hooks/config.py +225 -0
  230. alysis_code/hooks/dispatcher.py +1110 -0
  231. alysis_code/hooks/models.py +447 -0
  232. alysis_code/hooks/trust.py +202 -0
  233. alysis_code/host_actions.py +543 -0
  234. alysis_code/host_browser.py +103 -0
  235. alysis_code/ide/__init__.py +5 -0
  236. alysis_code/ide/activity_events.py +399 -0
  237. alysis_code/ide/approvals.py +337 -0
  238. alysis_code/ide/artifacts.py +153 -0
  239. alysis_code/ide/browser_egress_proxy.py +1076 -0
  240. alysis_code/ide/cdp_websocket_transport.py +1192 -0
  241. alysis_code/ide/change_ledger.py +1721 -0
  242. alysis_code/ide/context_blocks.py +979 -0
  243. alysis_code/ide/event_stream.py +531 -0
  244. alysis_code/ide/forge_protocol.py +3112 -0
  245. alysis_code/ide/forge_request_ledger.py +737 -0
  246. alysis_code/ide/health.py +965 -0
  247. alysis_code/ide/managed_browser.py +2251 -0
  248. alysis_code/ide/management_protocol.py +3414 -0
  249. alysis_code/ide/mcp_oauth_coordinator.py +744 -0
  250. alysis_code/ide/mcp_oauth_lifecycle.py +1504 -0
  251. alysis_code/ide/prompt_queue.py +1070 -0
  252. alysis_code/ide/protocol.py +191 -0
  253. alysis_code/ide/resumable_swarm.py +1543 -0
  254. alysis_code/ide/session_search.py +295 -0
  255. alysis_code/ide/stdio_bridge.py +9935 -0
  256. alysis_code/ide/structured_state.py +1579 -0
  257. alysis_code/ide/swarm_protocol.py +816 -0
  258. alysis_code/integration_gate.py +506 -0
  259. alysis_code/interactive_input_guard.py +39 -0
  260. alysis_code/interactive_plan_mode.py +26 -0
  261. alysis_code/internal_artifacts.py +179 -0
  262. alysis_code/knowledge_base.py +1409 -0
  263. alysis_code/knowledge_capture.py +1190 -0
  264. alysis_code/knowledge_librarian.py +605 -0
  265. alysis_code/language_policy.py +34 -0
  266. alysis_code/litellm_static_provider.py +535 -0
  267. alysis_code/llm/__init__.py +1 -0
  268. alysis_code/llm/anthropic_messages.py +2288 -0
  269. alysis_code/llm/base.py +71 -0
  270. alysis_code/llm/cache_capabilities.py +985 -0
  271. alysis_code/llm/cache_control_blocks.py +244 -0
  272. alysis_code/llm/cache_policy.py +388 -0
  273. alysis_code/llm/factory.py +373 -0
  274. alysis_code/llm/gemini_generate_content.py +2652 -0
  275. alysis_code/llm/gemini_interactions.py +739 -0
  276. alysis_code/llm/metadata.py +450 -0
  277. alysis_code/llm/openai_compat.py +2947 -0
  278. alysis_code/llm/openai_responses.py +2604 -0
  279. alysis_code/llm/protocols.py +609 -0
  280. alysis_code/llm/provider_limits.py +525 -0
  281. alysis_code/llm/request_plan.py +389 -0
  282. alysis_code/llm/request_shape.py +238 -0
  283. alysis_code/llm/streaming.py +108 -0
  284. alysis_code/llm/temperature_compat.py +78 -0
  285. alysis_code/llm/types.py +195 -0
  286. alysis_code/llm/usage_normalization.py +222 -0
  287. alysis_code/llm_error_display.py +315 -0
  288. alysis_code/logging_redaction.py +326 -0
  289. alysis_code/managed_host_deadline.py +196 -0
  290. alysis_code/mcp/README.md +33 -0
  291. alysis_code/mcp/__init__.py +24 -0
  292. alysis_code/mcp/client.py +1137 -0
  293. alysis_code/mcp/config.py +597 -0
  294. alysis_code/mcp/errors.py +113 -0
  295. alysis_code/mcp/forge_scope.py +154 -0
  296. alysis_code/mcp/jsonrpc.py +214 -0
  297. alysis_code/mcp/manager.py +2308 -0
  298. alysis_code/mcp/models.py +666 -0
  299. alysis_code/mcp/oauth.py +972 -0
  300. alysis_code/mcp/oauth_runtime.py +310 -0
  301. alysis_code/mcp/oauth_store.py +276 -0
  302. alysis_code/mcp/prompts.py +329 -0
  303. alysis_code/mcp/resources.py +295 -0
  304. alysis_code/mcp/roots.py +106 -0
  305. alysis_code/mcp/server_requests.py +75 -0
  306. alysis_code/mcp/token_store.py +859 -0
  307. alysis_code/mcp/transport_http.py +1338 -0
  308. alysis_code/mcp/transport_stdio.py +1267 -0
  309. alysis_code/mcp/untrusted_content.py +119 -0
  310. alysis_code/merge_conflict_reviewer.py +729 -0
  311. alysis_code/model_catalog/__init__.py +1 -0
  312. alysis_code/model_catalog/chatgpt_codex_subscription_snapshot.json +186 -0
  313. alysis_code/model_catalog/litellm_model_prices_snapshot.json +44715 -0
  314. alysis_code/model_catalog/litellm_model_prices_snapshot.meta.json +17 -0
  315. alysis_code/model_metadata_policy.py +223 -0
  316. alysis_code/model_metadata_utils.py +103 -0
  317. alysis_code/model_registry.py +1420 -0
  318. alysis_code/model_router.py +147 -0
  319. alysis_code/permission_policy.py +1016 -0
  320. alysis_code/personas.py +451 -0
  321. alysis_code/pipeline_facts.py +233 -0
  322. alysis_code/plan_assistant.py +4763 -0
  323. alysis_code/plan_mode.py +393 -0
  324. alysis_code/plan_reconciliation.py +1228 -0
  325. alysis_code/plan_repair.py +652 -0
  326. alysis_code/plan_validation.py +1099 -0
  327. alysis_code/planning_constraints.py +904 -0
  328. alysis_code/policy.py +95 -0
  329. alysis_code/preview_server.py +457 -0
  330. alysis_code/process_reaping.py +566 -0
  331. alysis_code/profile_presets.py +1834 -0
  332. alysis_code/profiles.py +666 -0
  333. alysis_code/provider_auth/__init__.py +29 -0
  334. alysis_code/provider_auth/base.py +99 -0
  335. alysis_code/provider_auth/openai_codex.py +951 -0
  336. alysis_code/provider_auth/registry.py +76 -0
  337. alysis_code/provider_auth/store.py +125 -0
  338. alysis_code/provider_diagnostics.py +1209 -0
  339. alysis_code/provider_model_catalog.py +685 -0
  340. alysis_code/provider_telemetry.py +1699 -0
  341. alysis_code/provider_url.py +75 -0
  342. alysis_code/reasoning_contracts.py +911 -0
  343. alysis_code/remote_sync.py +350 -0
  344. alysis_code/replanning.py +1195 -0
  345. alysis_code/repo_scan.py +1152 -0
  346. alysis_code/request_estimation.py +296 -0
  347. alysis_code/review_gate.py +617 -0
  348. alysis_code/run_lock.py +1141 -0
  349. alysis_code/run_outcome.py +58 -0
  350. alysis_code/run_provenance.py +774 -0
  351. alysis_code/run_state.py +445 -0
  352. alysis_code/runtime_artifacts.py +116 -0
  353. alysis_code/runtime_context_features.py +78 -0
  354. alysis_code/runtime_kind.py +52 -0
  355. alysis_code/safety/__init__.py +11 -0
  356. alysis_code/safety/mcp_sanitize.py +29 -0
  357. alysis_code/safety/safe_http.py +297 -0
  358. alysis_code/safety/subagent_report.py +184 -0
  359. alysis_code/sandbox_doctor.py +682 -0
  360. alysis_code/sandbox_runner.py +1025 -0
  361. alysis_code/sandbox_settings.py +423 -0
  362. alysis_code/serialized_paths.py +355 -0
  363. alysis_code/server/__init__.py +3 -0
  364. alysis_code/server/app.py +367 -0
  365. alysis_code/server/auth.py +34 -0
  366. alysis_code/server/job_config.py +30 -0
  367. alysis_code/server/settings.py +215 -0
  368. alysis_code/server/store.py +193 -0
  369. alysis_code/server/worker_runner.py +657 -0
  370. alysis_code/service_persistence.py +355 -0
  371. alysis_code/session_artifacts.py +108 -0
  372. alysis_code/session_metrics.py +331 -0
  373. alysis_code/session_store.py +624 -0
  374. alysis_code/skills/README.md +34 -0
  375. alysis_code/skills/__init__.py +104 -0
  376. alysis_code/skills/conventions.py +84 -0
  377. alysis_code/skills/discovery.py +176 -0
  378. alysis_code/skills/eval_models.py +232 -0
  379. alysis_code/skills/eval_runner.py +372 -0
  380. alysis_code/skills/evals.py +1344 -0
  381. alysis_code/skills/install.py +293 -0
  382. alysis_code/skills/loader.py +118 -0
  383. alysis_code/skills/matching.py +103 -0
  384. alysis_code/skills/models.py +71 -0
  385. alysis_code/skills/paths.py +56 -0
  386. alysis_code/skills/prompting.py +500 -0
  387. alysis_code/skills/scaffold.py +142 -0
  388. alysis_code/skills/state.py +441 -0
  389. alysis_code/skills/transactions.py +125 -0
  390. alysis_code/skills/validation.py +304 -0
  391. alysis_code/step_budget.py +238 -0
  392. alysis_code/subagent_labels.py +49 -0
  393. alysis_code/subagents.py +1072 -0
  394. alysis_code/surface/__init__.py +80 -0
  395. alysis_code/surface/base.py +305 -0
  396. alysis_code/surface/console.py +387 -0
  397. alysis_code/surface/events.py +372 -0
  398. alysis_code/surface/hidden_surface.py +529 -0
  399. alysis_code/surface/noop_surface.py +219 -0
  400. alysis_code/surface/rich_surface.py +1555 -0
  401. alysis_code/surface/styles.py +67 -0
  402. alysis_code/surface/theme.py +455 -0
  403. alysis_code/surface/types.py +100 -0
  404. alysis_code/swarm_backend.py +926 -0
  405. alysis_code/swarm_orchestrator.py +4020 -0
  406. alysis_code/swarm_scheduler.py +441 -0
  407. alysis_code/swarm_trace.py +429 -0
  408. alysis_code/swarm_worker.py +2119 -0
  409. alysis_code/swarm_write_guard.py +348 -0
  410. alysis_code/task_dependencies.py +170 -0
  411. alysis_code/task_readiness.py +992 -0
  412. alysis_code/task_scope.py +2148 -0
  413. alysis_code/terminal_manager.py +762 -0
  414. alysis_code/terminal_ownership.py +460 -0
  415. alysis_code/text_normalization.py +30 -0
  416. alysis_code/token_budget.py +97 -0
  417. alysis_code/tools/README.md +34 -0
  418. alysis_code/tools/__init__.py +1 -0
  419. alysis_code/tools/artifacts.py +127 -0
  420. alysis_code/tools/availability.py +188 -0
  421. alysis_code/tools/fs.py +1456 -0
  422. alysis_code/tools/git.py +461 -0
  423. alysis_code/tools/history.py +229 -0
  424. alysis_code/tools/http_timeout.py +78 -0
  425. alysis_code/tools/image_generation.py +552 -0
  426. alysis_code/tools/registry.py +2936 -0
  427. alysis_code/tools/repo_map.py +476 -0
  428. alysis_code/tools/search.py +563 -0
  429. alysis_code/tools/shell.py +135 -0
  430. alysis_code/tools/symbols.py +1350 -0
  431. alysis_code/tools/test_discovery.py +643 -0
  432. alysis_code/tools/web.py +482 -0
  433. alysis_code/tools/web_search.py +2012 -0
  434. alysis_code/tools/web_search_dashscope.py +557 -0
  435. alysis_code/tools/web_search_ddgs.py +221 -0
  436. alysis_code/tools/web_search_provider_adapters.py +1429 -0
  437. alysis_code/tools/web_search_tavily.py +194 -0
  438. alysis_code/updates.py +933 -0
  439. alysis_code/usage_tracker.py +1990 -0
  440. alysis_code/verification_command_analysis.py +1004 -0
  441. alysis_code/verification_contract.py +574 -0
  442. alysis_code/verification_failure_summary.py +273 -0
  443. alysis_code/verification_repair.py +385 -0
  444. alysis_code/verify_gate.py +3129 -0
  445. alysis_code/web_research.py +1872 -0
  446. alysis_code/web_search_adapters.py +66 -0
  447. alysis_code/web_search_policy.py +27 -0
  448. alysis_code/workspace_binding.py +389 -0
  449. alysis_code/workspace_binding_ui.py +408 -0
  450. alysis_code/workspace_context.py +273 -0
  451. alysis_code/workspace_isolation.py +138 -0
  452. alysis_code/workspace_provisioning.py +455 -0
  453. alysis_code-0.13.0.dist-info/METADATA +507 -0
  454. alysis_code-0.13.0.dist-info/RECORD +458 -0
  455. alysis_code-0.13.0.dist-info/WHEEL +4 -0
  456. alysis_code-0.13.0.dist-info/entry_points.txt +3 -0
  457. alysis_code-0.13.0.dist-info/licenses/LICENSE +176 -0
  458. alysis_code-0.13.0.dist-info/licenses/NOTICE +4 -0
@@ -0,0 +1,1137 @@
1
+ from __future__ import annotations
2
+
3
+ import copy
4
+ from collections.abc import Callable, Collection
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+ from typing import Any, Protocol, TypeVar
8
+ from urllib.parse import SplitResult, urlsplit, urlunsplit
9
+
10
+ from .. import __version__
11
+ from ..runtime_kind import RuntimeKind
12
+ from .errors import McpError, McpProtocolError, McpRemoteError
13
+ from .jsonrpc import JsonRpcErrorObject, JsonRpcNotification, JsonRpcResponse
14
+ from .models import ResolvedMcpServer
15
+ from .oauth import McpOAuthError
16
+ from .prompts import (
17
+ McpGetPromptResult,
18
+ McpListedPrompt,
19
+ McpPromptNormalizationError,
20
+ normalize_get_prompt_result,
21
+ normalize_list_prompts_result,
22
+ )
23
+ from .resources import (
24
+ McpListedResource,
25
+ McpReadResourceResult,
26
+ McpResourceNormalizationError,
27
+ normalize_list_resources_result,
28
+ normalize_read_resource_result,
29
+ )
30
+ from .roots import McpRootsRequestHandler
31
+ from .server_requests import McpServerRequestContext, McpServerRequestHandler
32
+ from .transport_http import (
33
+ McpHttpTransport,
34
+ McpHttpTransportError,
35
+ McpHttpTransportSessionExpiredError,
36
+ )
37
+ from .transport_stdio import McpStdioTransport, McpStdioTransportError
38
+
39
+ MCP_PROTOCOL_VERSION = "2024-11-05"
40
+ MCP_STDIO_PROTOCOL_VERSION = MCP_PROTOCOL_VERSION
41
+ MCP_HTTP_PREFERRED_PROTOCOL_VERSION = "2025-11-25"
42
+ MCP_HTTP_SUPPORTED_PROTOCOL_VERSIONS = (
43
+ MCP_HTTP_PREFERRED_PROTOCOL_VERSION,
44
+ "2025-06-18",
45
+ "2025-03-26",
46
+ )
47
+ _SUPPORTED_PROTOCOL_VERSIONS_BY_TRANSPORT = {
48
+ "stdio": (MCP_STDIO_PROTOCOL_VERSION,),
49
+ "http": MCP_HTTP_SUPPORTED_PROTOCOL_VERSIONS,
50
+ }
51
+ # `notifications/initialized` needs a real follow-up window for immediate
52
+ # server activity without turning every quiet startup into a full startup-timeout wait.
53
+ _INITIALIZED_NOTIFICATION_COMPLETION_TIMEOUT_S = 0.5
54
+ _ReadOnlyRequestResult = TypeVar("_ReadOnlyRequestResult")
55
+
56
+
57
+ class McpClientError(McpError):
58
+ pass
59
+
60
+
61
+ class McpClientProtocolError(McpClientError, McpProtocolError):
62
+ pass
63
+
64
+
65
+ class McpClientRemoteError(McpClientError, McpRemoteError):
66
+ pass
67
+
68
+
69
+ class _McpClientTransport(Protocol):
70
+ @property
71
+ def closed(self) -> bool: ...
72
+
73
+ def request(
74
+ self,
75
+ *,
76
+ method: str,
77
+ params: Any | None,
78
+ timeout_s: float,
79
+ ) -> JsonRpcResponse: ...
80
+
81
+ def send_notification(
82
+ self,
83
+ *,
84
+ method: str,
85
+ params: Any | None,
86
+ completion_timeout_s: float | None = None,
87
+ ) -> None: ...
88
+
89
+ def drain_notifications(self) -> tuple[JsonRpcNotification, ...]: ...
90
+
91
+ def close(self) -> None: ...
92
+
93
+ def stderr_tail(self) -> str: ...
94
+
95
+
96
+ def _redacted_server_url(value: str | None) -> str:
97
+ split = urlsplit(str(value or ""))
98
+ hostname = split.hostname or ""
99
+ if split.port is None:
100
+ netloc = hostname
101
+ else:
102
+ netloc = f"{hostname}:{split.port}"
103
+ redacted = SplitResult(
104
+ scheme=split.scheme,
105
+ netloc=netloc,
106
+ path=split.path or "/",
107
+ query="",
108
+ fragment="",
109
+ )
110
+ return urlunsplit(redacted)
111
+
112
+
113
+ def _server_context(server: ResolvedMcpServer) -> str:
114
+ if server.transport == "http":
115
+ return f"MCP server '{server.id}' (url '{_redacted_server_url(server.url)}')"
116
+ return f"MCP server '{server.id}' (command '{server.command}')"
117
+
118
+
119
+ def _build_client_error(
120
+ *,
121
+ server: ResolvedMcpServer,
122
+ message: str,
123
+ stderr_tail: str = "",
124
+ error_type: type[McpClientError] = McpClientError,
125
+ ) -> McpClientError:
126
+ full_message = f"{_server_context(server)}: {message}"
127
+ cleaned_tail = str(stderr_tail or "").strip()
128
+ if cleaned_tail:
129
+ full_message += f"\nstderr tail:\n{cleaned_tail}"
130
+ return error_type(full_message)
131
+
132
+
133
+ def _initial_protocol_version_for_transport(transport: str) -> str:
134
+ if transport == "http":
135
+ return MCP_HTTP_PREFERRED_PROTOCOL_VERSION
136
+ return MCP_STDIO_PROTOCOL_VERSION
137
+
138
+
139
+ def _supported_protocol_versions_for_transport(transport: str) -> tuple[str, ...]:
140
+ return tuple(_SUPPORTED_PROTOCOL_VERSIONS_BY_TRANSPORT.get(transport, ()))
141
+
142
+
143
+ def _validate_protocol_version(
144
+ *,
145
+ server: ResolvedMcpServer,
146
+ transport: str,
147
+ value: Any,
148
+ stderr_tail: str,
149
+ ) -> str:
150
+ protocol_version = _require_string(
151
+ value,
152
+ server=server,
153
+ context="initialize.protocolVersion",
154
+ )
155
+ supported_versions = _supported_protocol_versions_for_transport(transport)
156
+ if protocol_version in supported_versions:
157
+ return protocol_version
158
+ if not supported_versions:
159
+ supported_text = "no supported versions configured"
160
+ elif len(supported_versions) == 1:
161
+ supported_text = repr(supported_versions[0])
162
+ else:
163
+ supported_text = ", ".join(repr(item) for item in supported_versions)
164
+ raise _build_client_error(
165
+ server=server,
166
+ message=(
167
+ "server reported unsupported protocol version "
168
+ f"{protocol_version!r}; expected one of {supported_text}."
169
+ ),
170
+ stderr_tail=stderr_tail,
171
+ error_type=McpClientProtocolError,
172
+ )
173
+
174
+
175
+ def _require_object(
176
+ value: Any,
177
+ *,
178
+ server: ResolvedMcpServer,
179
+ context: str,
180
+ ) -> dict[str, Any]:
181
+ if not isinstance(value, dict):
182
+ raise _build_client_error(
183
+ server=server,
184
+ message=f"{context} must be an object.",
185
+ error_type=McpClientProtocolError,
186
+ )
187
+ return value
188
+
189
+
190
+ def _require_string(
191
+ value: Any,
192
+ *,
193
+ server: ResolvedMcpServer,
194
+ context: str,
195
+ ) -> str:
196
+ if not isinstance(value, str):
197
+ raise _build_client_error(
198
+ server=server,
199
+ message=f"{context} must be a string.",
200
+ error_type=McpClientProtocolError,
201
+ )
202
+ cleaned = value.strip()
203
+ if not cleaned:
204
+ raise _build_client_error(
205
+ server=server,
206
+ message=f"{context} cannot be empty.",
207
+ error_type=McpClientProtocolError,
208
+ )
209
+ return cleaned
210
+
211
+
212
+ @dataclass(frozen=True)
213
+ class McpListedTool:
214
+ name: str
215
+ description: str
216
+ input_schema: dict[str, Any]
217
+ raw_payload: dict[str, Any] = field(repr=False)
218
+
219
+
220
+ @dataclass(frozen=True)
221
+ class McpToolCallResult:
222
+ is_error: bool
223
+ structured_content: Any | None
224
+ content: tuple[dict[str, Any], ...]
225
+ content_summary: str
226
+ extracted_text: str
227
+ raw_payload: dict[str, Any] = field(repr=False)
228
+
229
+
230
+ class _BaseMcpClient:
231
+ transport: _McpClientTransport
232
+ _transport_error_types: tuple[type[BaseException], ...]
233
+
234
+ def __init__(
235
+ self,
236
+ *,
237
+ server: ResolvedMcpServer,
238
+ workspace_root: Path,
239
+ transport: _McpClientTransport,
240
+ transport_error_types: tuple[type[BaseException], ...],
241
+ runtime_kind: RuntimeKind,
242
+ server_request_context: McpServerRequestContext,
243
+ server_request_handler: McpServerRequestHandler,
244
+ ) -> None:
245
+ self.server = server
246
+ self.workspace_root = workspace_root.resolve()
247
+ self.transport = transport
248
+ self._transport_error_types = transport_error_types
249
+ self.runtime_kind = runtime_kind
250
+ self._server_request_context = server_request_context
251
+ self._server_request_handler = server_request_handler
252
+ self._client_capabilities = copy.deepcopy(
253
+ server_request_handler.client_capabilities(context=server_request_context)
254
+ )
255
+ self._initialized = False
256
+ self._supports_tools = False
257
+ self._supports_resources = False
258
+ self._supports_prompts = False
259
+ self._server_info: dict[str, Any] = {}
260
+ self._server_capabilities: dict[str, Any] = {}
261
+ self._tools_list_changed = False
262
+ self._resources_list_changed = False
263
+ self._prompts_list_changed = False
264
+ self._negotiated_protocol_version: str | None = None
265
+
266
+ @property
267
+ def closed(self) -> bool:
268
+ return self.transport.closed
269
+
270
+ @property
271
+ def tools_list_changed(self) -> bool:
272
+ return self._tools_list_changed
273
+
274
+ @property
275
+ def resources_list_changed(self) -> bool:
276
+ return self._resources_list_changed
277
+
278
+ @property
279
+ def prompts_list_changed(self) -> bool:
280
+ return self._prompts_list_changed
281
+
282
+ @property
283
+ def server_info(self) -> dict[str, Any]:
284
+ return dict(self._server_info)
285
+
286
+ @property
287
+ def server_capabilities(self) -> dict[str, Any]:
288
+ return copy.deepcopy(self._server_capabilities)
289
+
290
+ @property
291
+ def negotiated_protocol_version(self) -> str | None:
292
+ return self._negotiated_protocol_version
293
+
294
+ @property
295
+ def session_negotiated(self) -> bool:
296
+ return False
297
+
298
+ @property
299
+ def roots_capability_enabled(self) -> bool:
300
+ return isinstance(self._client_capabilities.get("roots"), dict)
301
+
302
+ @property
303
+ def supports_tools(self) -> bool:
304
+ return self._supports_tools
305
+
306
+ @property
307
+ def supports_resources(self) -> bool:
308
+ return self._supports_resources
309
+
310
+ @property
311
+ def supports_prompts(self) -> bool:
312
+ return self._supports_prompts
313
+
314
+ def _diagnostic_tail(self) -> str:
315
+ return self.transport.stderr_tail()
316
+
317
+ def _drain_notifications(self) -> tuple[JsonRpcNotification, ...]:
318
+ notifications = self.transport.drain_notifications()
319
+ for notification in notifications:
320
+ if notification.method == "notifications/tools/list_changed":
321
+ self._tools_list_changed = True
322
+ elif notification.method == "notifications/resources/list_changed":
323
+ self._resources_list_changed = True
324
+ elif notification.method == "notifications/prompts/list_changed":
325
+ self._prompts_list_changed = True
326
+ return notifications
327
+
328
+ def observe_notifications(self) -> None:
329
+ self._drain_notifications()
330
+
331
+ def _raise_remote_error(
332
+ self,
333
+ *,
334
+ method: str,
335
+ error: JsonRpcErrorObject,
336
+ ) -> None:
337
+ raise _build_client_error(
338
+ server=self.server,
339
+ message=(f"remote JSON-RPC error during '{method}': {error.code} {error.message}"),
340
+ stderr_tail=self._diagnostic_tail(),
341
+ error_type=McpClientRemoteError,
342
+ )
343
+
344
+ def _response_result_object(
345
+ self,
346
+ *,
347
+ method: str,
348
+ response: JsonRpcResponse,
349
+ ) -> dict[str, Any]:
350
+ if response.error is not None:
351
+ self._raise_remote_error(method=method, error=response.error)
352
+ return _require_object(
353
+ response.result,
354
+ server=self.server,
355
+ context=f"'{method}' result",
356
+ )
357
+
358
+ def _reset_initialization_state(self) -> None:
359
+ self._initialized = False
360
+ self._supports_tools = False
361
+ self._supports_resources = False
362
+ self._supports_prompts = False
363
+ self._server_info = {}
364
+ self._server_capabilities = {}
365
+ self._negotiated_protocol_version = None
366
+
367
+ def _apply_negotiated_protocol_version(self, protocol_version: str) -> None:
368
+ self._negotiated_protocol_version = protocol_version
369
+
370
+ def _initialize_once(self) -> None:
371
+ response = self.transport.request(
372
+ method="initialize",
373
+ params={
374
+ "protocolVersion": _initial_protocol_version_for_transport(self.server.transport),
375
+ "capabilities": copy.deepcopy(self._client_capabilities),
376
+ "clientInfo": {
377
+ "name": "alysis-code",
378
+ "version": __version__,
379
+ },
380
+ },
381
+ timeout_s=self.server.startup_timeout_s,
382
+ )
383
+ result = self._response_result_object(method="initialize", response=response)
384
+ protocol_version = _validate_protocol_version(
385
+ server=self.server,
386
+ transport=self.server.transport,
387
+ value=result.get("protocolVersion"),
388
+ stderr_tail=self._diagnostic_tail(),
389
+ )
390
+ capabilities = _require_object(
391
+ result.get("capabilities"),
392
+ server=self.server,
393
+ context="initialize.capabilities",
394
+ )
395
+ tools_capability = capabilities.get("tools")
396
+ if tools_capability is not None and not isinstance(tools_capability, dict):
397
+ raise _build_client_error(
398
+ server=self.server,
399
+ message="initialize.capabilities.tools must be an object when present.",
400
+ stderr_tail=self._diagnostic_tail(),
401
+ error_type=McpClientProtocolError,
402
+ )
403
+ resources_capability = capabilities.get("resources")
404
+ if resources_capability is not None and not isinstance(resources_capability, dict):
405
+ raise _build_client_error(
406
+ server=self.server,
407
+ message="initialize.capabilities.resources must be an object when present.",
408
+ stderr_tail=self._diagnostic_tail(),
409
+ error_type=McpClientProtocolError,
410
+ )
411
+ prompts_capability = capabilities.get("prompts")
412
+ if prompts_capability is not None and not isinstance(prompts_capability, dict):
413
+ raise _build_client_error(
414
+ server=self.server,
415
+ message="initialize.capabilities.prompts must be an object when present.",
416
+ stderr_tail=self._diagnostic_tail(),
417
+ error_type=McpClientProtocolError,
418
+ )
419
+ server_info = result.get("serverInfo") or {}
420
+ if not isinstance(server_info, dict):
421
+ raise _build_client_error(
422
+ server=self.server,
423
+ message="initialize.serverInfo must be an object when present.",
424
+ stderr_tail=self._diagnostic_tail(),
425
+ error_type=McpClientProtocolError,
426
+ )
427
+ self._server_info = copy.deepcopy(server_info)
428
+ self._server_capabilities = copy.deepcopy(capabilities)
429
+ self._supports_tools = "tools" in capabilities
430
+ self._supports_resources = "resources" in capabilities
431
+ self._supports_prompts = "prompts" in capabilities
432
+ self._apply_negotiated_protocol_version(protocol_version)
433
+ self.transport.send_notification(
434
+ method="notifications/initialized",
435
+ params={},
436
+ completion_timeout_s=min(
437
+ self.server.startup_timeout_s,
438
+ _INITIALIZED_NOTIFICATION_COMPLETION_TIMEOUT_S,
439
+ ),
440
+ )
441
+ self._initialized = True
442
+ self._drain_notifications()
443
+
444
+ def ensure_initialized(self) -> None:
445
+ if self._initialized:
446
+ self._drain_notifications()
447
+ return
448
+ try:
449
+ self._initialize_once()
450
+ except Exception as exc: # noqa: BLE001
451
+ if isinstance(exc, (McpClientError, *self._transport_error_types)):
452
+ self.close()
453
+ raise
454
+ self.close()
455
+ raise _build_client_error(
456
+ server=self.server,
457
+ message=f"initialize failed: {exc}",
458
+ stderr_tail=self._diagnostic_tail(),
459
+ ) from exc
460
+
461
+ def _require_tools_capability(self, *, method: str) -> None:
462
+ self.ensure_initialized()
463
+ if self._supports_tools:
464
+ return
465
+ raise _build_client_error(
466
+ server=self.server,
467
+ message=f"server does not advertise tools capability for '{method}'.",
468
+ stderr_tail=self._diagnostic_tail(),
469
+ error_type=McpClientProtocolError,
470
+ )
471
+
472
+ def _require_resources_capability(self, *, method: str) -> None:
473
+ self.ensure_initialized()
474
+ if self._supports_resources:
475
+ return
476
+ raise _build_client_error(
477
+ server=self.server,
478
+ message=f"server does not advertise resources capability for '{method}'.",
479
+ stderr_tail=self._diagnostic_tail(),
480
+ error_type=McpClientProtocolError,
481
+ )
482
+
483
+ def _require_prompts_capability(self, *, method: str) -> None:
484
+ self.ensure_initialized()
485
+ if self._supports_prompts:
486
+ return
487
+ raise _build_client_error(
488
+ server=self.server,
489
+ message=f"server does not advertise prompts capability for '{method}'.",
490
+ stderr_tail=self._diagnostic_tail(),
491
+ error_type=McpClientProtocolError,
492
+ )
493
+
494
+ def _list_tools_once(self) -> tuple[McpListedTool, ...]:
495
+ tools: list[McpListedTool] = []
496
+ seen_cursors: set[str] = set()
497
+ next_cursor: str | None = None
498
+ while True:
499
+ params: dict[str, Any] = {}
500
+ if next_cursor is not None:
501
+ params["cursor"] = next_cursor
502
+ response = self.transport.request(
503
+ method="tools/list",
504
+ params=params,
505
+ timeout_s=self.server.call_timeout_s,
506
+ )
507
+ result = self._response_result_object(method="tools/list", response=response)
508
+ page_tools = result.get("tools")
509
+ if not isinstance(page_tools, list):
510
+ raise _build_client_error(
511
+ server=self.server,
512
+ message="tools/list result.tools must be an array.",
513
+ stderr_tail=self._diagnostic_tail(),
514
+ error_type=McpClientProtocolError,
515
+ )
516
+ for raw_tool in page_tools:
517
+ payload = _require_object(
518
+ raw_tool,
519
+ server=self.server,
520
+ context="tools/list tool entry",
521
+ )
522
+ tools.append(
523
+ McpListedTool(
524
+ name=_require_string(
525
+ payload.get("name"),
526
+ server=self.server,
527
+ context="tools/list tool.name",
528
+ ),
529
+ description=str(payload.get("description") or "").strip(),
530
+ input_schema=_require_object(
531
+ payload.get("inputSchema"),
532
+ server=self.server,
533
+ context="tools/list tool.inputSchema",
534
+ ),
535
+ raw_payload=copy.deepcopy(payload),
536
+ )
537
+ )
538
+ raw_cursor = result.get("nextCursor")
539
+ if raw_cursor is None:
540
+ break
541
+ next_cursor = _require_string(
542
+ raw_cursor,
543
+ server=self.server,
544
+ context="tools/list nextCursor",
545
+ )
546
+ if next_cursor in seen_cursors:
547
+ raise _build_client_error(
548
+ server=self.server,
549
+ message=f"tools/list returned repeated nextCursor {next_cursor!r}.",
550
+ stderr_tail=self._diagnostic_tail(),
551
+ error_type=McpClientProtocolError,
552
+ )
553
+ seen_cursors.add(next_cursor)
554
+ self._drain_notifications()
555
+ return tuple(tools)
556
+
557
+ def list_tools(self) -> tuple[McpListedTool, ...]:
558
+ self._require_tools_capability(method="tools/list")
559
+ return self._list_tools_once()
560
+
561
+ def _list_resources_once(self) -> tuple[McpListedResource, ...]:
562
+ resources: list[McpListedResource] = []
563
+ seen_cursors: set[str] = set()
564
+ seen_uris: set[str] = set()
565
+ next_cursor: str | None = None
566
+ while True:
567
+ params: dict[str, Any] = {}
568
+ if next_cursor is not None:
569
+ params["cursor"] = next_cursor
570
+ response = self.transport.request(
571
+ method="resources/list",
572
+ params=params,
573
+ timeout_s=self.server.call_timeout_s,
574
+ )
575
+ result = self._response_result_object(method="resources/list", response=response)
576
+ try:
577
+ page_resources, raw_cursor = normalize_list_resources_result(result)
578
+ except McpResourceNormalizationError as exc:
579
+ raise _build_client_error(
580
+ server=self.server,
581
+ message=str(exc),
582
+ stderr_tail=self._diagnostic_tail(),
583
+ error_type=McpClientProtocolError,
584
+ ) from exc
585
+ for resource in page_resources:
586
+ if resource.uri in seen_uris:
587
+ raise _build_client_error(
588
+ server=self.server,
589
+ message=f"resources/list returned repeated resource URI {resource.uri!r}.",
590
+ stderr_tail=self._diagnostic_tail(),
591
+ error_type=McpClientProtocolError,
592
+ )
593
+ seen_uris.add(resource.uri)
594
+ resources.append(resource)
595
+ if raw_cursor is None:
596
+ break
597
+ if raw_cursor in seen_cursors:
598
+ raise _build_client_error(
599
+ server=self.server,
600
+ message=f"resources/list returned repeated nextCursor {raw_cursor!r}.",
601
+ stderr_tail=self._diagnostic_tail(),
602
+ error_type=McpClientProtocolError,
603
+ )
604
+ seen_cursors.add(raw_cursor)
605
+ next_cursor = raw_cursor
606
+ self._drain_notifications()
607
+ return tuple(resources)
608
+
609
+ def list_resources(self) -> tuple[McpListedResource, ...]:
610
+ self._require_resources_capability(method="resources/list")
611
+ return self._list_resources_once()
612
+
613
+ def _list_prompts_once(self) -> tuple[McpListedPrompt, ...]:
614
+ prompts: list[McpListedPrompt] = []
615
+ seen_cursors: set[str] = set()
616
+ seen_names: set[str] = set()
617
+ next_cursor: str | None = None
618
+ while True:
619
+ params: dict[str, Any] = {}
620
+ if next_cursor is not None:
621
+ params["cursor"] = next_cursor
622
+ response = self.transport.request(
623
+ method="prompts/list",
624
+ params=params,
625
+ timeout_s=self.server.call_timeout_s,
626
+ )
627
+ result = self._response_result_object(method="prompts/list", response=response)
628
+ try:
629
+ page_prompts, raw_cursor = normalize_list_prompts_result(result)
630
+ except McpPromptNormalizationError as exc:
631
+ raise _build_client_error(
632
+ server=self.server,
633
+ message=str(exc),
634
+ stderr_tail=self._diagnostic_tail(),
635
+ error_type=McpClientProtocolError,
636
+ ) from exc
637
+ for prompt in page_prompts:
638
+ name_key = prompt.name.casefold()
639
+ if name_key in seen_names:
640
+ raise _build_client_error(
641
+ server=self.server,
642
+ message=f"prompts/list returned repeated prompt name {prompt.name!r}.",
643
+ stderr_tail=self._diagnostic_tail(),
644
+ error_type=McpClientProtocolError,
645
+ )
646
+ seen_names.add(name_key)
647
+ prompts.append(prompt)
648
+ if raw_cursor is None:
649
+ break
650
+ if raw_cursor in seen_cursors:
651
+ raise _build_client_error(
652
+ server=self.server,
653
+ message=f"prompts/list returned repeated nextCursor {raw_cursor!r}.",
654
+ stderr_tail=self._diagnostic_tail(),
655
+ error_type=McpClientProtocolError,
656
+ )
657
+ seen_cursors.add(raw_cursor)
658
+ next_cursor = raw_cursor
659
+ self._drain_notifications()
660
+ return tuple(prompts)
661
+
662
+ def list_prompts(self) -> tuple[McpListedPrompt, ...]:
663
+ self._require_prompts_capability(method="prompts/list")
664
+ return self._list_prompts_once()
665
+
666
+ def _normalize_prompt_arguments(
667
+ self,
668
+ *,
669
+ arguments: dict[str, str] | None,
670
+ ) -> dict[str, str] | None:
671
+ if arguments is None:
672
+ return None
673
+ if not isinstance(arguments, dict):
674
+ raise _build_client_error(
675
+ server=self.server,
676
+ message="prompts/get arguments must be an object mapping strings to strings.",
677
+ error_type=McpClientProtocolError,
678
+ )
679
+ normalized: dict[str, str] = {}
680
+ for raw_key, raw_value in arguments.items():
681
+ key = _require_string(
682
+ raw_key,
683
+ server=self.server,
684
+ context="prompts/get argument name",
685
+ )
686
+ if not isinstance(raw_value, str):
687
+ raise _build_client_error(
688
+ server=self.server,
689
+ message="prompts/get argument values must be strings.",
690
+ error_type=McpClientProtocolError,
691
+ )
692
+ normalized[key] = raw_value
693
+ return normalized
694
+
695
+ def _get_prompt_once(
696
+ self,
697
+ *,
698
+ name: str,
699
+ arguments: dict[str, str] | None,
700
+ ) -> McpGetPromptResult:
701
+ params: dict[str, Any] = {"name": name}
702
+ normalized_arguments = self._normalize_prompt_arguments(arguments=arguments)
703
+ if normalized_arguments:
704
+ params["arguments"] = normalized_arguments
705
+ response = self.transport.request(
706
+ method="prompts/get",
707
+ params=params,
708
+ timeout_s=self.server.call_timeout_s,
709
+ )
710
+ result = self._response_result_object(method="prompts/get", response=response)
711
+ try:
712
+ normalized = normalize_get_prompt_result(result, expected_name=name)
713
+ except McpPromptNormalizationError as exc:
714
+ raise _build_client_error(
715
+ server=self.server,
716
+ message=str(exc),
717
+ stderr_tail=self._diagnostic_tail(),
718
+ error_type=McpClientProtocolError,
719
+ ) from exc
720
+ self._drain_notifications()
721
+ return normalized
722
+
723
+ def get_prompt(
724
+ self,
725
+ *,
726
+ name: str,
727
+ arguments: dict[str, str] | None = None,
728
+ ) -> McpGetPromptResult:
729
+ self._require_prompts_capability(method="prompts/get")
730
+ normalized_name = _require_string(
731
+ name,
732
+ server=self.server,
733
+ context="prompts/get name",
734
+ )
735
+ return self._get_prompt_once(name=normalized_name, arguments=arguments)
736
+
737
+ def _read_resource_once(
738
+ self,
739
+ *,
740
+ resource_uri: str,
741
+ allowed_uris: Collection[str] | None = None,
742
+ ) -> McpReadResourceResult:
743
+ if allowed_uris is None:
744
+ raise _build_client_error(
745
+ server=self.server,
746
+ message=(
747
+ "resources/read requires a frozen resource snapshot allowlist from "
748
+ "resources/list; arbitrary URI reads are blocked."
749
+ ),
750
+ error_type=McpClientError,
751
+ )
752
+ if isinstance(allowed_uris, str):
753
+ raise _build_client_error(
754
+ server=self.server,
755
+ message="resources/read allowed_uris must be a collection of URIs, not a string.",
756
+ error_type=McpClientProtocolError,
757
+ )
758
+ if not isinstance(allowed_uris, Collection):
759
+ raise _build_client_error(
760
+ server=self.server,
761
+ message="resources/read allowed_uris must be a collection of URI strings.",
762
+ error_type=McpClientProtocolError,
763
+ )
764
+ normalized_allowed_uris = {
765
+ _require_string(
766
+ item,
767
+ server=self.server,
768
+ context="resources/read allowed URI",
769
+ )
770
+ for item in allowed_uris
771
+ }
772
+ if resource_uri not in normalized_allowed_uris:
773
+ raise _build_client_error(
774
+ server=self.server,
775
+ message=(
776
+ "resources/read is limited to the frozen resource snapshot for this session; "
777
+ f"URI {resource_uri!r} was not snapshotted."
778
+ ),
779
+ error_type=McpClientError,
780
+ )
781
+ response = self.transport.request(
782
+ method="resources/read",
783
+ params={"uri": resource_uri},
784
+ timeout_s=self.server.call_timeout_s,
785
+ )
786
+ result = self._response_result_object(method="resources/read", response=response)
787
+ try:
788
+ normalized = normalize_read_resource_result(result, expected_uri=resource_uri)
789
+ except McpResourceNormalizationError as exc:
790
+ raise _build_client_error(
791
+ server=self.server,
792
+ message=str(exc),
793
+ stderr_tail=self._diagnostic_tail(),
794
+ error_type=McpClientProtocolError,
795
+ ) from exc
796
+ self._drain_notifications()
797
+ return normalized
798
+
799
+ def read_resource(
800
+ self,
801
+ *,
802
+ resource_uri: str,
803
+ allowed_uris: Collection[str] | None = None,
804
+ ) -> McpReadResourceResult:
805
+ self._require_resources_capability(method="resources/read")
806
+ normalized_uri = _require_string(
807
+ resource_uri,
808
+ server=self.server,
809
+ context="resources/read uri",
810
+ )
811
+ return self._read_resource_once(resource_uri=normalized_uri, allowed_uris=allowed_uris)
812
+
813
+ def _call_tool_once(
814
+ self,
815
+ *,
816
+ tool_name: str,
817
+ arguments: dict[str, Any],
818
+ ) -> McpToolCallResult:
819
+ response = self.transport.request(
820
+ method="tools/call",
821
+ params={
822
+ "name": tool_name,
823
+ "arguments": arguments,
824
+ },
825
+ timeout_s=self.server.call_timeout_s,
826
+ )
827
+ result = self._response_result_object(method="tools/call", response=response)
828
+ content = result.get("content")
829
+ if content is None:
830
+ content = []
831
+ if not isinstance(content, list):
832
+ raise _build_client_error(
833
+ server=self.server,
834
+ message="tools/call result.content must be an array when present.",
835
+ stderr_tail=self._diagnostic_tail(),
836
+ error_type=McpClientProtocolError,
837
+ )
838
+ is_error = bool(result.get("isError", False))
839
+ if "isError" in result and not isinstance(result.get("isError"), bool):
840
+ raise _build_client_error(
841
+ server=self.server,
842
+ message="tools/call result.isError must be a boolean when present.",
843
+ stderr_tail=self._diagnostic_tail(),
844
+ error_type=McpClientProtocolError,
845
+ )
846
+ normalized_content, content_summary, extracted_text = _normalize_content_items(
847
+ server=self.server,
848
+ content_items=content,
849
+ stderr_tail=self._diagnostic_tail(),
850
+ )
851
+ self._drain_notifications()
852
+ return McpToolCallResult(
853
+ is_error=is_error,
854
+ structured_content=copy.deepcopy(result.get("structuredContent")),
855
+ content=normalized_content,
856
+ content_summary=content_summary,
857
+ extracted_text=extracted_text,
858
+ raw_payload=copy.deepcopy(result),
859
+ )
860
+
861
+ def call_tool(
862
+ self,
863
+ *,
864
+ tool_name: str,
865
+ arguments: dict[str, Any],
866
+ ) -> McpToolCallResult:
867
+ self._require_tools_capability(method="tools/call")
868
+ if not isinstance(arguments, dict):
869
+ raise _build_client_error(
870
+ server=self.server,
871
+ message="tools/call arguments must be an object.",
872
+ stderr_tail=self._diagnostic_tail(),
873
+ error_type=McpClientProtocolError,
874
+ )
875
+ return self._call_tool_once(tool_name=tool_name, arguments=arguments)
876
+
877
+ def close(self) -> None:
878
+ self.transport.close()
879
+
880
+
881
+ class McpStdioClient(_BaseMcpClient):
882
+ def __init__(
883
+ self,
884
+ *,
885
+ server: ResolvedMcpServer,
886
+ workspace_root: Path,
887
+ runtime_kind: RuntimeKind | str | None = None,
888
+ ) -> None:
889
+ resolved_workspace_root = workspace_root.resolve()
890
+ request_context = McpServerRequestContext.create(
891
+ server_id=server.id,
892
+ workspace_root=resolved_workspace_root,
893
+ runtime_kind=runtime_kind,
894
+ fallback_runtime_kind=RuntimeKind.SUBAGENT,
895
+ )
896
+ request_handler = McpRootsRequestHandler(server=server)
897
+ super().__init__(
898
+ server=server,
899
+ workspace_root=resolved_workspace_root,
900
+ transport=McpStdioTransport(
901
+ server=server,
902
+ workspace_root=resolved_workspace_root,
903
+ server_request_context=request_context,
904
+ server_request_handler=request_handler,
905
+ ),
906
+ transport_error_types=(McpStdioTransportError,),
907
+ runtime_kind=request_context.runtime_kind,
908
+ server_request_context=request_context,
909
+ server_request_handler=request_handler,
910
+ )
911
+
912
+
913
+ class McpHttpClient(_BaseMcpClient):
914
+ transport: McpHttpTransport
915
+
916
+ def __init__(
917
+ self,
918
+ *,
919
+ server: ResolvedMcpServer,
920
+ workspace_root: Path,
921
+ runtime_kind: RuntimeKind | str | None = None,
922
+ ) -> None:
923
+ resolved_workspace_root = workspace_root.resolve()
924
+ request_context = McpServerRequestContext.create(
925
+ server_id=server.id,
926
+ workspace_root=resolved_workspace_root,
927
+ runtime_kind=runtime_kind,
928
+ fallback_runtime_kind=RuntimeKind.SUBAGENT,
929
+ )
930
+ request_handler = McpRootsRequestHandler(server=server)
931
+ super().__init__(
932
+ server=server,
933
+ workspace_root=resolved_workspace_root,
934
+ transport=McpHttpTransport(
935
+ server=server,
936
+ workspace_root=resolved_workspace_root,
937
+ server_request_context=request_context,
938
+ server_request_handler=request_handler,
939
+ ),
940
+ transport_error_types=(McpHttpTransportError, McpOAuthError),
941
+ runtime_kind=request_context.runtime_kind,
942
+ server_request_context=request_context,
943
+ server_request_handler=request_handler,
944
+ )
945
+
946
+ @property
947
+ def session_negotiated(self) -> bool:
948
+ return self.transport.session_negotiated
949
+
950
+ def _apply_negotiated_protocol_version(self, protocol_version: str) -> None:
951
+ super()._apply_negotiated_protocol_version(protocol_version)
952
+ self.transport.set_negotiated_protocol_version(protocol_version)
953
+
954
+ def _reset_http_session_state(self) -> None:
955
+ self.transport.reset_session_state()
956
+ self._reset_initialization_state()
957
+
958
+ def _reinitialize_after_session_expiry(self) -> None:
959
+ self._reset_http_session_state()
960
+ self.ensure_initialized()
961
+
962
+ def _retry_read_only_request_once(
963
+ self,
964
+ *,
965
+ method: str,
966
+ request_fn: Callable[[], _ReadOnlyRequestResult],
967
+ ) -> _ReadOnlyRequestResult:
968
+ for attempt in range(2):
969
+ try:
970
+ return request_fn()
971
+ except McpHttpTransportSessionExpiredError as exc:
972
+ if attempt > 0:
973
+ raise _build_client_error(
974
+ server=self.server,
975
+ message=(
976
+ f"HTTP MCP session expired during '{method}' even after one "
977
+ "reinitialize+retry."
978
+ ),
979
+ error_type=McpClientError,
980
+ ) from exc
981
+ self._reinitialize_after_session_expiry()
982
+ raise AssertionError("unreachable")
983
+
984
+ def list_tools(self) -> tuple[McpListedTool, ...]:
985
+ self._require_tools_capability(method="tools/list")
986
+ return self._retry_read_only_request_once(
987
+ method="tools/list", request_fn=self._list_tools_once
988
+ )
989
+
990
+ def list_resources(self) -> tuple[McpListedResource, ...]:
991
+ self._require_resources_capability(method="resources/list")
992
+ return self._retry_read_only_request_once(
993
+ method="resources/list",
994
+ request_fn=self._list_resources_once,
995
+ )
996
+
997
+ def list_prompts(self) -> tuple[McpListedPrompt, ...]:
998
+ self._require_prompts_capability(method="prompts/list")
999
+ return self._retry_read_only_request_once(
1000
+ method="prompts/list",
1001
+ request_fn=self._list_prompts_once,
1002
+ )
1003
+
1004
+ def read_resource(
1005
+ self,
1006
+ *,
1007
+ resource_uri: str,
1008
+ allowed_uris: Collection[str] | None = None,
1009
+ ) -> McpReadResourceResult:
1010
+ self._require_resources_capability(method="resources/read")
1011
+ normalized_uri = _require_string(
1012
+ resource_uri,
1013
+ server=self.server,
1014
+ context="resources/read uri",
1015
+ )
1016
+ return self._retry_read_only_request_once(
1017
+ method="resources/read",
1018
+ request_fn=lambda: self._read_resource_once(
1019
+ resource_uri=normalized_uri,
1020
+ allowed_uris=allowed_uris,
1021
+ ),
1022
+ )
1023
+
1024
+ def get_prompt(
1025
+ self,
1026
+ *,
1027
+ name: str,
1028
+ arguments: dict[str, str] | None = None,
1029
+ ) -> McpGetPromptResult:
1030
+ self._require_prompts_capability(method="prompts/get")
1031
+ normalized_name = _require_string(
1032
+ name,
1033
+ server=self.server,
1034
+ context="prompts/get name",
1035
+ )
1036
+ return self._retry_read_only_request_once(
1037
+ method="prompts/get",
1038
+ request_fn=lambda: self._get_prompt_once(
1039
+ name=normalized_name,
1040
+ arguments=arguments,
1041
+ ),
1042
+ )
1043
+
1044
+ def call_tool(
1045
+ self,
1046
+ *,
1047
+ tool_name: str,
1048
+ arguments: dict[str, Any],
1049
+ ) -> McpToolCallResult:
1050
+ self._require_tools_capability(method="tools/call")
1051
+ if not isinstance(arguments, dict):
1052
+ raise _build_client_error(
1053
+ server=self.server,
1054
+ message="tools/call arguments must be an object.",
1055
+ stderr_tail=self._diagnostic_tail(),
1056
+ error_type=McpClientProtocolError,
1057
+ )
1058
+ try:
1059
+ return self._call_tool_once(tool_name=tool_name, arguments=arguments)
1060
+ except McpHttpTransportSessionExpiredError as exc:
1061
+ reinitialize_error: Exception | None = None
1062
+ try:
1063
+ self._reinitialize_after_session_expiry()
1064
+ except Exception as recovery_exc: # noqa: BLE001
1065
+ if isinstance(recovery_exc, Exception):
1066
+ reinitialize_error = recovery_exc
1067
+ else:
1068
+ reinitialize_error = RuntimeError(str(recovery_exc))
1069
+ message = (
1070
+ "HTTP MCP session expired before or during 'tools/call'; the active call was "
1071
+ "not automatically retried because it may be side-effectful."
1072
+ )
1073
+ if reinitialize_error is not None:
1074
+ message += f" Session reinitialization also failed: {reinitialize_error}"
1075
+ raise _build_client_error(
1076
+ server=self.server,
1077
+ message=message,
1078
+ error_type=McpClientError,
1079
+ ) from exc
1080
+
1081
+
1082
+ def _normalize_content_items(
1083
+ *,
1084
+ server: ResolvedMcpServer,
1085
+ content_items: list[Any],
1086
+ stderr_tail: str,
1087
+ ) -> tuple[tuple[dict[str, Any], ...], str, str]:
1088
+ normalized: list[dict[str, Any]] = []
1089
+ summaries: list[str] = []
1090
+ text_parts: list[str] = []
1091
+ for index, raw_item in enumerate(content_items):
1092
+ item = _require_object(
1093
+ raw_item,
1094
+ server=server,
1095
+ context=f"tools/call content[{index}]",
1096
+ )
1097
+ item_type = _require_string(
1098
+ item.get("type"),
1099
+ server=server,
1100
+ context=f"tools/call content[{index}].type",
1101
+ )
1102
+ if item_type == "text":
1103
+ text = _require_string(
1104
+ item.get("text"),
1105
+ server=server,
1106
+ context=f"tools/call content[{index}].text",
1107
+ )
1108
+ normalized.append({"type": "text", "text": text, "summary": f"text({len(text)} chars)"})
1109
+ summaries.append(f"text({len(text)} chars)")
1110
+ text_parts.append(text)
1111
+ continue
1112
+
1113
+ summary = f"{item_type} item omitted"
1114
+ payload: dict[str, Any] = {
1115
+ "type": item_type,
1116
+ "summary": summary,
1117
+ }
1118
+ mime_type = item.get("mimeType")
1119
+ if isinstance(mime_type, str) and mime_type.strip():
1120
+ payload["mime_type"] = mime_type.strip()
1121
+ summary = f"{mime_type.strip()} {summary}"
1122
+ payload["summary"] = summary
1123
+ uri = item.get("uri")
1124
+ if isinstance(uri, str) and uri.strip():
1125
+ payload["uri"] = uri.strip()
1126
+ if item_type == "resource":
1127
+ resource = item.get("resource")
1128
+ if isinstance(resource, dict):
1129
+ resource_uri = resource.get("uri")
1130
+ if isinstance(resource_uri, str) and resource_uri.strip():
1131
+ payload["resource_uri"] = resource_uri.strip()
1132
+ resource_mime = resource.get("mimeType")
1133
+ if isinstance(resource_mime, str) and resource_mime.strip():
1134
+ payload["resource_mime_type"] = resource_mime.strip()
1135
+ normalized.append(payload)
1136
+ summaries.append(summary)
1137
+ return tuple(normalized), ", ".join(summaries) or "no content", "\n\n".join(text_parts)