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,666 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from collections.abc import Callable
5
+ from dataclasses import dataclass, field
6
+ from ipaddress import ip_address
7
+ from pathlib import Path
8
+ from typing import Annotated, Any, Literal, cast
9
+ from urllib.parse import urlsplit
10
+
11
+ from pydantic import (
12
+ BaseModel,
13
+ ConfigDict,
14
+ Field,
15
+ StrictBool,
16
+ StrictInt,
17
+ StrictStr,
18
+ field_validator,
19
+ model_validator,
20
+ )
21
+
22
+ from ..config import ConfigError as AppConfigError
23
+ from ..runtime_kind import RuntimeKind, normalize_runtime_kind
24
+ from .errors import McpConfigError as ConfigError
25
+
26
+ TransportKind = Literal["stdio", "http"]
27
+ RootsMode = Literal["disabled", "workspace"]
28
+ ResourcesMode = Literal["disabled", "listed_read_only"]
29
+ PromptsMode = Literal["disabled", "listed_get_only"]
30
+
31
+ _SERVER_ID_RE = re.compile(r"^[a-z0-9][a-z0-9._-]*(/[a-z0-9][a-z0-9._-]*)?$")
32
+ _ENV_NAME_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
33
+ _TOOL_PREFIX_RE = re.compile(r"^[a-z0-9][a-z0-9_-]*$")
34
+
35
+
36
+ def normalize_server_id(raw_value: object) -> str:
37
+ value = str(raw_value).strip().lower()
38
+ if not value:
39
+ raise ConfigError("MCP server id cannot be empty.")
40
+ if not _SERVER_ID_RE.fullmatch(value):
41
+ raise ConfigError(
42
+ f"Invalid MCP server id: {raw_value!r}. Expected lowercase [a-z0-9._-] "
43
+ "with at most one plugin scope slash."
44
+ )
45
+ return value
46
+
47
+
48
+ def normalize_ordered_name_list(
49
+ value: object,
50
+ *,
51
+ field_name: str,
52
+ ) -> list[str]:
53
+ if value is None:
54
+ return []
55
+ if not isinstance(value, list):
56
+ raise TypeError(f"{field_name} must be a list of strings.")
57
+ normalized: list[str] = []
58
+ seen: set[str] = set()
59
+ for item in value:
60
+ if not isinstance(item, str):
61
+ raise TypeError(f"{field_name} items must be strings.")
62
+ cleaned = item.strip()
63
+ if not cleaned:
64
+ continue
65
+ lowered = cleaned.lower()
66
+ if lowered in seen:
67
+ continue
68
+ seen.add(lowered)
69
+ normalized.append(cleaned)
70
+ return normalized
71
+
72
+
73
+ def normalize_ordered_args_list(value: object, *, field_name: str) -> list[str]:
74
+ if value is None:
75
+ return []
76
+ if not isinstance(value, list):
77
+ raise TypeError(f"{field_name} must be a list of strings.")
78
+ normalized: list[str] = []
79
+ for item in value:
80
+ if not isinstance(item, str):
81
+ raise TypeError(f"{field_name} items must be strings.")
82
+ cleaned = item.strip()
83
+ if cleaned:
84
+ normalized.append(cleaned)
85
+ return normalized
86
+
87
+
88
+ def normalize_ordered_string_list(
89
+ value: object,
90
+ *,
91
+ field_name: str,
92
+ ) -> list[str]:
93
+ if value is None:
94
+ return []
95
+ if not isinstance(value, list):
96
+ raise TypeError(f"{field_name} must be a list of strings.")
97
+ normalized: list[str] = []
98
+ seen: set[str] = set()
99
+ for item in value:
100
+ if not isinstance(item, str):
101
+ raise TypeError(f"{field_name} items must be strings.")
102
+ cleaned = item.strip()
103
+ if not cleaned or cleaned in seen:
104
+ continue
105
+ seen.add(cleaned)
106
+ normalized.append(cleaned)
107
+ return normalized
108
+
109
+
110
+ def normalize_optional_string(
111
+ value: object,
112
+ *,
113
+ field_name: str,
114
+ lowercase: bool = False,
115
+ ) -> str | None:
116
+ if value is None:
117
+ return None
118
+ if not isinstance(value, str):
119
+ raise TypeError(f"{field_name} must be a string.")
120
+ cleaned = value.strip()
121
+ if not cleaned:
122
+ return None
123
+ if lowercase:
124
+ cleaned = cleaned.lower()
125
+ return cleaned
126
+
127
+
128
+ def normalize_timeout(value: object, *, field_name: str) -> float:
129
+ if isinstance(value, bool):
130
+ raise TypeError(f"{field_name} must be a positive number.")
131
+ if not isinstance(value, (int, float)):
132
+ raise TypeError(f"{field_name} must be a positive number.")
133
+ normalized = float(value)
134
+ if normalized <= 0:
135
+ raise ValueError(f"{field_name} must be > 0.")
136
+ return normalized
137
+
138
+
139
+ def normalize_roots_mode(value: object, *, field_name: str) -> RootsMode:
140
+ normalized = normalize_optional_string(value, field_name=field_name, lowercase=True)
141
+ if normalized is None:
142
+ raise ValueError(f"{field_name} cannot be empty.")
143
+ if normalized not in {"disabled", "workspace"}:
144
+ raise ValueError(f"{field_name} must be 'disabled' or 'workspace'.")
145
+ return cast(RootsMode, normalized)
146
+
147
+
148
+ def normalize_resources_mode(value: object, *, field_name: str) -> ResourcesMode:
149
+ normalized = normalize_optional_string(value, field_name=field_name, lowercase=True)
150
+ if normalized is None:
151
+ raise ValueError(f"{field_name} cannot be empty.")
152
+ if normalized not in {"disabled", "listed_read_only"}:
153
+ raise ValueError(f"{field_name} must be 'disabled' or 'listed_read_only'.")
154
+ return cast(ResourcesMode, normalized)
155
+
156
+
157
+ def normalize_prompts_mode(value: object, *, field_name: str) -> PromptsMode:
158
+ normalized = normalize_optional_string(value, field_name=field_name, lowercase=True)
159
+ if normalized is None:
160
+ raise ValueError(f"{field_name} cannot be empty.")
161
+ if normalized not in {"disabled", "listed_get_only"}:
162
+ raise ValueError(f"{field_name} must be 'disabled' or 'listed_get_only'.")
163
+ return cast(PromptsMode, normalized)
164
+
165
+
166
+ def normalize_runtime_kind_list(value: object) -> list[RuntimeKind] | None:
167
+ if value is None:
168
+ return None
169
+ if not isinstance(value, list):
170
+ raise TypeError("enabled_in must be a list of runtime kinds.")
171
+ normalized: list[RuntimeKind] = []
172
+ seen: set[RuntimeKind] = set()
173
+ for item in value:
174
+ try:
175
+ kind = normalize_runtime_kind(item)
176
+ except AppConfigError as exc:
177
+ raise ValueError(str(exc)) from exc
178
+ if kind in seen:
179
+ continue
180
+ seen.add(kind)
181
+ normalized.append(kind)
182
+ return normalized
183
+
184
+
185
+ def normalize_string_mapping(
186
+ value: object,
187
+ *,
188
+ field_name: str,
189
+ key_validator: Callable[[str], None] | None = None,
190
+ ) -> dict[str, str] | None:
191
+ if value is None:
192
+ return None
193
+ if not isinstance(value, dict):
194
+ raise TypeError(f"{field_name} must be an object mapping strings to strings.")
195
+ normalized: dict[str, str] = {}
196
+ for raw_key, raw_item in value.items():
197
+ if not isinstance(raw_key, str):
198
+ raise TypeError(f"{field_name} keys must be strings.")
199
+ if not isinstance(raw_item, str):
200
+ raise TypeError(f"{field_name} values must be strings.")
201
+ key = raw_key.strip()
202
+ if not key:
203
+ raise ValueError(f"{field_name} keys cannot be empty.")
204
+ if key_validator is not None:
205
+ key_validator(key)
206
+ normalized[key] = raw_item.strip()
207
+ return normalized
208
+
209
+
210
+ def validate_http_url(value: str, *, field_name: str) -> str:
211
+ split = urlsplit(value)
212
+ if split.scheme not in {"http", "https"} or not split.netloc:
213
+ raise ValueError(f"{field_name} must be an http:// or https:// URL.")
214
+ return value
215
+
216
+
217
+ def _is_loopback_host(hostname: str | None) -> bool:
218
+ cleaned = str(hostname or "").strip()
219
+ if not cleaned:
220
+ return False
221
+ if cleaned.lower() == "localhost":
222
+ return True
223
+ try:
224
+ return ip_address(cleaned).is_loopback
225
+ except ValueError:
226
+ return False
227
+
228
+
229
+ def validate_https_url(
230
+ value: str,
231
+ *,
232
+ field_name: str,
233
+ allow_loopback_http: bool = False,
234
+ ) -> str:
235
+ split = urlsplit(value)
236
+ if not split.netloc:
237
+ raise ValueError(f"{field_name} must be an absolute URL.")
238
+ if split.scheme == "https":
239
+ return value
240
+ if split.scheme == "http" and allow_loopback_http and _is_loopback_host(split.hostname):
241
+ return value
242
+ if allow_loopback_http:
243
+ raise ValueError(
244
+ f"{field_name} must be an https:// URL (http:// is only allowed for loopback hosts)."
245
+ )
246
+ raise ValueError(f"{field_name} must be an https:// URL.")
247
+
248
+
249
+ def validate_tcp_port(value: object, *, field_name: str) -> int:
250
+ if isinstance(value, bool):
251
+ raise TypeError(f"{field_name} must be a valid TCP port.")
252
+ if not isinstance(value, int):
253
+ raise TypeError(f"{field_name} must be a valid TCP port.")
254
+ if not 1 <= int(value) <= 65535:
255
+ raise ValueError(f"{field_name} must be between 1 and 65535.")
256
+ return int(value)
257
+
258
+
259
+ def validate_redirect_host(value: str, *, field_name: str) -> str:
260
+ if any(marker in value for marker in ("://", "/", "?", "#", "@")):
261
+ raise ValueError(f"{field_name} must be a hostname or IP address without a port.")
262
+ if any(ch.isspace() for ch in value):
263
+ raise ValueError(f"{field_name} must be a hostname or IP address without whitespace.")
264
+ if ":" in value:
265
+ try:
266
+ ip_address(value)
267
+ except ValueError as exc:
268
+ raise ValueError(
269
+ f"{field_name} must be a hostname or IP address without a port."
270
+ ) from exc
271
+ return value
272
+ try:
273
+ ip_address(value)
274
+ return value
275
+ except ValueError:
276
+ pass
277
+ labels = value.split(".")
278
+ if any(not label for label in labels):
279
+ raise ValueError(f"{field_name} must be a hostname or IP address without a port.")
280
+ for label in labels:
281
+ if len(label) > 63 or not re.fullmatch(r"[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?", label):
282
+ raise ValueError(f"{field_name} must be a hostname or IP address without a port.")
283
+ return value
284
+
285
+
286
+ def validate_env_name(name: str) -> None:
287
+ if not _ENV_NAME_RE.fullmatch(name):
288
+ raise ValueError("env keys must be valid environment variable names.")
289
+
290
+
291
+ def validate_tool_policy_overlap(
292
+ *,
293
+ server_id: str,
294
+ allowed_tools: tuple[str, ...],
295
+ denied_tools: tuple[str, ...],
296
+ ) -> None:
297
+ overlap = sorted(set(allowed_tools) & set(denied_tools))
298
+ if overlap:
299
+ joined = ", ".join(overlap)
300
+ raise ConfigError(
301
+ f"MCP server '{server_id}' cannot allow and deny the same tool(s): {joined}"
302
+ )
303
+
304
+
305
+ class _StrictModel(BaseModel):
306
+ model_config = ConfigDict(extra="forbid")
307
+
308
+
309
+ class _PolicyModel(_StrictModel):
310
+ enabled: StrictBool = True
311
+ enabled_in: list[RuntimeKind] | None = None
312
+ trust: StrictStr = "explicit"
313
+ allowed_tools: list[StrictStr] = Field(default_factory=list)
314
+ denied_tools: list[StrictStr] = Field(default_factory=list)
315
+ startup_timeout_s: float = 10.0
316
+ call_timeout_s: float = 60.0
317
+ tool_prefix: StrictStr | None = None
318
+ roots_mode: RootsMode = "disabled"
319
+ resources_mode: ResourcesMode = "disabled"
320
+ prompts_mode: PromptsMode = "disabled"
321
+
322
+ @field_validator("enabled_in", mode="before")
323
+ @classmethod
324
+ def _normalize_enabled_in(cls, value: object) -> list[RuntimeKind] | None:
325
+ return normalize_runtime_kind_list(value)
326
+
327
+ @field_validator("trust", mode="before")
328
+ @classmethod
329
+ def _normalize_trust(cls, value: object) -> str:
330
+ normalized = normalize_optional_string(value, field_name="trust", lowercase=True)
331
+ if normalized is None:
332
+ raise ValueError("trust cannot be empty.")
333
+ if normalized != "explicit":
334
+ raise ValueError("trust must be 'explicit'; no other MCP trust modes are supported.")
335
+ return normalized
336
+
337
+ @field_validator("allowed_tools", "denied_tools", mode="before")
338
+ @classmethod
339
+ def _normalize_tool_lists(cls, value: object, info: Any) -> list[str]:
340
+ return normalize_ordered_name_list(value, field_name=str(info.field_name))
341
+
342
+ @field_validator("startup_timeout_s", "call_timeout_s", mode="before")
343
+ @classmethod
344
+ def _normalize_timeouts(cls, value: object, info: Any) -> float:
345
+ return normalize_timeout(value, field_name=str(info.field_name))
346
+
347
+ @field_validator("tool_prefix", mode="before")
348
+ @classmethod
349
+ def _normalize_tool_prefix(cls, value: object) -> str | None:
350
+ normalized = normalize_optional_string(value, field_name="tool_prefix", lowercase=True)
351
+ if normalized is None:
352
+ return None
353
+ if not _TOOL_PREFIX_RE.fullmatch(normalized):
354
+ raise ValueError("tool_prefix must match [a-z0-9][a-z0-9_-]*.")
355
+ return normalized
356
+
357
+ @field_validator("roots_mode", mode="before")
358
+ @classmethod
359
+ def _normalize_roots_mode(cls, value: object) -> RootsMode:
360
+ return normalize_roots_mode(value, field_name="roots_mode")
361
+
362
+ @field_validator("resources_mode", mode="before")
363
+ @classmethod
364
+ def _normalize_resources_mode(cls, value: object) -> ResourcesMode:
365
+ return normalize_resources_mode(value, field_name="resources_mode")
366
+
367
+ @field_validator("prompts_mode", mode="before")
368
+ @classmethod
369
+ def _normalize_prompts_mode(cls, value: object) -> PromptsMode:
370
+ return normalize_prompts_mode(value, field_name="prompts_mode")
371
+
372
+ @model_validator(mode="after")
373
+ def _validate_tool_policy_overlap(self) -> _PolicyModel:
374
+ overlap = sorted(set(self.allowed_tools) & set(self.denied_tools))
375
+ if overlap:
376
+ joined = ", ".join(overlap)
377
+ raise ValueError(f"allowed_tools and denied_tools cannot overlap: {joined}")
378
+ return self
379
+
380
+
381
+ class UserMcpStdioServer(_PolicyModel):
382
+ transport: Literal["stdio"]
383
+ command: StrictStr
384
+ args: list[StrictStr] = Field(default_factory=list)
385
+ env: dict[StrictStr, StrictStr] | None = None
386
+
387
+ @field_validator("command", mode="before")
388
+ @classmethod
389
+ def _normalize_command(cls, value: object) -> str:
390
+ normalized = normalize_optional_string(value, field_name="command")
391
+ if normalized is None:
392
+ raise ValueError("command cannot be empty.")
393
+ return normalized
394
+
395
+ @field_validator("args", mode="before")
396
+ @classmethod
397
+ def _normalize_args(cls, value: object) -> list[str]:
398
+ return normalize_ordered_args_list(value, field_name="args")
399
+
400
+ @field_validator("env", mode="before")
401
+ @classmethod
402
+ def _normalize_env(cls, value: object) -> dict[str, str] | None:
403
+ return normalize_string_mapping(value, field_name="env", key_validator=validate_env_name)
404
+
405
+
406
+ class UserMcpHttpOAuthConfig(_StrictModel):
407
+ client_id: StrictStr
408
+ redirect_host: StrictStr | None = None
409
+ redirect_port: StrictInt | None = None
410
+ scopes: list[StrictStr] | None = None
411
+ authorization_server_url: StrictStr | None = None
412
+
413
+ @field_validator("client_id", mode="before")
414
+ @classmethod
415
+ def _normalize_client_id(cls, value: object) -> str:
416
+ normalized = normalize_optional_string(value, field_name="client_id")
417
+ if normalized is None:
418
+ raise ValueError("client_id cannot be empty.")
419
+ return normalized
420
+
421
+ @field_validator("redirect_host", mode="before")
422
+ @classmethod
423
+ def _normalize_redirect_host(cls, value: object) -> str | None:
424
+ normalized = normalize_optional_string(value, field_name="redirect_host")
425
+ if normalized is None:
426
+ return None
427
+ return validate_redirect_host(normalized, field_name="redirect_host")
428
+
429
+ @field_validator("redirect_port", mode="before")
430
+ @classmethod
431
+ def _normalize_redirect_port(cls, value: object) -> int | None:
432
+ if value is None:
433
+ return None
434
+ return validate_tcp_port(value, field_name="redirect_port")
435
+
436
+ @field_validator("scopes", mode="before")
437
+ @classmethod
438
+ def _normalize_scopes(cls, value: object) -> list[str] | None:
439
+ if value is None:
440
+ return None
441
+ return normalize_ordered_string_list(value, field_name="scopes")
442
+
443
+ @field_validator("authorization_server_url", mode="before")
444
+ @classmethod
445
+ def _normalize_authorization_server_url(cls, value: object) -> str | None:
446
+ normalized = normalize_optional_string(value, field_name="authorization_server_url")
447
+ if normalized is None:
448
+ return None
449
+ return validate_https_url(
450
+ normalized,
451
+ field_name="authorization_server_url",
452
+ allow_loopback_http=True,
453
+ )
454
+
455
+
456
+ class UserMcpHttpServer(_PolicyModel):
457
+ transport: Literal["http"]
458
+ url: StrictStr
459
+ headers: dict[StrictStr, StrictStr] | None = None
460
+ oauth: UserMcpHttpOAuthConfig | None = None
461
+
462
+ @field_validator("url", mode="before")
463
+ @classmethod
464
+ def _normalize_url(cls, value: object) -> str:
465
+ normalized = normalize_optional_string(value, field_name="url")
466
+ if normalized is None:
467
+ raise ValueError("url cannot be empty.")
468
+ return validate_https_url(normalized, field_name="url", allow_loopback_http=True)
469
+
470
+ @field_validator("headers", mode="before")
471
+ @classmethod
472
+ def _normalize_headers(cls, value: object) -> dict[str, str] | None:
473
+ return normalize_string_mapping(value, field_name="headers")
474
+
475
+ @model_validator(mode="after")
476
+ def _validate_auth_configuration(self) -> UserMcpHttpServer:
477
+ if self.oauth is None or not self.headers:
478
+ return self
479
+ if any(str(key).casefold() == "authorization" for key in self.headers):
480
+ raise ValueError(
481
+ "oauth cannot be combined with headers.Authorization on the same HTTP server."
482
+ )
483
+ return self
484
+
485
+
486
+ UserMcpServerModel = Annotated[
487
+ UserMcpStdioServer | UserMcpHttpServer, Field(discriminator="transport")
488
+ ]
489
+
490
+
491
+ class ProjectMcpServerOverride(_StrictModel):
492
+ enabled: StrictBool | None = None
493
+ enabled_in: list[RuntimeKind] | None = None
494
+ allowed_tools: list[StrictStr] | None = None
495
+ denied_tools: list[StrictStr] | None = None
496
+ startup_timeout_s: float | None = None
497
+ call_timeout_s: float | None = None
498
+ roots_mode: RootsMode | None = None
499
+ resources_mode: ResourcesMode | None = None
500
+ prompts_mode: PromptsMode | None = None
501
+
502
+ @field_validator("enabled_in", mode="before")
503
+ @classmethod
504
+ def _normalize_enabled_in(cls, value: object) -> list[RuntimeKind] | None:
505
+ return normalize_runtime_kind_list(value)
506
+
507
+ @field_validator("allowed_tools", "denied_tools", mode="before")
508
+ @classmethod
509
+ def _normalize_tool_lists(cls, value: object, info: Any) -> list[str] | None:
510
+ if value is None:
511
+ return None
512
+ return normalize_ordered_name_list(value, field_name=str(info.field_name))
513
+
514
+ @field_validator("startup_timeout_s", "call_timeout_s", mode="before")
515
+ @classmethod
516
+ def _normalize_timeouts(cls, value: object, info: Any) -> float | None:
517
+ if value is None:
518
+ return None
519
+ return normalize_timeout(value, field_name=str(info.field_name))
520
+
521
+ @field_validator("roots_mode", mode="before")
522
+ @classmethod
523
+ def _normalize_roots_mode(cls, value: object) -> RootsMode | None:
524
+ if value is None:
525
+ return None
526
+ return normalize_roots_mode(value, field_name="roots_mode")
527
+
528
+ @field_validator("resources_mode", mode="before")
529
+ @classmethod
530
+ def _normalize_resources_mode(cls, value: object) -> ResourcesMode | None:
531
+ if value is None:
532
+ return None
533
+ return normalize_resources_mode(value, field_name="resources_mode")
534
+
535
+ @field_validator("prompts_mode", mode="before")
536
+ @classmethod
537
+ def _normalize_prompts_mode(cls, value: object) -> PromptsMode | None:
538
+ if value is None:
539
+ return None
540
+ return normalize_prompts_mode(value, field_name="prompts_mode")
541
+
542
+ @model_validator(mode="after")
543
+ def _validate_tool_policy_overlap(self) -> ProjectMcpServerOverride:
544
+ allowed_tools = tuple(self.allowed_tools or [])
545
+ denied_tools = tuple(self.denied_tools or [])
546
+ overlap = sorted(set(allowed_tools) & set(denied_tools))
547
+ if overlap:
548
+ joined = ", ".join(overlap)
549
+ raise ValueError(f"allowed_tools and denied_tools cannot overlap: {joined}")
550
+ return self
551
+
552
+
553
+ class UserMcpConfigFile(_StrictModel):
554
+ schema_version: StrictInt = 1
555
+ servers: dict[str, UserMcpServerModel] = Field(default_factory=dict)
556
+
557
+ @field_validator("schema_version")
558
+ @classmethod
559
+ def _validate_schema_version(cls, value: int) -> int:
560
+ if value != 1:
561
+ raise ValueError("schema_version must equal 1.")
562
+ return value
563
+
564
+
565
+ class ProjectMcpConfigFile(_StrictModel):
566
+ schema_version: StrictInt = 1
567
+ servers: dict[str, ProjectMcpServerOverride] = Field(default_factory=dict)
568
+
569
+ @field_validator("schema_version")
570
+ @classmethod
571
+ def _validate_schema_version(cls, value: int) -> int:
572
+ if value != 1:
573
+ raise ValueError("schema_version must equal 1.")
574
+ return value
575
+
576
+
577
+ def _redact_sensitive_strings(values: tuple[str, ...]) -> list[str]:
578
+ return ["[redacted]" for _ in values]
579
+
580
+
581
+ @dataclass(frozen=True)
582
+ class ResolvedMcpHttpOAuthConfig:
583
+ client_id: str
584
+ redirect_host: str | None = None
585
+ redirect_port: int | None = None
586
+ scopes: tuple[str, ...] = field(default_factory=tuple)
587
+ authorization_server_url: str | None = None
588
+
589
+
590
+ @dataclass(frozen=True)
591
+ class ResolvedMcpServer:
592
+ id: str
593
+ transport: TransportKind
594
+ enabled: bool
595
+ enabled_in: tuple[RuntimeKind, ...] | None
596
+ trust: str
597
+ allowed_tools: tuple[str, ...]
598
+ denied_tools: tuple[str, ...]
599
+ startup_timeout_s: float
600
+ call_timeout_s: float
601
+ tool_prefix: str | None
602
+ roots_mode: RootsMode = "disabled"
603
+ resources_mode: ResourcesMode = "disabled"
604
+ prompts_mode: PromptsMode = "disabled"
605
+ command: str | None = None
606
+ args: tuple[str, ...] = field(default_factory=tuple, repr=False)
607
+ env: dict[str, str] = field(default_factory=dict, repr=False)
608
+ url: str | None = field(default=None, repr=False)
609
+ headers: dict[str, str] = field(default_factory=dict, repr=False)
610
+ oauth: ResolvedMcpHttpOAuthConfig | None = field(default=None, repr=False)
611
+
612
+ def enabled_for(self, runtime_kind: RuntimeKind) -> bool:
613
+ if not self.enabled:
614
+ return False
615
+ if self.enabled_in is None:
616
+ return True
617
+ return runtime_kind in self.enabled_in
618
+
619
+ def redacted_connection_payload(self) -> dict[str, Any]:
620
+ payload: dict[str, Any] = {
621
+ "id": self.id,
622
+ "transport": self.transport,
623
+ "enabled": self.enabled,
624
+ "enabled_in": [kind.value for kind in self.enabled_in] if self.enabled_in else None,
625
+ "trust": self.trust,
626
+ "allowed_tools": list(self.allowed_tools),
627
+ "denied_tools": list(self.denied_tools),
628
+ "startup_timeout_s": self.startup_timeout_s,
629
+ "call_timeout_s": self.call_timeout_s,
630
+ "tool_prefix": self.tool_prefix,
631
+ "roots_mode": self.roots_mode,
632
+ "resources_mode": self.resources_mode,
633
+ "prompts_mode": self.prompts_mode,
634
+ }
635
+ if self.transport == "stdio":
636
+ payload["command"] = self.command
637
+ payload["args"] = _redact_sensitive_strings(self.args)
638
+ payload["env"] = {key: "[redacted]" for key in self.env}
639
+ else:
640
+ payload["headers"] = {key: "[redacted]" for key in self.headers}
641
+ if self.oauth is not None:
642
+ payload["oauth"] = {
643
+ "client_id": self.oauth.client_id,
644
+ "redirect_host": self.oauth.redirect_host,
645
+ "redirect_port": self.oauth.redirect_port,
646
+ "scopes": list(self.oauth.scopes),
647
+ "authorization_server_url": self.oauth.authorization_server_url,
648
+ }
649
+ return payload
650
+
651
+
652
+ @dataclass(frozen=True)
653
+ class ResolvedMcpConfig:
654
+ workspace_root: Path
655
+ user_config_path: Path
656
+ project_config_path: Path
657
+ user_config_present: bool
658
+ project_config_present: bool
659
+ servers: tuple[ResolvedMcpServer, ...] = ()
660
+
661
+ @property
662
+ def has_any_config(self) -> bool:
663
+ return self.user_config_present or self.project_config_present
664
+
665
+ def active_servers_for(self, runtime_kind: RuntimeKind) -> tuple[ResolvedMcpServer, ...]:
666
+ return tuple(server for server in self.servers if server.enabled_for(runtime_kind))