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,310 @@
1
+ from __future__ import annotations
2
+
3
+ import html
4
+ import threading
5
+ import time
6
+ import webbrowser
7
+ from collections.abc import Callable
8
+ from dataclasses import dataclass
9
+ from http import HTTPStatus
10
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
11
+ from urllib.parse import parse_qs, urlsplit
12
+
13
+ from .oauth import (
14
+ McpAuthorizationServerMetadata,
15
+ McpOAuthCallbackError,
16
+ build_authorization_url,
17
+ build_pkce_challenge,
18
+ exchange_authorization_code,
19
+ generate_oauth_state,
20
+ generate_pkce_verifier,
21
+ )
22
+ from .oauth_store import McpOAuthTokenRecord, save_oauth_token_record
23
+
24
+ __all__ = [
25
+ "McpOAuthCallbackPayload",
26
+ "McpOAuthLoginResult",
27
+ "perform_authorization_code_login",
28
+ ]
29
+
30
+ _CALLBACK_PATH = "/oauth/callback"
31
+ _LOCALHOST_CALLBACK_TIMEOUT_S = 120.0
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class McpOAuthCallbackPayload:
36
+ code: str | None
37
+ state: str | None
38
+ error: str | None = None
39
+ error_description: str | None = None
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class McpOAuthLoginResult:
44
+ server_id: str
45
+ authorization_url: str
46
+ redirect_uri: str
47
+ browser_opened: bool
48
+ token_record: McpOAuthTokenRecord
49
+
50
+
51
+ class _CallbackState:
52
+ def __init__(self) -> None:
53
+ self.event = threading.Event()
54
+ self.payload: McpOAuthCallbackPayload | None = None
55
+ self._lock = threading.Lock()
56
+
57
+ def set_payload(self, payload: McpOAuthCallbackPayload) -> None:
58
+ with self._lock:
59
+ if self.payload is None:
60
+ self.payload = payload
61
+ self.event.set()
62
+
63
+
64
+ class _CallbackListenerHttpServer(ThreadingHTTPServer):
65
+ daemon_threads = True
66
+
67
+
68
+ class _CallbackHandler(BaseHTTPRequestHandler):
69
+ server_version = "AlysisOAuthCallback/1.0"
70
+
71
+ def log_message(self, format: str, *args: object) -> None: # noqa: A003
72
+ return
73
+
74
+ @property
75
+ def callback_state(self) -> _CallbackState:
76
+ return self.server.callback_state # type: ignore[attr-defined]
77
+
78
+ def do_GET(self) -> None: # noqa: N802
79
+ parsed = urlsplit(self.path)
80
+ if parsed.path != _CALLBACK_PATH:
81
+ self.send_error(HTTPStatus.NOT_FOUND)
82
+ return
83
+ query = parse_qs(parsed.query)
84
+ payload = McpOAuthCallbackPayload(
85
+ code=(query.get("code") or [None])[0],
86
+ state=(query.get("state") or [None])[0],
87
+ error=(query.get("error") or [None])[0],
88
+ error_description=(query.get("error_description") or [None])[0],
89
+ )
90
+ self.callback_state.set_payload(payload)
91
+ message = (
92
+ "OAuth login failed. Return to the terminal for details."
93
+ if payload.error
94
+ else "OAuth login complete. You can close this window and return to the terminal."
95
+ )
96
+ self._send_html(HTTPStatus.OK, message)
97
+
98
+ def _send_html(self, status: int, message: str) -> None:
99
+ safe_message = html.escape(message)
100
+ body = (
101
+ '<!doctype html><html><head><meta charset="utf-8"></head>'
102
+ f"<body><p>{safe_message}</p></body></html>"
103
+ ).encode()
104
+ self.send_response(status)
105
+ self.send_header("Content-Type", "text/html; charset=utf-8")
106
+ self.send_header("Content-Length", str(len(body)))
107
+ self.end_headers()
108
+ self.wfile.write(body)
109
+
110
+
111
+ class _LocalhostCallbackListener:
112
+ def __init__(
113
+ self,
114
+ *,
115
+ server_id: str,
116
+ authorization_server_url: str,
117
+ host: str,
118
+ port: int,
119
+ ) -> None:
120
+ self.server_id = server_id
121
+ self.authorization_server_url = authorization_server_url
122
+ self.callback_state = _CallbackState()
123
+ self._server = _CallbackListenerHttpServer((host, port), _CallbackHandler)
124
+ self._server.callback_state = self.callback_state # type: ignore[attr-defined]
125
+ self._thread = threading.Thread(target=self._server.serve_forever, daemon=True)
126
+ self._thread.start()
127
+ self._closed = False
128
+
129
+ @property
130
+ def redirect_uri(self) -> str:
131
+ host, port = self._server.server_address
132
+ return f"http://{host}:{port}{_CALLBACK_PATH}"
133
+
134
+ def wait_for_callback(
135
+ self,
136
+ *,
137
+ timeout_s: float,
138
+ interrupt_check: Callable[[], None] | None = None,
139
+ ) -> McpOAuthCallbackPayload:
140
+ deadline = time.monotonic() + timeout_s
141
+ try:
142
+ while True:
143
+ remaining = deadline - time.monotonic()
144
+ if remaining <= 0:
145
+ raise McpOAuthCallbackError(
146
+ f"timed out waiting {int(timeout_s)}s for the localhost OAuth callback.",
147
+ server_id=self.server_id,
148
+ authorization_server_url=self.authorization_server_url,
149
+ )
150
+ if interrupt_check is not None:
151
+ interrupt_check()
152
+ if self.callback_state.event.wait(timeout=min(0.1, remaining)):
153
+ if self.callback_state.payload is None:
154
+ raise McpOAuthCallbackError(
155
+ "localhost OAuth callback completed without payload.",
156
+ server_id=self.server_id,
157
+ authorization_server_url=self.authorization_server_url,
158
+ )
159
+ return self.callback_state.payload
160
+ except KeyboardInterrupt as exc:
161
+ raise McpOAuthCallbackError(
162
+ "OAuth login interrupted while waiting for the localhost callback.",
163
+ server_id=self.server_id,
164
+ authorization_server_url=self.authorization_server_url,
165
+ ) from exc
166
+
167
+ def close(self) -> None:
168
+ if self._closed:
169
+ return
170
+ self._closed = True
171
+ self._server.shutdown()
172
+ self._server.server_close()
173
+ self._thread.join(timeout=5.0)
174
+
175
+
176
+ def _open_localhost_listener(
177
+ *,
178
+ server_id: str,
179
+ authorization_server_url: str,
180
+ redirect_host: str | None,
181
+ redirect_port: int | None,
182
+ ) -> _LocalhostCallbackListener:
183
+ host = str(redirect_host or "127.0.0.1").strip() or "127.0.0.1"
184
+ preferred_port = 0 if redirect_port is None else int(redirect_port)
185
+ attempted_ports = [preferred_port, 0]
186
+ last_error: OSError | None = None
187
+ for index, port in enumerate(attempted_ports):
188
+ try:
189
+ return _LocalhostCallbackListener(
190
+ server_id=server_id,
191
+ authorization_server_url=authorization_server_url,
192
+ host=host,
193
+ port=port,
194
+ )
195
+ except OSError as exc:
196
+ last_error = exc
197
+ if index == len(attempted_ports) - 1:
198
+ break
199
+ raise McpOAuthCallbackError(
200
+ f"failed to bind localhost OAuth callback listener on host '{host}' after two attempts.",
201
+ server_id=server_id,
202
+ authorization_server_url=authorization_server_url,
203
+ ) from last_error
204
+
205
+
206
+ def _write_manual_login_instructions(
207
+ writer: Callable[[str], None],
208
+ *,
209
+ authorization_url: str,
210
+ redirect_uri: str,
211
+ ) -> None:
212
+ writer(
213
+ "Open this URL in a browser to continue MCP OAuth login:\n"
214
+ f"{authorization_url}\n"
215
+ f"Redirect URI: {redirect_uri}\n"
216
+ "This flow still requires localhost callback access and cannot complete without "
217
+ "the redirect reaching this machine."
218
+ )
219
+
220
+
221
+ def perform_authorization_code_login(
222
+ *,
223
+ server_id: str,
224
+ authorization_server_metadata: McpAuthorizationServerMetadata,
225
+ resource_server_url: str,
226
+ client_id: str,
227
+ scopes: tuple[str, ...] | list[str] | None = None,
228
+ redirect_host: str | None = None,
229
+ redirect_port: int | None = None,
230
+ timeout_s: float = _LOCALHOST_CALLBACK_TIMEOUT_S,
231
+ token_timeout_s: float = 10.0,
232
+ browser_opener: Callable[[str], bool] | None = None,
233
+ output_write: Callable[[str], None] | None = None,
234
+ interrupt_check: Callable[[], None] | None = None,
235
+ ) -> McpOAuthLoginResult:
236
+ writer = output_write or print
237
+ listener = _open_localhost_listener(
238
+ server_id=server_id,
239
+ authorization_server_url=authorization_server_metadata.authorization_endpoint,
240
+ redirect_host=redirect_host,
241
+ redirect_port=redirect_port,
242
+ )
243
+ try:
244
+ code_verifier = generate_pkce_verifier()
245
+ code_challenge = build_pkce_challenge(code_verifier)
246
+ state = generate_oauth_state()
247
+ authorization_url = build_authorization_url(
248
+ server_id=server_id,
249
+ authorization_server_metadata=authorization_server_metadata,
250
+ resource_server_url=resource_server_url,
251
+ client_id=client_id,
252
+ redirect_uri=listener.redirect_uri,
253
+ code_challenge=code_challenge,
254
+ state=state,
255
+ scopes=scopes,
256
+ )
257
+ opener = browser_opener or webbrowser.open
258
+ try:
259
+ browser_opened = bool(opener(authorization_url))
260
+ except Exception:
261
+ browser_opened = False
262
+ if not browser_opened:
263
+ _write_manual_login_instructions(
264
+ writer,
265
+ authorization_url=authorization_url,
266
+ redirect_uri=listener.redirect_uri,
267
+ )
268
+ callback = listener.wait_for_callback(timeout_s=timeout_s, interrupt_check=interrupt_check)
269
+ if callback.error:
270
+ details = callback.error
271
+ if callback.error_description:
272
+ details = f"{details}: {callback.error_description}"
273
+ raise McpOAuthCallbackError(
274
+ f"authorization server returned callback error '{details}'.",
275
+ server_id=server_id,
276
+ authorization_server_url=authorization_server_metadata.authorization_endpoint,
277
+ )
278
+ if callback.state != state:
279
+ raise McpOAuthCallbackError(
280
+ "received OAuth callback with mismatched state.",
281
+ server_id=server_id,
282
+ authorization_server_url=authorization_server_metadata.authorization_endpoint,
283
+ )
284
+ if not callback.code:
285
+ raise McpOAuthCallbackError(
286
+ "received OAuth callback without an authorization code.",
287
+ server_id=server_id,
288
+ authorization_server_url=authorization_server_metadata.authorization_endpoint,
289
+ )
290
+ record = exchange_authorization_code(
291
+ server_id=server_id,
292
+ authorization_server_metadata=authorization_server_metadata,
293
+ resource_server_url=resource_server_url,
294
+ client_id=client_id,
295
+ code=callback.code,
296
+ redirect_uri=listener.redirect_uri,
297
+ code_verifier=code_verifier,
298
+ requested_scopes=scopes,
299
+ timeout_s=token_timeout_s,
300
+ )
301
+ save_oauth_token_record(server_id, record)
302
+ return McpOAuthLoginResult(
303
+ server_id=server_id,
304
+ authorization_url=authorization_url,
305
+ redirect_uri=listener.redirect_uri,
306
+ browser_opened=browser_opened,
307
+ token_record=record,
308
+ )
309
+ finally:
310
+ listener.close()
@@ -0,0 +1,276 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from dataclasses import dataclass, field
5
+ from datetime import UTC, datetime
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from .config import user_mcp_config_path
10
+ from .errors import (
11
+ McpOAuthTokenStoreError,
12
+ McpTokenStoreCorruptError,
13
+ McpTokenStoreMigrationError,
14
+ McpTokenStoreUnavailableError,
15
+ McpTokenStoreVersionError,
16
+ )
17
+ from .models import normalize_ordered_string_list, normalize_server_id
18
+ from .token_store import load_token_payload_result, migrate_legacy_token_payload, save_token_payload
19
+
20
+ __all__ = [
21
+ "McpOAuthTokenRecord",
22
+ "McpOAuthTokenStoreError",
23
+ "McpTokenStoreCorruptError",
24
+ "McpTokenStoreMigrationError",
25
+ "McpTokenStoreUnavailableError",
26
+ "McpTokenStoreVersionError",
27
+ "delete_oauth_token_record",
28
+ "list_oauth_token_server_ids",
29
+ "load_oauth_token_record",
30
+ "mcp_oauth_token_store_path",
31
+ "save_oauth_token_record",
32
+ ]
33
+
34
+ _OAUTH_BINDING_ID_RE = re.compile(r"^[A-Za-z0-9_-]{32,128}$")
35
+
36
+
37
+ def _store_path() -> Path:
38
+ return user_mcp_config_path().with_name("mcp_oauth_tokens.json")
39
+
40
+
41
+ def mcp_oauth_token_store_path() -> Path:
42
+ return _store_path()
43
+
44
+
45
+ def _canonical_server_id(server_id: str) -> str:
46
+ try:
47
+ return normalize_server_id(server_id)
48
+ except Exception as exc: # noqa: BLE001
49
+ raise McpOAuthTokenStoreError(
50
+ f"Invalid OAuth token store server id: {server_id!r}"
51
+ ) from exc
52
+
53
+
54
+ def _require_string(value: object, *, field_name: str, server_id: str) -> str:
55
+ if not isinstance(value, str):
56
+ raise McpOAuthTokenStoreError(
57
+ f"OAuth token store entry for server '{server_id}' field '{field_name}' must be a string."
58
+ )
59
+ cleaned = value.strip()
60
+ if not cleaned:
61
+ raise McpOAuthTokenStoreError(
62
+ f"OAuth token store entry for server '{server_id}' field '{field_name}' cannot be empty."
63
+ )
64
+ return cleaned
65
+
66
+
67
+ def _optional_string(value: object, *, field_name: str, server_id: str) -> str | None:
68
+ if value is None:
69
+ return None
70
+ return _require_string(value, field_name=field_name, server_id=server_id)
71
+
72
+
73
+ def _optional_binding_id(value: object, *, server_id: str) -> str | None:
74
+ if value is None:
75
+ return None
76
+ binding_id = _require_string(value, field_name="binding_id", server_id=server_id)
77
+ if not _OAUTH_BINDING_ID_RE.fullmatch(binding_id):
78
+ raise McpOAuthTokenStoreError(
79
+ f"OAuth token store entry for server '{server_id}' field 'binding_id' is invalid."
80
+ )
81
+ return binding_id
82
+
83
+
84
+ def _normalize_datetime(value: datetime, *, field_name: str) -> datetime:
85
+ if not isinstance(value, datetime):
86
+ raise McpOAuthTokenStoreError(f"OAuth token field '{field_name}' must be a datetime.")
87
+ if value.tzinfo is None:
88
+ raise McpOAuthTokenStoreError(
89
+ f"OAuth token field '{field_name}' must be timezone-aware UTC."
90
+ )
91
+ return value.astimezone(UTC).replace(microsecond=0)
92
+
93
+
94
+ def _parse_datetime(value: object, *, field_name: str, server_id: str) -> datetime:
95
+ raw = _require_string(value, field_name=field_name, server_id=server_id)
96
+ normalized = raw[:-1] + "+00:00" if raw.endswith("Z") else raw
97
+ try:
98
+ parsed = datetime.fromisoformat(normalized)
99
+ except ValueError as exc:
100
+ raise McpOAuthTokenStoreError(
101
+ f"OAuth token store entry for server '{server_id}' field '{field_name}' must be ISO 8601 UTC."
102
+ ) from exc
103
+ if parsed.tzinfo is None:
104
+ raise McpOAuthTokenStoreError(
105
+ f"OAuth token store entry for server '{server_id}' field '{field_name}' must be timezone-aware UTC."
106
+ )
107
+ return parsed.astimezone(UTC).replace(microsecond=0)
108
+
109
+
110
+ def _format_datetime(value: datetime) -> str:
111
+ return _normalize_datetime(value, field_name="timestamp").isoformat().replace("+00:00", "Z")
112
+
113
+
114
+ @dataclass(frozen=True)
115
+ class McpOAuthTokenRecord:
116
+ access_token: str = field(repr=False)
117
+ token_type: str
118
+ expires_at: datetime
119
+ refresh_token: str | None = field(default=None, repr=False)
120
+ granted_scopes: tuple[str, ...] = field(default_factory=tuple)
121
+ obtained_at: datetime = field(default_factory=lambda: datetime.now(UTC).replace(microsecond=0))
122
+ binding_id: str | None = field(default=None, repr=False)
123
+
124
+ def __post_init__(self) -> None:
125
+ access_token = _require_string(
126
+ self.access_token, field_name="access_token", server_id="<record>"
127
+ )
128
+ token_type = _require_string(self.token_type, field_name="token_type", server_id="<record>")
129
+ refresh_token = _optional_string(
130
+ self.refresh_token,
131
+ field_name="refresh_token",
132
+ server_id="<record>",
133
+ )
134
+ scopes = tuple(
135
+ normalize_ordered_string_list(
136
+ list(self.granted_scopes),
137
+ field_name="granted_scopes",
138
+ )
139
+ )
140
+ expires_at = _normalize_datetime(self.expires_at, field_name="expires_at")
141
+ obtained_at = _normalize_datetime(self.obtained_at, field_name="obtained_at")
142
+ binding_id = _optional_binding_id(self.binding_id, server_id="<record>")
143
+ object.__setattr__(self, "access_token", access_token)
144
+ object.__setattr__(self, "token_type", token_type)
145
+ object.__setattr__(self, "refresh_token", refresh_token)
146
+ object.__setattr__(self, "granted_scopes", scopes)
147
+ object.__setattr__(self, "expires_at", expires_at)
148
+ object.__setattr__(self, "obtained_at", obtained_at)
149
+ object.__setattr__(self, "binding_id", binding_id)
150
+
151
+ def as_payload(self) -> dict[str, Any]:
152
+ payload = {
153
+ "access_token": self.access_token,
154
+ "token_type": self.token_type,
155
+ "expires_at": _format_datetime(self.expires_at),
156
+ "refresh_token": self.refresh_token,
157
+ "granted_scopes": list(self.granted_scopes),
158
+ "obtained_at": _format_datetime(self.obtained_at),
159
+ }
160
+ if self.binding_id is not None:
161
+ payload["binding_id"] = self.binding_id
162
+ return payload
163
+
164
+ @classmethod
165
+ def from_payload(cls, payload: object, *, server_id: str) -> McpOAuthTokenRecord:
166
+ if not isinstance(payload, dict):
167
+ raise McpOAuthTokenStoreError(
168
+ f"OAuth token store entry for server '{server_id}' must be an object."
169
+ )
170
+ scopes_raw = payload.get("granted_scopes")
171
+ try:
172
+ granted_scopes = tuple(
173
+ normalize_ordered_string_list(scopes_raw, field_name="granted_scopes")
174
+ )
175
+ except (TypeError, ValueError) as exc:
176
+ raise McpOAuthTokenStoreError(
177
+ f"OAuth token store entry for server '{server_id}' field 'granted_scopes' is invalid."
178
+ ) from exc
179
+ return cls(
180
+ access_token=_require_string(
181
+ payload.get("access_token"),
182
+ field_name="access_token",
183
+ server_id=server_id,
184
+ ),
185
+ token_type=_require_string(
186
+ payload.get("token_type"),
187
+ field_name="token_type",
188
+ server_id=server_id,
189
+ ),
190
+ expires_at=_parse_datetime(
191
+ payload.get("expires_at"),
192
+ field_name="expires_at",
193
+ server_id=server_id,
194
+ ),
195
+ refresh_token=_optional_string(
196
+ payload.get("refresh_token"),
197
+ field_name="refresh_token",
198
+ server_id=server_id,
199
+ ),
200
+ granted_scopes=granted_scopes,
201
+ obtained_at=_parse_datetime(
202
+ payload.get("obtained_at"),
203
+ field_name="obtained_at",
204
+ server_id=server_id,
205
+ ),
206
+ binding_id=_optional_binding_id(payload.get("binding_id"), server_id=server_id),
207
+ )
208
+
209
+
210
+ def _load_store_payload() -> tuple[dict[str, Any], bool]:
211
+ path = _store_path()
212
+ result = load_token_payload_result(path)
213
+ payload = result.payload
214
+ if not isinstance(payload, dict):
215
+ raise McpOAuthTokenStoreError(f"Invalid OAuth token store format: {path}")
216
+ normalized: dict[str, Any] = {}
217
+ for raw_server_id, record in payload.items():
218
+ if not isinstance(raw_server_id, str):
219
+ raise McpOAuthTokenStoreError(f"Invalid OAuth token store server id in: {path}")
220
+ normalized[_canonical_server_id(raw_server_id)] = record
221
+ return normalized, result.legacy_plaintext
222
+
223
+
224
+ def _migrate_after_successful_legacy_read(
225
+ payload: dict[str, Any], *, legacy_plaintext: bool
226
+ ) -> None:
227
+ if legacy_plaintext:
228
+ migrate_legacy_token_payload(_store_path(), payload)
229
+
230
+
231
+ def load_oauth_token_record(server_id: str) -> McpOAuthTokenRecord | None:
232
+ canonical_server_id = _canonical_server_id(server_id)
233
+ payload, legacy_plaintext = _load_store_payload()
234
+ record = payload.get(canonical_server_id)
235
+ if record is None:
236
+ _migrate_after_successful_legacy_read(payload, legacy_plaintext=legacy_plaintext)
237
+ return None
238
+ loaded = McpOAuthTokenRecord.from_payload(record, server_id=canonical_server_id)
239
+ _migrate_after_successful_legacy_read(payload, legacy_plaintext=legacy_plaintext)
240
+ return loaded
241
+
242
+
243
+ def save_oauth_token_record(server_id: str, record: McpOAuthTokenRecord) -> None:
244
+ canonical_server_id = _canonical_server_id(server_id)
245
+ path = _store_path()
246
+ payload, _legacy_plaintext = _load_store_payload()
247
+ payload[canonical_server_id] = record.as_payload()
248
+ try:
249
+ save_token_payload(path, payload)
250
+ except McpOAuthTokenStoreError as exc:
251
+ raise McpOAuthTokenStoreError(
252
+ f"Failed to write OAuth token store entry for server '{canonical_server_id}' to {path}"
253
+ ) from exc
254
+
255
+
256
+ def delete_oauth_token_record(server_id: str) -> bool:
257
+ canonical_server_id = _canonical_server_id(server_id)
258
+ payload, legacy_plaintext = _load_store_payload()
259
+ if canonical_server_id not in payload:
260
+ _migrate_after_successful_legacy_read(payload, legacy_plaintext=legacy_plaintext)
261
+ return False
262
+ del payload[canonical_server_id]
263
+ path = _store_path()
264
+ try:
265
+ save_token_payload(path, payload)
266
+ except McpOAuthTokenStoreError as exc:
267
+ raise McpOAuthTokenStoreError(
268
+ f"Failed to delete OAuth token store entry for server '{canonical_server_id}' from {path}"
269
+ ) from exc
270
+ return True
271
+
272
+
273
+ def list_oauth_token_server_ids() -> tuple[str, ...]:
274
+ payload, legacy_plaintext = _load_store_payload()
275
+ _migrate_after_successful_legacy_read(payload, legacy_plaintext=legacy_plaintext)
276
+ return tuple(sorted(payload))