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,972 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import base64
5
+ import hashlib
6
+ import re
7
+ import secrets
8
+ from dataclasses import dataclass, field
9
+ from datetime import UTC, datetime, timedelta
10
+ from typing import Any, Literal
11
+ from urllib.parse import SplitResult, urlencode, urlsplit, urlunsplit
12
+
13
+ import httpx
14
+
15
+ from ..safety import SafeHttpError, safe_http_request
16
+ from .errors import McpAuthError, McpConfigError
17
+ from .models import validate_http_url, validate_https_url
18
+ from .oauth_store import McpOAuthTokenRecord, McpOAuthTokenStoreError
19
+
20
+ __all__ = [
21
+ "McpAuthorizationServerMetadata",
22
+ "McpOAuthAuthRequiredError",
23
+ "McpOAuthCallbackError",
24
+ "McpOAuthConfigError",
25
+ "McpOAuthDiscoveryError",
26
+ "McpOAuthError",
27
+ "McpOAuthInsufficientScopeError",
28
+ "McpOAuthReLoginRequired",
29
+ "McpOAuthTokenExchangeError",
30
+ "McpOAuthTokenStoreError",
31
+ "McpProtectedResourceMetadata",
32
+ "_redact_token",
33
+ "build_authorization_url",
34
+ "build_pkce_challenge",
35
+ "canonical_mcp_resource_uri",
36
+ "discover_authorization_server_metadata",
37
+ "discover_authorization_server_metadata_from_url",
38
+ "discover_protected_resource_metadata",
39
+ "exchange_authorization_code",
40
+ "generate_oauth_state",
41
+ "generate_pkce_verifier",
42
+ "is_token_expired",
43
+ "parse_www_authenticate_bearer_challenge",
44
+ "refresh_access_token",
45
+ "resolve_requested_scopes",
46
+ ]
47
+
48
+ _PKCE_VERIFIER_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"
49
+ _PKCE_VERIFIER_RE = re.compile(r"^[A-Za-z0-9\-\._~]{43,128}$")
50
+ _BEARER_PARAM_RE = re.compile(r'([A-Za-z_][A-Za-z0-9_-]*)="([^"]*)"')
51
+
52
+
53
+ class McpOAuthError(McpAuthError):
54
+ def __init__(
55
+ self,
56
+ message: str,
57
+ *,
58
+ server_id: str,
59
+ authorization_server_url: str | None = None,
60
+ ) -> None:
61
+ self.server_id = str(server_id).strip()
62
+ self.authorization_server_origin = _url_origin(authorization_server_url)
63
+ prefix = f"{self.__class__.__name__}: server '{self.server_id}'"
64
+ if self.authorization_server_origin:
65
+ prefix += f" auth server '{self.authorization_server_origin}'"
66
+ cleaned = str(message or "OAuth operation failed.").strip()
67
+ super().__init__(f"{prefix}: {cleaned}", server_id=self.server_id)
68
+
69
+
70
+ class McpOAuthConfigError(McpOAuthError, McpConfigError):
71
+ error_code = "mcp_oauth_config_error"
72
+
73
+
74
+ class McpOAuthDiscoveryError(McpOAuthError):
75
+ pass
76
+
77
+
78
+ class McpOAuthAuthRequiredError(McpOAuthError):
79
+ pass
80
+
81
+
82
+ class McpOAuthReLoginRequired(McpOAuthError):
83
+ pass
84
+
85
+
86
+ class McpOAuthInsufficientScopeError(McpOAuthError):
87
+ pass
88
+
89
+
90
+ class McpOAuthCallbackError(McpOAuthError):
91
+ pass
92
+
93
+
94
+ class McpOAuthTokenExchangeError(McpOAuthError):
95
+ pass
96
+
97
+
98
+ class _McpOAuthDiscoveryLookupError(McpOAuthDiscoveryError):
99
+ pass
100
+
101
+
102
+ @dataclass(frozen=True)
103
+ class McpProtectedResourceMetadata:
104
+ authorization_servers: tuple[str, ...]
105
+ resource: str | None = None
106
+ scopes_supported: tuple[str, ...] = field(default_factory=tuple)
107
+ raw_payload: dict[str, Any] = field(repr=False, default_factory=dict)
108
+
109
+
110
+ @dataclass(frozen=True)
111
+ class McpAuthorizationServerMetadata:
112
+ issuer: str
113
+ authorization_endpoint: str
114
+ token_endpoint: str
115
+ code_challenge_methods_supported: tuple[str, ...] = field(default_factory=tuple)
116
+ response_types_supported: tuple[str, ...] = field(default_factory=tuple)
117
+ scopes_supported: tuple[str, ...] = field(default_factory=tuple)
118
+ raw_payload: dict[str, Any] = field(repr=False, default_factory=dict)
119
+
120
+
121
+ def _redact_token(value: str | None) -> str:
122
+ if not value:
123
+ return "[redacted:0chars]"
124
+ return f"[redacted:{len(value)}chars]"
125
+
126
+
127
+ def _url_origin(value: str | None) -> str | None:
128
+ if not value:
129
+ return None
130
+ split = urlsplit(value)
131
+ if not split.scheme or not split.netloc:
132
+ return None
133
+ netloc = split.netloc
134
+ return urlunsplit(SplitResult(split.scheme, netloc, "", "", ""))
135
+
136
+
137
+ def canonical_mcp_resource_uri(server_url: str) -> str:
138
+ normalized_url = validate_http_url(server_url, field_name="resource_server_url")
139
+ split = urlsplit(normalized_url)
140
+ hostname = str(split.hostname or "").strip()
141
+ if not hostname:
142
+ raise McpOAuthConfigError(
143
+ "resource_server_url must include a valid host.",
144
+ server_id="oauth",
145
+ authorization_server_url=normalized_url,
146
+ )
147
+ normalized_scheme = split.scheme.lower()
148
+ normalized_host = hostname.lower()
149
+ if ":" in normalized_host and not normalized_host.startswith("["):
150
+ normalized_host = f"[{normalized_host}]"
151
+ netloc = normalized_host
152
+ if split.port is not None:
153
+ netloc = f"{netloc}:{split.port}"
154
+ path = split.path or ""
155
+ normalized_path = "" if path == "/" else path
156
+ return urlunsplit(SplitResult(normalized_scheme, netloc, normalized_path, "", ""))
157
+
158
+
159
+ def _required_string(
160
+ value: object,
161
+ *,
162
+ field_name: str,
163
+ server_id: str,
164
+ authorization_server_url: str | None = None,
165
+ error_cls: type[McpOAuthError] = McpOAuthDiscoveryError,
166
+ ) -> str:
167
+ if not isinstance(value, str):
168
+ raise error_cls(
169
+ f"field '{field_name}' must be a string.",
170
+ server_id=server_id,
171
+ authorization_server_url=authorization_server_url,
172
+ )
173
+ cleaned = value.strip()
174
+ if not cleaned:
175
+ raise error_cls(
176
+ f"field '{field_name}' cannot be empty.",
177
+ server_id=server_id,
178
+ authorization_server_url=authorization_server_url,
179
+ )
180
+ return cleaned
181
+
182
+
183
+ def _optional_string(
184
+ value: object,
185
+ *,
186
+ field_name: str,
187
+ server_id: str,
188
+ authorization_server_url: str | None = None,
189
+ error_cls: type[McpOAuthError] = McpOAuthDiscoveryError,
190
+ ) -> str | None:
191
+ if value is None:
192
+ return None
193
+ return _required_string(
194
+ value,
195
+ field_name=field_name,
196
+ server_id=server_id,
197
+ authorization_server_url=authorization_server_url,
198
+ error_cls=error_cls,
199
+ )
200
+
201
+
202
+ def _normalize_string_list(
203
+ value: object,
204
+ *,
205
+ field_name: str,
206
+ server_id: str,
207
+ authorization_server_url: str | None = None,
208
+ error_cls: type[McpOAuthError] = McpOAuthDiscoveryError,
209
+ ) -> tuple[str, ...]:
210
+ if value is None:
211
+ return ()
212
+ if not isinstance(value, list):
213
+ raise error_cls(
214
+ f"field '{field_name}' must be an array of strings.",
215
+ server_id=server_id,
216
+ authorization_server_url=authorization_server_url,
217
+ )
218
+ normalized: list[str] = []
219
+ for item in value:
220
+ normalized.append(
221
+ _required_string(
222
+ item,
223
+ field_name=field_name,
224
+ server_id=server_id,
225
+ authorization_server_url=authorization_server_url,
226
+ error_cls=error_cls,
227
+ )
228
+ )
229
+ return tuple(normalized)
230
+
231
+
232
+ def _normalize_scope_list(
233
+ value: object,
234
+ *,
235
+ field_name: str,
236
+ server_id: str,
237
+ authorization_server_url: str | None = None,
238
+ error_cls: type[McpOAuthError] = McpOAuthDiscoveryError,
239
+ ) -> tuple[str, ...]:
240
+ return _normalize_requested_scopes(
241
+ _normalize_string_list(
242
+ value,
243
+ field_name=field_name,
244
+ server_id=server_id,
245
+ authorization_server_url=authorization_server_url,
246
+ error_cls=error_cls,
247
+ )
248
+ )
249
+
250
+
251
+ def _require_json_object(
252
+ payload: object,
253
+ *,
254
+ context: str,
255
+ server_id: str,
256
+ authorization_server_url: str | None = None,
257
+ ) -> dict[str, Any]:
258
+ if not isinstance(payload, dict):
259
+ raise McpOAuthDiscoveryError(
260
+ f"{context} must be a JSON object.",
261
+ server_id=server_id,
262
+ authorization_server_url=authorization_server_url,
263
+ )
264
+ return payload
265
+
266
+
267
+ def _fetch_json(
268
+ *,
269
+ url: str,
270
+ server_id: str,
271
+ timeout_s: float,
272
+ authorization_server_url: str | None = None,
273
+ ) -> dict[str, Any]:
274
+ try:
275
+ response = asyncio.run(
276
+ safe_http_request(
277
+ "GET",
278
+ url,
279
+ timeout=timeout_s,
280
+ headers={"Accept": "application/json"},
281
+ )
282
+ )
283
+ response.raise_for_status()
284
+ except SafeHttpError as exc:
285
+ raise _McpOAuthDiscoveryLookupError(
286
+ f"request blocked for '{url}': {exc}",
287
+ server_id=server_id,
288
+ authorization_server_url=authorization_server_url or url,
289
+ ) from exc
290
+ except httpx.HTTPError as exc:
291
+ raise _McpOAuthDiscoveryLookupError(
292
+ f"request failed for '{url}'.",
293
+ server_id=server_id,
294
+ authorization_server_url=authorization_server_url or url,
295
+ ) from exc
296
+ try:
297
+ payload = response.json()
298
+ except ValueError as exc:
299
+ raise McpOAuthDiscoveryError(
300
+ f"response from '{url}' was not valid JSON.",
301
+ server_id=server_id,
302
+ authorization_server_url=authorization_server_url or url,
303
+ ) from exc
304
+ return _require_json_object(
305
+ payload,
306
+ context=f"discovery document '{url}'",
307
+ server_id=server_id,
308
+ authorization_server_url=authorization_server_url or url,
309
+ )
310
+
311
+
312
+ def parse_www_authenticate_bearer_challenge(header_value: str | None) -> dict[str, str]:
313
+ text = str(header_value or "").strip()
314
+ if not text:
315
+ return {}
316
+ lowered = text.lower()
317
+ bearer_index = lowered.find("bearer")
318
+ if bearer_index < 0:
319
+ return {}
320
+ bearer_segment = text[bearer_index + len("Bearer") :]
321
+ params: dict[str, str] = {}
322
+ for match in _BEARER_PARAM_RE.finditer(bearer_segment):
323
+ params[match.group(1)] = match.group(2)
324
+ return params
325
+
326
+
327
+ def _normalize_protected_resource_metadata(
328
+ payload: dict[str, Any],
329
+ *,
330
+ server_id: str,
331
+ ) -> McpProtectedResourceMetadata:
332
+ resource = _optional_string(payload.get("resource"), field_name="resource", server_id=server_id)
333
+ authorization_servers = _normalize_string_list(
334
+ payload.get("authorization_servers"),
335
+ field_name="authorization_servers",
336
+ server_id=server_id,
337
+ )
338
+ if not authorization_servers:
339
+ raise McpOAuthDiscoveryError(
340
+ "protected resource metadata must include authorization_servers.",
341
+ server_id=server_id,
342
+ )
343
+ normalized_servers = tuple(
344
+ validate_https_url(url, field_name="authorization_servers[]", allow_loopback_http=True)
345
+ for url in authorization_servers
346
+ )
347
+ return McpProtectedResourceMetadata(
348
+ authorization_servers=normalized_servers,
349
+ resource=resource,
350
+ scopes_supported=_normalize_scope_list(
351
+ payload.get("scopes_supported"),
352
+ field_name="scopes_supported",
353
+ server_id=server_id,
354
+ ),
355
+ raw_payload=dict(payload),
356
+ )
357
+
358
+
359
+ def _normalize_authorization_server_metadata(
360
+ payload: dict[str, Any],
361
+ *,
362
+ server_id: str,
363
+ authorization_server_url: str,
364
+ ) -> McpAuthorizationServerMetadata:
365
+ issuer = validate_https_url(
366
+ _required_string(
367
+ payload.get("issuer"),
368
+ field_name="issuer",
369
+ server_id=server_id,
370
+ authorization_server_url=authorization_server_url,
371
+ ),
372
+ field_name="issuer",
373
+ allow_loopback_http=True,
374
+ )
375
+ authorization_endpoint = validate_https_url(
376
+ _required_string(
377
+ payload.get("authorization_endpoint"),
378
+ field_name="authorization_endpoint",
379
+ server_id=server_id,
380
+ authorization_server_url=authorization_server_url,
381
+ ),
382
+ field_name="authorization_endpoint",
383
+ allow_loopback_http=True,
384
+ )
385
+ token_endpoint = validate_https_url(
386
+ _required_string(
387
+ payload.get("token_endpoint"),
388
+ field_name="token_endpoint",
389
+ server_id=server_id,
390
+ authorization_server_url=authorization_server_url,
391
+ ),
392
+ field_name="token_endpoint",
393
+ allow_loopback_http=True,
394
+ )
395
+ return McpAuthorizationServerMetadata(
396
+ issuer=issuer,
397
+ authorization_endpoint=authorization_endpoint,
398
+ token_endpoint=token_endpoint,
399
+ code_challenge_methods_supported=_normalize_string_list(
400
+ payload.get("code_challenge_methods_supported"),
401
+ field_name="code_challenge_methods_supported",
402
+ server_id=server_id,
403
+ authorization_server_url=authorization_server_url,
404
+ ),
405
+ response_types_supported=_normalize_string_list(
406
+ payload.get("response_types_supported"),
407
+ field_name="response_types_supported",
408
+ server_id=server_id,
409
+ authorization_server_url=authorization_server_url,
410
+ ),
411
+ scopes_supported=_normalize_scope_list(
412
+ payload.get("scopes_supported"),
413
+ field_name="scopes_supported",
414
+ server_id=server_id,
415
+ authorization_server_url=authorization_server_url,
416
+ ),
417
+ raw_payload=dict(payload),
418
+ )
419
+
420
+
421
+ def _authorization_server_metadata_urls(authorization_server_url: str) -> tuple[str, ...]:
422
+ split = urlsplit(authorization_server_url)
423
+ origin = urlunsplit(SplitResult(split.scheme, split.netloc, "", "", ""))
424
+ if split.path and split.path != "/":
425
+ appended_path = split.path.rstrip("/") + "/.well-known/openid-configuration"
426
+ return (
427
+ f"{origin}/.well-known/oauth-authorization-server{split.path}",
428
+ f"{origin}/.well-known/openid-configuration{split.path}",
429
+ f"{origin}{appended_path}",
430
+ )
431
+ return (
432
+ f"{origin}/.well-known/oauth-authorization-server",
433
+ f"{origin}/.well-known/openid-configuration",
434
+ )
435
+
436
+
437
+ def _protected_resource_metadata_urls(resource_server_url: str) -> tuple[str, ...]:
438
+ split = urlsplit(resource_server_url)
439
+ origin = urlunsplit(SplitResult(split.scheme, split.netloc, "", "", ""))
440
+ urls: list[str] = []
441
+ if split.path and split.path != "/":
442
+ urls.append(f"{origin}/.well-known/oauth-protected-resource{split.path}")
443
+ urls.append(f"{origin}/.well-known/oauth-protected-resource")
444
+ return tuple(urls)
445
+
446
+
447
+ def discover_protected_resource_metadata(
448
+ *,
449
+ server_id: str,
450
+ resource_server_url: str,
451
+ unauthorized_response: httpx.Response | None = None,
452
+ timeout_s: float = 10.0,
453
+ client: httpx.Client | None = None,
454
+ ) -> McpProtectedResourceMetadata:
455
+ del client
456
+ normalized_resource_url = validate_http_url(
457
+ resource_server_url, field_name="resource_server_url"
458
+ )
459
+ challenge_params = parse_www_authenticate_bearer_challenge(
460
+ unauthorized_response.headers.get("WWW-Authenticate")
461
+ if unauthorized_response is not None
462
+ else None
463
+ )
464
+ resource_metadata_url = challenge_params.get("resource_metadata")
465
+ if resource_metadata_url is not None:
466
+ normalized_metadata_url = validate_https_url(
467
+ resource_metadata_url,
468
+ field_name="resource_metadata",
469
+ allow_loopback_http=True,
470
+ )
471
+ payload = _fetch_json(
472
+ url=normalized_metadata_url,
473
+ server_id=server_id,
474
+ timeout_s=timeout_s,
475
+ )
476
+ return _normalize_protected_resource_metadata(payload, server_id=server_id)
477
+ last_lookup_error: _McpOAuthDiscoveryLookupError | None = None
478
+ for metadata_url in _protected_resource_metadata_urls(normalized_resource_url):
479
+ try:
480
+ payload = _fetch_json(
481
+ url=metadata_url,
482
+ server_id=server_id,
483
+ timeout_s=timeout_s,
484
+ )
485
+ except _McpOAuthDiscoveryLookupError as exc:
486
+ last_lookup_error = exc
487
+ continue
488
+ return _normalize_protected_resource_metadata(payload, server_id=server_id)
489
+ raise McpOAuthDiscoveryError(
490
+ "protected resource metadata discovery failed via path-specific and root well-known fallbacks.",
491
+ server_id=server_id,
492
+ authorization_server_url=normalized_resource_url,
493
+ ) from last_lookup_error
494
+
495
+
496
+ def discover_authorization_server_metadata_from_url(
497
+ *,
498
+ server_id: str,
499
+ authorization_server_url: str,
500
+ timeout_s: float = 10.0,
501
+ client: httpx.Client | None = None,
502
+ ) -> McpAuthorizationServerMetadata:
503
+ del client
504
+ normalized_auth_server_url = validate_https_url(
505
+ authorization_server_url,
506
+ field_name="authorization_server_url",
507
+ allow_loopback_http=True,
508
+ )
509
+ last_lookup_error: _McpOAuthDiscoveryLookupError | None = None
510
+ for metadata_url in _authorization_server_metadata_urls(normalized_auth_server_url):
511
+ try:
512
+ payload = _fetch_json(
513
+ url=metadata_url,
514
+ server_id=server_id,
515
+ timeout_s=timeout_s,
516
+ authorization_server_url=normalized_auth_server_url,
517
+ )
518
+ except _McpOAuthDiscoveryLookupError as exc:
519
+ last_lookup_error = exc
520
+ continue
521
+ return _normalize_authorization_server_metadata(
522
+ payload,
523
+ server_id=server_id,
524
+ authorization_server_url=normalized_auth_server_url,
525
+ )
526
+ raise McpOAuthDiscoveryError(
527
+ "authorization server discovery failed via RFC 8414 and OIDC fallbacks.",
528
+ server_id=server_id,
529
+ authorization_server_url=normalized_auth_server_url,
530
+ ) from last_lookup_error
531
+
532
+
533
+ def discover_authorization_server_metadata(
534
+ *,
535
+ server_id: str,
536
+ resource_server_url: str,
537
+ unauthorized_response: httpx.Response | None = None,
538
+ authorization_server_url: str | None = None,
539
+ timeout_s: float = 10.0,
540
+ client: httpx.Client | None = None,
541
+ ) -> McpAuthorizationServerMetadata:
542
+ del client
543
+ if authorization_server_url is not None:
544
+ return discover_authorization_server_metadata_from_url(
545
+ server_id=server_id,
546
+ authorization_server_url=authorization_server_url,
547
+ timeout_s=timeout_s,
548
+ )
549
+ protected_metadata = discover_protected_resource_metadata(
550
+ server_id=server_id,
551
+ resource_server_url=resource_server_url,
552
+ unauthorized_response=unauthorized_response,
553
+ timeout_s=timeout_s,
554
+ )
555
+ return discover_authorization_server_metadata_from_url(
556
+ server_id=server_id,
557
+ authorization_server_url=protected_metadata.authorization_servers[0],
558
+ timeout_s=timeout_s,
559
+ )
560
+
561
+
562
+ def generate_pkce_verifier(length: int = 64) -> str:
563
+ if not 43 <= int(length) <= 128:
564
+ raise McpOAuthConfigError(
565
+ "PKCE verifier length must be between 43 and 128 characters.",
566
+ server_id="pkce",
567
+ )
568
+ return "".join(secrets.choice(_PKCE_VERIFIER_CHARS) for _ in range(int(length)))
569
+
570
+
571
+ def build_pkce_challenge(verifier: str) -> str:
572
+ if not isinstance(verifier, str) or not _PKCE_VERIFIER_RE.fullmatch(verifier):
573
+ raise McpOAuthConfigError(
574
+ "PKCE verifier must be 43-128 RFC 3986 unreserved characters.",
575
+ server_id="pkce",
576
+ )
577
+ digest = hashlib.sha256(verifier.encode("ascii")).digest()
578
+ return base64.urlsafe_b64encode(digest).decode("ascii").rstrip("=")
579
+
580
+
581
+ def generate_oauth_state() -> str:
582
+ return secrets.token_hex(32)
583
+
584
+
585
+ def _normalize_scope_string(scope_value: str) -> tuple[str, ...]:
586
+ parts = [part.strip() for part in scope_value.split(" ")]
587
+ normalized: list[str] = []
588
+ seen: set[str] = set()
589
+ for part in parts:
590
+ if not part or part in seen:
591
+ continue
592
+ seen.add(part)
593
+ normalized.append(part)
594
+ return tuple(normalized)
595
+
596
+
597
+ def _normalize_requested_scopes(scopes: tuple[str, ...] | list[str] | None) -> tuple[str, ...]:
598
+ normalized: list[str] = []
599
+ seen: set[str] = set()
600
+ for raw_scope in list(scopes or []):
601
+ cleaned = str(raw_scope or "").strip()
602
+ if not cleaned or cleaned in seen:
603
+ continue
604
+ seen.add(cleaned)
605
+ normalized.append(cleaned)
606
+ return tuple(normalized)
607
+
608
+
609
+ def resolve_requested_scopes(
610
+ *,
611
+ configured_scopes: tuple[str, ...] | list[str] | None,
612
+ challenge_scope: str | None,
613
+ metadata_scopes_supported: tuple[str, ...] | list[str] | None,
614
+ existing_granted_scopes: tuple[str, ...] | list[str] | None,
615
+ purpose: Literal["login", "refresh"],
616
+ ) -> tuple[str, ...]:
617
+ configured = _normalize_requested_scopes(configured_scopes)
618
+ if configured:
619
+ return configured
620
+ if purpose == "login":
621
+ challenge = _normalize_scope_string(challenge_scope) if challenge_scope else ()
622
+ if challenge:
623
+ return challenge
624
+ metadata_scopes = _normalize_requested_scopes(metadata_scopes_supported)
625
+ if metadata_scopes:
626
+ return metadata_scopes
627
+ return ()
628
+ if purpose == "refresh":
629
+ granted = _normalize_requested_scopes(existing_granted_scopes)
630
+ if granted:
631
+ return granted
632
+ return ()
633
+ raise ValueError(f"unsupported scope resolution purpose: {purpose}")
634
+
635
+
636
+ def _utc_now() -> datetime:
637
+ return datetime.now(UTC).replace(microsecond=0)
638
+
639
+
640
+ def _parse_expires_in(
641
+ value: object,
642
+ *,
643
+ server_id: str,
644
+ authorization_server_url: str,
645
+ ) -> int:
646
+ if isinstance(value, bool):
647
+ raise McpOAuthTokenExchangeError(
648
+ "token response field 'expires_in' must be a non-negative integer.",
649
+ server_id=server_id,
650
+ authorization_server_url=authorization_server_url,
651
+ )
652
+ if isinstance(value, int):
653
+ expires_in = value
654
+ elif isinstance(value, float) and value.is_integer():
655
+ expires_in = int(value)
656
+ elif isinstance(value, str) and value.strip().isdigit():
657
+ expires_in = int(value.strip())
658
+ else:
659
+ raise McpOAuthTokenExchangeError(
660
+ "token response field 'expires_in' must be a non-negative integer.",
661
+ server_id=server_id,
662
+ authorization_server_url=authorization_server_url,
663
+ )
664
+ if expires_in < 0:
665
+ raise McpOAuthTokenExchangeError(
666
+ "token response field 'expires_in' must be a non-negative integer.",
667
+ server_id=server_id,
668
+ authorization_server_url=authorization_server_url,
669
+ )
670
+ return expires_in
671
+
672
+
673
+ def _normalize_token_response(
674
+ payload: object,
675
+ *,
676
+ server_id: str,
677
+ authorization_server_url: str,
678
+ requested_scopes: tuple[str, ...] | list[str] | None,
679
+ existing_granted_scopes: tuple[str, ...] | list[str] | None = None,
680
+ existing_refresh_token: str | None = None,
681
+ obtained_at: datetime | None = None,
682
+ ) -> McpOAuthTokenRecord:
683
+ if not isinstance(payload, dict):
684
+ raise McpOAuthTokenExchangeError(
685
+ "token response must be a JSON object.",
686
+ server_id=server_id,
687
+ authorization_server_url=authorization_server_url,
688
+ )
689
+ access_token = _required_string(
690
+ payload.get("access_token"),
691
+ field_name="access_token",
692
+ server_id=server_id,
693
+ authorization_server_url=authorization_server_url,
694
+ error_cls=McpOAuthTokenExchangeError,
695
+ )
696
+ token_type = _required_string(
697
+ payload.get("token_type"),
698
+ field_name="token_type",
699
+ server_id=server_id,
700
+ authorization_server_url=authorization_server_url,
701
+ error_cls=McpOAuthTokenExchangeError,
702
+ )
703
+ if token_type.casefold() != "bearer":
704
+ raise McpOAuthTokenExchangeError(
705
+ "token response field 'token_type' must be 'Bearer'.",
706
+ server_id=server_id,
707
+ authorization_server_url=authorization_server_url,
708
+ )
709
+ expires_in = _parse_expires_in(
710
+ payload.get("expires_in"),
711
+ server_id=server_id,
712
+ authorization_server_url=authorization_server_url,
713
+ )
714
+ response_scope = _optional_string(
715
+ payload.get("scope"),
716
+ field_name="scope",
717
+ server_id=server_id,
718
+ authorization_server_url=authorization_server_url,
719
+ error_cls=McpOAuthTokenExchangeError,
720
+ )
721
+ refresh_token = _optional_string(
722
+ payload.get("refresh_token"),
723
+ field_name="refresh_token",
724
+ server_id=server_id,
725
+ authorization_server_url=authorization_server_url,
726
+ error_cls=McpOAuthTokenExchangeError,
727
+ )
728
+ if refresh_token is None:
729
+ refresh_token = existing_refresh_token
730
+ normalized_requested_scopes = _normalize_requested_scopes(requested_scopes)
731
+ normalized_existing_granted_scopes = _normalize_requested_scopes(existing_granted_scopes)
732
+ if response_scope is not None:
733
+ granted_scopes = _normalize_scope_string(response_scope)
734
+ elif normalized_requested_scopes:
735
+ granted_scopes = normalized_requested_scopes
736
+ elif normalized_existing_granted_scopes:
737
+ granted_scopes = normalized_existing_granted_scopes
738
+ else:
739
+ granted_scopes = ()
740
+ recorded_at = (obtained_at or _utc_now()).astimezone(UTC).replace(microsecond=0)
741
+ return McpOAuthTokenRecord(
742
+ access_token=access_token,
743
+ token_type=token_type,
744
+ expires_at=recorded_at + timedelta(seconds=expires_in),
745
+ refresh_token=refresh_token,
746
+ granted_scopes=granted_scopes,
747
+ obtained_at=recorded_at,
748
+ )
749
+
750
+
751
+ def is_token_expired(record: McpOAuthTokenRecord, *, now: datetime | None = None) -> bool:
752
+ reference = (now or _utc_now()).astimezone(UTC).replace(microsecond=0)
753
+ return reference >= record.expires_at
754
+
755
+
756
+ def build_authorization_url(
757
+ *,
758
+ server_id: str,
759
+ authorization_server_metadata: McpAuthorizationServerMetadata,
760
+ resource_server_url: str,
761
+ client_id: str,
762
+ redirect_uri: str,
763
+ code_challenge: str,
764
+ state: str,
765
+ scopes: tuple[str, ...] | list[str] | None = None,
766
+ ) -> str:
767
+ normalized_redirect_uri = validate_http_url(redirect_uri, field_name="redirect_uri")
768
+ normalized_client_id = _required_string(
769
+ client_id,
770
+ field_name="client_id",
771
+ server_id=server_id,
772
+ authorization_server_url=authorization_server_metadata.authorization_endpoint,
773
+ error_cls=McpOAuthConfigError,
774
+ )
775
+ normalized_state = _required_string(
776
+ state,
777
+ field_name="state",
778
+ server_id=server_id,
779
+ authorization_server_url=authorization_server_metadata.authorization_endpoint,
780
+ error_cls=McpOAuthConfigError,
781
+ )
782
+ normalized_challenge = _required_string(
783
+ code_challenge,
784
+ field_name="code_challenge",
785
+ server_id=server_id,
786
+ authorization_server_url=authorization_server_metadata.authorization_endpoint,
787
+ error_cls=McpOAuthConfigError,
788
+ )
789
+ params = {
790
+ "response_type": "code",
791
+ "client_id": normalized_client_id,
792
+ "redirect_uri": normalized_redirect_uri,
793
+ "resource": canonical_mcp_resource_uri(resource_server_url),
794
+ "code_challenge": normalized_challenge,
795
+ "code_challenge_method": "S256",
796
+ "state": normalized_state,
797
+ }
798
+ normalized_scopes = _normalize_requested_scopes(scopes)
799
+ if normalized_scopes:
800
+ params["scope"] = " ".join(normalized_scopes)
801
+ return f"{authorization_server_metadata.authorization_endpoint}?{urlencode(params)}"
802
+
803
+
804
+ def _post_token_request(
805
+ *,
806
+ server_id: str,
807
+ token_endpoint: str,
808
+ payload: dict[str, str],
809
+ timeout_s: float,
810
+ client: httpx.Client | None = None,
811
+ ) -> dict[str, Any]:
812
+ del client
813
+ encoded_payload = urlencode(payload).encode("utf-8")
814
+ try:
815
+ response = asyncio.run(
816
+ safe_http_request(
817
+ "POST",
818
+ token_endpoint,
819
+ timeout=timeout_s,
820
+ headers={
821
+ "Accept": "application/json",
822
+ "Content-Type": "application/x-www-form-urlencoded",
823
+ },
824
+ content=encoded_payload,
825
+ )
826
+ )
827
+ except httpx.TimeoutException as exc:
828
+ raise McpOAuthTokenExchangeError(
829
+ "token endpoint request timed out.",
830
+ server_id=server_id,
831
+ authorization_server_url=token_endpoint,
832
+ ) from exc
833
+ except SafeHttpError as exc:
834
+ raise McpOAuthTokenExchangeError(
835
+ f"token endpoint request blocked: {exc}",
836
+ server_id=server_id,
837
+ authorization_server_url=token_endpoint,
838
+ ) from exc
839
+ except httpx.HTTPError as exc:
840
+ raise McpOAuthTokenExchangeError(
841
+ "token endpoint request failed.",
842
+ server_id=server_id,
843
+ authorization_server_url=token_endpoint,
844
+ ) from exc
845
+ if response.status_code >= 400:
846
+ raise McpOAuthTokenExchangeError(
847
+ f"token endpoint returned status {response.status_code}.",
848
+ server_id=server_id,
849
+ authorization_server_url=token_endpoint,
850
+ )
851
+ try:
852
+ return _require_json_object(
853
+ response.json(),
854
+ context="token response",
855
+ server_id=server_id,
856
+ authorization_server_url=token_endpoint,
857
+ )
858
+ except McpOAuthDiscoveryError as exc:
859
+ raise McpOAuthTokenExchangeError(
860
+ "token response must be a JSON object.",
861
+ server_id=server_id,
862
+ authorization_server_url=token_endpoint,
863
+ ) from exc
864
+ except ValueError as exc:
865
+ raise McpOAuthTokenExchangeError(
866
+ "token response was not valid JSON.",
867
+ server_id=server_id,
868
+ authorization_server_url=token_endpoint,
869
+ ) from exc
870
+
871
+
872
+ def exchange_authorization_code(
873
+ *,
874
+ server_id: str,
875
+ authorization_server_metadata: McpAuthorizationServerMetadata,
876
+ resource_server_url: str,
877
+ client_id: str,
878
+ code: str,
879
+ redirect_uri: str,
880
+ code_verifier: str,
881
+ requested_scopes: tuple[str, ...] | list[str] | None = None,
882
+ timeout_s: float = 10.0,
883
+ client: httpx.Client | None = None,
884
+ ) -> McpOAuthTokenRecord:
885
+ normalized_code = _required_string(
886
+ code,
887
+ field_name="code",
888
+ server_id=server_id,
889
+ authorization_server_url=authorization_server_metadata.token_endpoint,
890
+ error_cls=McpOAuthTokenExchangeError,
891
+ )
892
+ normalized_client_id = _required_string(
893
+ client_id,
894
+ field_name="client_id",
895
+ server_id=server_id,
896
+ authorization_server_url=authorization_server_metadata.token_endpoint,
897
+ error_cls=McpOAuthTokenExchangeError,
898
+ )
899
+ normalized_redirect_uri = validate_http_url(redirect_uri, field_name="redirect_uri")
900
+ build_pkce_challenge(code_verifier)
901
+ token_payload = _post_token_request(
902
+ server_id=server_id,
903
+ token_endpoint=authorization_server_metadata.token_endpoint,
904
+ payload={
905
+ "grant_type": "authorization_code",
906
+ "code": normalized_code,
907
+ "redirect_uri": normalized_redirect_uri,
908
+ "client_id": normalized_client_id,
909
+ "resource": canonical_mcp_resource_uri(resource_server_url),
910
+ "code_verifier": code_verifier,
911
+ },
912
+ timeout_s=timeout_s,
913
+ client=client,
914
+ )
915
+ return _normalize_token_response(
916
+ token_payload,
917
+ server_id=server_id,
918
+ authorization_server_url=authorization_server_metadata.token_endpoint,
919
+ requested_scopes=requested_scopes,
920
+ )
921
+
922
+
923
+ def refresh_access_token(
924
+ *,
925
+ server_id: str,
926
+ authorization_server_metadata: McpAuthorizationServerMetadata,
927
+ resource_server_url: str,
928
+ client_id: str,
929
+ refresh_token: str,
930
+ requested_scopes: tuple[str, ...] | list[str] | None = None,
931
+ existing_granted_scopes: tuple[str, ...] | list[str] | None = None,
932
+ timeout_s: float = 10.0,
933
+ client: httpx.Client | None = None,
934
+ ) -> McpOAuthTokenRecord:
935
+ normalized_refresh_token = _required_string(
936
+ refresh_token,
937
+ field_name="refresh_token",
938
+ server_id=server_id,
939
+ authorization_server_url=authorization_server_metadata.token_endpoint,
940
+ error_cls=McpOAuthTokenExchangeError,
941
+ )
942
+ normalized_client_id = _required_string(
943
+ client_id,
944
+ field_name="client_id",
945
+ server_id=server_id,
946
+ authorization_server_url=authorization_server_metadata.token_endpoint,
947
+ error_cls=McpOAuthTokenExchangeError,
948
+ )
949
+ normalized_requested_scopes = _normalize_requested_scopes(requested_scopes)
950
+ token_request_payload = {
951
+ "grant_type": "refresh_token",
952
+ "refresh_token": normalized_refresh_token,
953
+ "client_id": normalized_client_id,
954
+ "resource": canonical_mcp_resource_uri(resource_server_url),
955
+ }
956
+ if normalized_requested_scopes:
957
+ token_request_payload["scope"] = " ".join(normalized_requested_scopes)
958
+ token_payload = _post_token_request(
959
+ server_id=server_id,
960
+ token_endpoint=authorization_server_metadata.token_endpoint,
961
+ payload=token_request_payload,
962
+ timeout_s=timeout_s,
963
+ client=client,
964
+ )
965
+ return _normalize_token_response(
966
+ token_payload,
967
+ server_id=server_id,
968
+ authorization_server_url=authorization_server_metadata.token_endpoint,
969
+ requested_scopes=requested_scopes,
970
+ existing_granted_scopes=existing_granted_scopes,
971
+ existing_refresh_token=normalized_refresh_token,
972
+ )