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,744 @@
1
+ """Production coordinator for IDE-owned MCP OAuth authorization-code login.
2
+
3
+ The coordinator is intentionally protocol-agnostic. It starts a loopback
4
+ listener, returns public flow metadata immediately, and completes discovery,
5
+ callback validation, code exchange, and encrypted credential persistence
6
+ without placing authorization codes or tokens in a caller-visible object.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import hmac
12
+ import html
13
+ import ipaddress
14
+ import math
15
+ import secrets
16
+ import threading
17
+ import time
18
+ from collections.abc import Callable, Iterable
19
+ from dataclasses import dataclass, field
20
+ from datetime import UTC, datetime
21
+ from http import HTTPStatus
22
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
23
+ from pathlib import Path
24
+ from typing import Protocol
25
+ from urllib.parse import parse_qs, urlsplit
26
+
27
+ from ..mcp.oauth import (
28
+ McpAuthorizationServerMetadata,
29
+ canonical_mcp_resource_uri,
30
+ discover_authorization_server_metadata,
31
+ exchange_authorization_code,
32
+ )
33
+ from ..mcp.oauth_store import (
34
+ McpOAuthTokenRecord,
35
+ delete_oauth_token_record,
36
+ load_oauth_token_record,
37
+ save_oauth_token_record,
38
+ )
39
+ from .mcp_oauth_lifecycle import (
40
+ AuthorizationCodeFlowRequest,
41
+ McpOAuthFlowRegistry,
42
+ OAuthCompletionClaim,
43
+ OAuthCompletionRejectedError,
44
+ OAuthFlowState,
45
+ OAuthFlowStateError,
46
+ OAuthFlowStatus,
47
+ OAuthLogoutResult,
48
+ OAuthTokenSet,
49
+ OAuthTokenVault,
50
+ OAuthValidationError,
51
+ )
52
+
53
+ __all__ = [
54
+ "EncryptedMcpOAuthTokenVault",
55
+ "McpOAuthCoordinatorConfig",
56
+ "McpOAuthCoordinatorError",
57
+ "McpOAuthIdeCoordinator",
58
+ "McpOAuthLoginRequest",
59
+ ]
60
+
61
+ _CALLBACK_PREFIX = "/oauth/callback/"
62
+ _MAX_CALLBACK_TARGET_BYTES = 16 * 1024
63
+ _MAX_AUTHORIZATION_CODE_BYTES = 8 * 1024
64
+ _MAX_STATE_BYTES = 2 * 1024
65
+ _TOKEN_VAULT_LOCK = threading.RLock()
66
+
67
+
68
+ class McpOAuthCoordinatorError(RuntimeError):
69
+ """Coordinator error safe for logs and IDE protocol error messages."""
70
+
71
+
72
+ class _DiscoveryCallable(Protocol):
73
+ def __call__(
74
+ self,
75
+ *,
76
+ server_id: str,
77
+ resource_server_url: str,
78
+ authorization_server_url: str | None = None,
79
+ timeout_s: float = 10.0,
80
+ ) -> McpAuthorizationServerMetadata: ...
81
+
82
+
83
+ class _ExchangeCallable(Protocol):
84
+ def __call__(
85
+ self,
86
+ *,
87
+ server_id: str,
88
+ authorization_server_metadata: McpAuthorizationServerMetadata,
89
+ resource_server_url: str,
90
+ client_id: str,
91
+ code: str,
92
+ redirect_uri: str,
93
+ code_verifier: str,
94
+ requested_scopes: tuple[str, ...],
95
+ timeout_s: float,
96
+ ) -> McpOAuthTokenRecord: ...
97
+
98
+
99
+ @dataclass(frozen=True, slots=True)
100
+ class McpOAuthLoginRequest:
101
+ server_id: str
102
+ resource_server_url: str
103
+ client_id: str = field(repr=False)
104
+ scopes: tuple[str, ...] = field(default_factory=tuple)
105
+ authorization_server_url: str | None = None
106
+
107
+
108
+ @dataclass(frozen=True, slots=True)
109
+ class McpOAuthCoordinatorConfig:
110
+ callback_host: str = "127.0.0.1"
111
+ callback_port: int = 0
112
+ callback_timeout_seconds: float = 10 * 60.0
113
+ discovery_timeout_seconds: float = 10.0
114
+ token_timeout_seconds: float = 15.0
115
+ shutdown_wait_seconds: float = 20.0
116
+
117
+ def __post_init__(self) -> None:
118
+ if self.callback_host != "127.0.0.1":
119
+ raise OAuthValidationError("MCP OAuth callback host must be 127.0.0.1.")
120
+ if (
121
+ isinstance(self.callback_port, bool)
122
+ or not isinstance(self.callback_port, int)
123
+ or self.callback_port < 0
124
+ or self.callback_port > 65535
125
+ or 0 < self.callback_port < 1024
126
+ ):
127
+ raise OAuthValidationError("MCP OAuth callback port is invalid.")
128
+ _bounded_seconds(
129
+ self.callback_timeout_seconds,
130
+ field_name="callback timeout",
131
+ minimum=30.0,
132
+ maximum=25 * 60.0,
133
+ )
134
+ _bounded_seconds(
135
+ self.discovery_timeout_seconds,
136
+ field_name="discovery timeout",
137
+ minimum=1.0,
138
+ maximum=60.0,
139
+ )
140
+ _bounded_seconds(
141
+ self.token_timeout_seconds,
142
+ field_name="token timeout",
143
+ minimum=1.0,
144
+ maximum=120.0,
145
+ )
146
+ _bounded_seconds(
147
+ self.shutdown_wait_seconds,
148
+ field_name="shutdown wait",
149
+ minimum=0.0,
150
+ maximum=120.0,
151
+ )
152
+
153
+
154
+ class EncryptedMcpOAuthTokenVault(OAuthTokenVault):
155
+ """Adapter from lifecycle credentials to Alysis Code's encrypted OAuth store."""
156
+
157
+ def store(self, server_id: str, tokens: OAuthTokenSet, *, binding_id: str) -> None:
158
+ if tokens.expires_at is None:
159
+ raise OAuthValidationError("MCP OAuth token expiry is missing.")
160
+ expires_at = datetime.fromtimestamp(float(tokens.expires_at), tz=UTC).replace(microsecond=0)
161
+ obtained_at = datetime.now(UTC).replace(microsecond=0)
162
+ record = McpOAuthTokenRecord(
163
+ access_token=tokens.access_token,
164
+ refresh_token=tokens.refresh_token,
165
+ token_type=tokens.token_type,
166
+ expires_at=expires_at,
167
+ granted_scopes=tokens.scopes,
168
+ obtained_at=obtained_at,
169
+ binding_id=binding_id,
170
+ )
171
+ with _TOKEN_VAULT_LOCK:
172
+ save_oauth_token_record(server_id, record)
173
+
174
+ def load(self, server_id: str) -> OAuthTokenSet | None:
175
+ with _TOKEN_VAULT_LOCK:
176
+ record = load_oauth_token_record(server_id)
177
+ if record is None:
178
+ return None
179
+ return _tokens_from_record(record)
180
+
181
+ def has_binding(self, server_id: str, *, binding_id: str) -> bool:
182
+ with _TOKEN_VAULT_LOCK:
183
+ record = load_oauth_token_record(server_id)
184
+ if record is None or record.binding_id is None:
185
+ return False
186
+ return hmac.compare_digest(record.binding_id, binding_id)
187
+
188
+ def delete(self, server_id: str) -> bool:
189
+ with _TOKEN_VAULT_LOCK:
190
+ return delete_oauth_token_record(server_id)
191
+
192
+
193
+ @dataclass(frozen=True, slots=True)
194
+ class _CallbackPayload:
195
+ code: str | None = field(repr=False)
196
+ state: str | None = field(repr=False)
197
+ provider_error: bool = False
198
+ malformed: bool = False
199
+
200
+
201
+ class _CallbackInbox:
202
+ def __init__(self) -> None:
203
+ self.event = threading.Event()
204
+ self._lock = threading.Lock()
205
+ self._payload: _CallbackPayload | None = None
206
+ self._expected_state: str | None = None
207
+
208
+ def expect_state(self, state: str) -> None:
209
+ with self._lock:
210
+ if self._expected_state is not None:
211
+ raise McpOAuthCoordinatorError("MCP OAuth callback state is already configured.")
212
+ self._expected_state = state
213
+
214
+ def deliver(self, payload: _CallbackPayload) -> bool:
215
+ with self._lock:
216
+ if (
217
+ self._expected_state is None
218
+ or payload.state is None
219
+ or not hmac.compare_digest(self._expected_state, payload.state)
220
+ ):
221
+ return False
222
+ if self._payload is not None:
223
+ return False
224
+ self._payload = payload
225
+ self.event.set()
226
+ return True
227
+
228
+ def read(self) -> _CallbackPayload | None:
229
+ with self._lock:
230
+ return self._payload
231
+
232
+
233
+ class _LoopbackHttpServer(ThreadingHTTPServer):
234
+ daemon_threads = True
235
+ allow_reuse_address = False
236
+
237
+
238
+ class _LoopbackCallbackHandler(BaseHTTPRequestHandler):
239
+ server_version = "AlysisOAuthCallback/2.0"
240
+ sys_version = ""
241
+
242
+ def log_message(self, format: str, *args: object) -> None: # noqa: A003
243
+ del format, args
244
+
245
+ @property
246
+ def callback_path(self) -> str:
247
+ return str(self.server.callback_path) # type: ignore[attr-defined]
248
+
249
+ @property
250
+ def inbox(self) -> _CallbackInbox:
251
+ return self.server.callback_inbox # type: ignore[attr-defined,no-any-return]
252
+
253
+ def do_GET(self) -> None: # noqa: N802
254
+ if self.headers.get("Host", "") != str(
255
+ self.server.expected_host_header # type: ignore[attr-defined]
256
+ ):
257
+ self._send_page(HTTPStatus.BAD_REQUEST, succeeded=False)
258
+ return
259
+ if len(self.path.encode("utf-8", errors="ignore")) > _MAX_CALLBACK_TARGET_BYTES:
260
+ self._send_page(HTTPStatus.REQUEST_URI_TOO_LONG, succeeded=False)
261
+ return
262
+ parsed = urlsplit(self.path)
263
+ if parsed.path != self.callback_path:
264
+ self._send_page(HTTPStatus.NOT_FOUND, succeeded=False)
265
+ return
266
+ try:
267
+ query = parse_qs(
268
+ parsed.query,
269
+ keep_blank_values=True,
270
+ strict_parsing=True,
271
+ max_num_fields=8,
272
+ )
273
+ except ValueError:
274
+ self.inbox.deliver(_CallbackPayload(code=None, state=None, malformed=True))
275
+ self._send_page(HTTPStatus.BAD_REQUEST, succeeded=False)
276
+ return
277
+ code = _single_callback_value(query, "code", maximum=_MAX_AUTHORIZATION_CODE_BYTES)
278
+ state = _single_callback_value(query, "state", maximum=_MAX_STATE_BYTES)
279
+ provider_error = "error" in query
280
+ malformed = (not provider_error and code is None) or state is None
281
+ accepted = self.inbox.deliver(
282
+ _CallbackPayload(
283
+ code=code,
284
+ state=state,
285
+ provider_error=provider_error,
286
+ malformed=malformed,
287
+ )
288
+ )
289
+ self._send_page(
290
+ HTTPStatus.OK if accepted else HTTPStatus.CONFLICT,
291
+ succeeded=accepted and not provider_error and not malformed,
292
+ )
293
+
294
+ def _send_page(self, status: HTTPStatus, *, succeeded: bool) -> None:
295
+ message = (
296
+ "Authorization received. Return to Visual Studio Code."
297
+ if succeeded
298
+ else "Authorization could not be accepted. Return to Visual Studio Code."
299
+ )
300
+ body = (
301
+ '<!doctype html><html><head><meta charset="utf-8">'
302
+ '<meta name="viewport" content="width=device-width,initial-scale=1">'
303
+ f"<title>Alysis Code OAuth</title></head><body><p>{html.escape(message)}</p>"
304
+ "<script>window.history.replaceState({},document.title,window.location.pathname);"
305
+ "</script></body></html>"
306
+ ).encode()
307
+ self.send_response(int(status))
308
+ self.send_header("Content-Type", "text/html; charset=utf-8")
309
+ self.send_header("Content-Length", str(len(body)))
310
+ self.send_header("Cache-Control", "no-store")
311
+ self.send_header("Pragma", "no-cache")
312
+ self.send_header("Referrer-Policy", "no-referrer")
313
+ self.send_header(
314
+ "Content-Security-Policy", "default-src 'none'; script-src 'unsafe-inline'"
315
+ )
316
+ self.send_header("X-Content-Type-Options", "nosniff")
317
+ self.end_headers()
318
+ try:
319
+ self.wfile.write(body)
320
+ except (BrokenPipeError, ConnectionResetError):
321
+ return
322
+
323
+
324
+ class _LoopbackCallbackListener:
325
+ def __init__(self, *, host: str, port: int) -> None:
326
+ self.callback_path = f"{_CALLBACK_PREFIX}{secrets.token_urlsafe(24)}"
327
+ self.inbox = _CallbackInbox()
328
+ try:
329
+ self._server = _LoopbackHttpServer((host, port), _LoopbackCallbackHandler)
330
+ except OSError as exc:
331
+ raise McpOAuthCoordinatorError("MCP OAuth callback listener could not start.") from exc
332
+ self._server.callback_path = self.callback_path # type: ignore[attr-defined]
333
+ self._server.callback_inbox = self.inbox # type: ignore[attr-defined]
334
+ bound_host, bound_port = self._server.server_address[:2]
335
+ self._server.expected_host_header = f"{bound_host}:{bound_port}" # type: ignore[attr-defined]
336
+ self._closed = False
337
+ self._close_lock = threading.Lock()
338
+ self._thread = threading.Thread(
339
+ target=self._serve,
340
+ name="alysis-mcp-oauth-callback",
341
+ daemon=True,
342
+ )
343
+ self._thread.start()
344
+
345
+ def _serve(self) -> None:
346
+ self._server.serve_forever(poll_interval=0.05)
347
+
348
+ @property
349
+ def redirect_uri(self) -> str:
350
+ host, port = self._server.server_address[:2]
351
+ return f"http://{host}:{port}{self.callback_path}"
352
+
353
+ def expect_state(self, state: str) -> None:
354
+ self.inbox.expect_state(state)
355
+
356
+ def wait(
357
+ self, *, timeout_seconds: float, cancel_event: threading.Event
358
+ ) -> _CallbackPayload | None:
359
+ deadline = time.monotonic() + timeout_seconds
360
+ while not cancel_event.is_set():
361
+ remaining = deadline - time.monotonic()
362
+ if remaining <= 0:
363
+ return None
364
+ if self.inbox.event.wait(min(0.05, remaining)):
365
+ return self.inbox.read()
366
+ return None
367
+
368
+ def close(self) -> None:
369
+ with self._close_lock:
370
+ if self._closed:
371
+ return
372
+ self._closed = True
373
+ self._server.shutdown()
374
+ self._server.server_close()
375
+ if threading.current_thread() is not self._thread:
376
+ self._thread.join(timeout=2.0)
377
+
378
+
379
+ @dataclass(slots=True)
380
+ class _ActiveFlow:
381
+ server_id: str
382
+ registry: McpOAuthFlowRegistry
383
+ listener: _LoopbackCallbackListener
384
+ cancel_event: threading.Event
385
+ thread: threading.Thread | None = None
386
+
387
+
388
+ class McpOAuthIdeCoordinator:
389
+ """Own asynchronous loopback authorization for one IDE bridge process."""
390
+
391
+ def __init__(
392
+ self,
393
+ *,
394
+ registry_path: str | Path | None = None,
395
+ workspace_roots: Iterable[str | Path] = (),
396
+ vault: OAuthTokenVault | None = None,
397
+ config: McpOAuthCoordinatorConfig | None = None,
398
+ discovery: _DiscoveryCallable = discover_authorization_server_metadata,
399
+ exchanger: _ExchangeCallable = exchange_authorization_code,
400
+ revoker: Callable[[OAuthTokenSet], None] | None = None,
401
+ ) -> None:
402
+ self.config = config or McpOAuthCoordinatorConfig()
403
+ self.vault = vault or EncryptedMcpOAuthTokenVault()
404
+ self._workspace_roots = tuple(workspace_roots)
405
+ self._registry = McpOAuthFlowRegistry(
406
+ self.vault,
407
+ registry_path,
408
+ workspace_roots=self._workspace_roots,
409
+ completion_lease_seconds=min(
410
+ 10 * 60.0,
411
+ max(30.0, self.config.token_timeout_seconds + 15.0),
412
+ ),
413
+ )
414
+ self._discovery = discovery
415
+ self._exchanger = exchanger
416
+ self._revoker = revoker
417
+ self._lock = threading.RLock()
418
+ self._completion_lock = threading.RLock()
419
+ self._active: dict[str, _ActiveFlow] = {}
420
+ self._blocked_servers: set[str] = set()
421
+ self._closed = False
422
+
423
+ def start_authorization_code(self, request: McpOAuthLoginRequest) -> OAuthFlowStatus:
424
+ with self._lock:
425
+ if self._closed:
426
+ raise McpOAuthCoordinatorError("MCP OAuth coordinator is closed.")
427
+
428
+ resource_server_url = _secure_resource_url(request.resource_server_url)
429
+ try:
430
+ metadata = self._discovery(
431
+ server_id=request.server_id,
432
+ resource_server_url=resource_server_url,
433
+ authorization_server_url=request.authorization_server_url,
434
+ timeout_s=self.config.discovery_timeout_seconds,
435
+ )
436
+ except Exception: # noqa: BLE001 - discovery providers may include sensitive details
437
+ raise McpOAuthCoordinatorError("MCP OAuth metadata discovery failed.") from None
438
+
439
+ listener = _LoopbackCallbackListener(
440
+ host=self.config.callback_host,
441
+ port=self.config.callback_port,
442
+ )
443
+ flow_registry = McpOAuthFlowRegistry(
444
+ self.vault,
445
+ self._registry.path,
446
+ redirect_allowlist=(listener.redirect_uri,),
447
+ workspace_roots=self._workspace_roots,
448
+ completion_lease_seconds=min(
449
+ 10 * 60.0,
450
+ max(30.0, self.config.token_timeout_seconds + 15.0),
451
+ ),
452
+ )
453
+ try:
454
+ status = flow_registry.start_authorization_code(
455
+ AuthorizationCodeFlowRequest(
456
+ server_id=request.server_id,
457
+ authorization_endpoint=metadata.authorization_endpoint,
458
+ token_endpoint=metadata.token_endpoint,
459
+ client_id=request.client_id,
460
+ redirect_uri=listener.redirect_uri,
461
+ scopes=request.scopes,
462
+ extra_authorization_params={"resource": resource_server_url},
463
+ expires_in=(
464
+ self.config.callback_timeout_seconds
465
+ + self.config.token_timeout_seconds
466
+ + 30.0
467
+ ),
468
+ # MCP OAuth does not require an OpenID id_token. State and
469
+ # PKCE remain mandatory; nonce is still sent when accepted.
470
+ require_nonce=False,
471
+ )
472
+ )
473
+ listener.expect_state(_state_from_authorization_url(status.authorization_url))
474
+ except Exception:
475
+ listener.close()
476
+ raise
477
+
478
+ active = _ActiveFlow(
479
+ server_id=status.server_id,
480
+ registry=flow_registry,
481
+ listener=listener,
482
+ cancel_event=threading.Event(),
483
+ )
484
+ worker = threading.Thread(
485
+ target=self._run_flow,
486
+ args=(active, status.flow_id, metadata, resource_server_url),
487
+ name=f"alysis-mcp-oauth-{status.flow_id[:8]}",
488
+ daemon=True,
489
+ )
490
+ active.thread = worker
491
+ with self._completion_lock:
492
+ self._blocked_servers.discard(status.server_id)
493
+ with self._lock:
494
+ if self._closed:
495
+ listener.close()
496
+ flow_registry.cancel(status.flow_id)
497
+ raise McpOAuthCoordinatorError("MCP OAuth coordinator is closed.")
498
+ self._active[status.flow_id] = active
499
+ try:
500
+ worker.start()
501
+ except Exception:
502
+ with self._lock:
503
+ self._active.pop(status.flow_id, None)
504
+ listener.close()
505
+ flow_registry.fail_pending(status.flow_id, error_code="worker_start_failed")
506
+ raise McpOAuthCoordinatorError("MCP OAuth worker could not start.") from None
507
+ return status
508
+
509
+ def status(self, flow_id: str) -> OAuthFlowStatus:
510
+ status = self._registry.status(flow_id)
511
+ if status.state in {
512
+ OAuthFlowState.COMPLETED,
513
+ OAuthFlowState.CANCELLED,
514
+ OAuthFlowState.FAILED,
515
+ OAuthFlowState.EXPIRED,
516
+ }:
517
+ self._close_terminal_handle(flow_id)
518
+ return status
519
+
520
+ def cancel(self, flow_id: str) -> OAuthFlowStatus:
521
+ with self._lock:
522
+ active = self._active.get(flow_id)
523
+ if active is not None:
524
+ active.cancel_event.set()
525
+ active.listener.close()
526
+ status = self._registry.status(flow_id)
527
+ if status.state is OAuthFlowState.PENDING:
528
+ registry = active.registry if active is not None else self._registry
529
+ return registry.cancel(flow_id)
530
+ return status
531
+
532
+ def logout(self, server_id: str) -> OAuthLogoutResult:
533
+ with self._completion_lock:
534
+ self._blocked_servers.add(server_id)
535
+ with self._lock:
536
+ active_items = [
537
+ (flow_id, active)
538
+ for flow_id, active in self._active.items()
539
+ if active.server_id == server_id
540
+ ]
541
+ for flow_id, active in active_items:
542
+ active.cancel_event.set()
543
+ active.listener.close()
544
+ try:
545
+ status = active.registry.status(flow_id)
546
+ if status.state is OAuthFlowState.PENDING:
547
+ active.registry.cancel(flow_id)
548
+ except Exception: # noqa: BLE001 - logout continues local cleanup
549
+ pass
550
+ return self._registry.logout(server_id, revoker=self._revoker)
551
+
552
+ def close(self) -> None:
553
+ with self._lock:
554
+ if self._closed:
555
+ return
556
+ self._closed = True
557
+ active_items = list(self._active.items())
558
+ with self._completion_lock:
559
+ self._blocked_servers.update(active.server_id for _, active in active_items)
560
+ for flow_id, active in active_items:
561
+ active.cancel_event.set()
562
+ active.listener.close()
563
+ try:
564
+ status = active.registry.status(flow_id)
565
+ if status.state is OAuthFlowState.PENDING:
566
+ active.registry.cancel(flow_id)
567
+ except Exception: # noqa: BLE001 - shutdown is best effort after listener close
568
+ pass
569
+ deadline = time.monotonic() + self.config.shutdown_wait_seconds
570
+ for _, active in active_items:
571
+ thread = active.thread
572
+ if thread is None or thread is threading.current_thread():
573
+ continue
574
+ thread.join(timeout=max(0.0, deadline - time.monotonic()))
575
+ with self._lock:
576
+ self._active.clear()
577
+
578
+ def __enter__(self) -> McpOAuthIdeCoordinator:
579
+ return self
580
+
581
+ def __exit__(self, exc_type: object, exc: object, traceback: object) -> None:
582
+ del exc_type, exc, traceback
583
+ self.close()
584
+
585
+ def _run_flow(
586
+ self,
587
+ active: _ActiveFlow,
588
+ flow_id: str,
589
+ metadata: McpAuthorizationServerMetadata,
590
+ resource_server_url: str,
591
+ ) -> None:
592
+ claim: OAuthCompletionClaim | None = None
593
+ try:
594
+ payload = active.listener.wait(
595
+ timeout_seconds=self.config.callback_timeout_seconds,
596
+ cancel_event=active.cancel_event,
597
+ )
598
+ if active.cancel_event.is_set():
599
+ self._cancel_pending_if_possible(active.registry, flow_id)
600
+ return
601
+ if payload is None:
602
+ self._fail_pending_if_possible(active.registry, flow_id, "callback_timeout")
603
+ return
604
+ if payload.provider_error:
605
+ self._fail_pending_if_possible(active.registry, flow_id, "provider_denied")
606
+ return
607
+ if payload.malformed or payload.code is None or payload.state is None:
608
+ self._fail_pending_if_possible(active.registry, flow_id, "invalid_callback")
609
+ return
610
+ try:
611
+ claim = active.registry.begin_completion(flow_id, state=payload.state)
612
+ except OAuthCompletionRejectedError:
613
+ self._fail_pending_if_possible(active.registry, flow_id, "callback_rejected")
614
+ return
615
+ if active.cancel_event.is_set():
616
+ active.registry.fail_completion(claim, error_code="cancelled_by_user")
617
+ return
618
+ material = claim.material
619
+ if material.pkce_verifier is None or material.redirect_uri is None:
620
+ active.registry.fail_completion(claim, error_code="exchange_material_missing")
621
+ return
622
+ try:
623
+ record = self._exchanger(
624
+ server_id=material.server_id,
625
+ authorization_server_metadata=metadata,
626
+ resource_server_url=resource_server_url,
627
+ client_id=material.client_id,
628
+ code=payload.code,
629
+ redirect_uri=material.redirect_uri,
630
+ code_verifier=material.pkce_verifier,
631
+ requested_scopes=material.scopes,
632
+ timeout_s=self.config.token_timeout_seconds,
633
+ )
634
+ except Exception: # noqa: BLE001 - token/code details must not escape the worker
635
+ active.registry.fail_completion(claim, error_code="token_exchange_failed")
636
+ return
637
+ with self._completion_lock:
638
+ if active.cancel_event.is_set() or active.server_id in self._blocked_servers:
639
+ active.registry.fail_completion(claim, error_code="cancelled_by_user")
640
+ return
641
+ active.registry.finish_completion(claim, _tokens_from_record(record))
642
+ except OAuthFlowStateError:
643
+ return
644
+ except Exception: # noqa: BLE001 - convert every worker failure to an allowlisted code
645
+ if claim is not None:
646
+ try:
647
+ active.registry.fail_completion(claim, error_code="authorization_worker_failed")
648
+ except Exception: # noqa: BLE001
649
+ pass
650
+ else:
651
+ self._fail_pending_if_possible(
652
+ active.registry, flow_id, "authorization_worker_failed"
653
+ )
654
+ finally:
655
+ active.listener.close()
656
+ with self._lock:
657
+ if self._active.get(flow_id) is active:
658
+ self._active.pop(flow_id, None)
659
+
660
+ def _close_terminal_handle(self, flow_id: str) -> None:
661
+ with self._lock:
662
+ active = self._active.get(flow_id)
663
+ if active is not None:
664
+ active.listener.close()
665
+
666
+ @staticmethod
667
+ def _fail_pending_if_possible(
668
+ registry: McpOAuthFlowRegistry, flow_id: str, error_code: str
669
+ ) -> None:
670
+ try:
671
+ if registry.status(flow_id).state is OAuthFlowState.PENDING:
672
+ registry.fail_pending(flow_id, error_code=error_code)
673
+ except Exception: # noqa: BLE001
674
+ return
675
+
676
+ @staticmethod
677
+ def _cancel_pending_if_possible(registry: McpOAuthFlowRegistry, flow_id: str) -> None:
678
+ try:
679
+ if registry.status(flow_id).state is OAuthFlowState.PENDING:
680
+ registry.cancel(flow_id)
681
+ except Exception: # noqa: BLE001
682
+ return
683
+
684
+
685
+ def _tokens_from_record(record: McpOAuthTokenRecord) -> OAuthTokenSet:
686
+ return OAuthTokenSet(
687
+ access_token=record.access_token,
688
+ refresh_token=record.refresh_token,
689
+ token_type=record.token_type,
690
+ expires_at=record.expires_at.timestamp(),
691
+ scopes=record.granted_scopes,
692
+ )
693
+
694
+
695
+ def _single_callback_value(query: dict[str, list[str]], key: str, *, maximum: int) -> str | None:
696
+ values = query.get(key)
697
+ if values is None or len(values) != 1:
698
+ return None
699
+ value = values[0]
700
+ if not value or len(value.encode("utf-8")) > maximum:
701
+ return None
702
+ return value
703
+
704
+
705
+ def _state_from_authorization_url(authorization_url: str | None) -> str:
706
+ if authorization_url is None:
707
+ raise McpOAuthCoordinatorError("MCP OAuth authorization URL is missing.")
708
+ try:
709
+ values = parse_qs(urlsplit(authorization_url).query, max_num_fields=32)["state"]
710
+ except (KeyError, ValueError):
711
+ raise McpOAuthCoordinatorError("MCP OAuth authorization state is missing.") from None
712
+ if len(values) != 1 or not values[0]:
713
+ raise McpOAuthCoordinatorError("MCP OAuth authorization state is invalid.")
714
+ return values[0]
715
+
716
+
717
+ def _secure_resource_url(value: str) -> str:
718
+ try:
719
+ split = urlsplit(value)
720
+ host = str(split.hostname or "")
721
+ if not host or split.username is not None or split.password is not None or split.fragment:
722
+ raise ValueError
723
+ if split.scheme.lower() == "http":
724
+ if not ipaddress.ip_address(host).is_loopback:
725
+ raise ValueError
726
+ elif split.scheme.lower() != "https":
727
+ raise ValueError
728
+ return canonical_mcp_resource_uri(value)
729
+ except (TypeError, ValueError):
730
+ raise OAuthValidationError(
731
+ "MCP OAuth resource server must use HTTPS or loopback HTTP."
732
+ ) from None
733
+
734
+
735
+ def _bounded_seconds(value: object, *, field_name: str, minimum: float, maximum: float) -> float:
736
+ if (
737
+ isinstance(value, bool)
738
+ or not isinstance(value, (int, float))
739
+ or not math.isfinite(float(value))
740
+ or float(value) < minimum
741
+ or float(value) > maximum
742
+ ):
743
+ raise OAuthValidationError(f"MCP OAuth {field_name} is outside the allowed range.")
744
+ return float(value)