alysis-code 0.13.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (458) hide show
  1. alysis_code/__init__.py +3 -0
  2. alysis_code/__main__.py +4 -0
  3. alysis_code/_build_info.py +14 -0
  4. alysis_code/account_login.py +468 -0
  5. alysis_code/agent/README.md +35 -0
  6. alysis_code/agent/__init__.py +11 -0
  7. alysis_code/agent/acceptance_contract.py +2217 -0
  8. alysis_code/agent/blast_radius.py +1403 -0
  9. alysis_code/agent/cache_keepalive.py +227 -0
  10. alysis_code/agent/completion_certificate.py +366 -0
  11. alysis_code/agent/completion_gate.py +306 -0
  12. alysis_code/agent/empty_response_stall.py +403 -0
  13. alysis_code/agent/errors.py +28 -0
  14. alysis_code/agent/llm_calls.py +475 -0
  15. alysis_code/agent/mutation_classification.py +227 -0
  16. alysis_code/agent/prompt_context.py +2508 -0
  17. alysis_code/agent/read_ledger.py +253 -0
  18. alysis_code/agent/regression_baseline.py +642 -0
  19. alysis_code/agent/reproduction_first.py +610 -0
  20. alysis_code/agent/sensitive_output.py +629 -0
  21. alysis_code/agent/session.py +3218 -0
  22. alysis_code/agent/steering.py +191 -0
  23. alysis_code/agent/subagent_execution.py +5177 -0
  24. alysis_code/agent/subagent_workspace.py +666 -0
  25. alysis_code/agent/tools_assembly.py +4728 -0
  26. alysis_code/agent/turn/__init__.py +101 -0
  27. alysis_code/agent/turn/core.py +8483 -0
  28. alysis_code/agent/turn/events.py +113 -0
  29. alysis_code/agent/turn/exploration.py +590 -0
  30. alysis_code/agent/turn/interventions.py +65 -0
  31. alysis_code/agent/turn/read_cache.py +420 -0
  32. alysis_code/agent/turn/snapshot.py +179 -0
  33. alysis_code/agent/turn_contract.py +661 -0
  34. alysis_code/agent/turn_path.py +129 -0
  35. alysis_code/agent/verification.py +2885 -0
  36. alysis_code/agent/verification_commands.py +512 -0
  37. alysis_code/agent/verification_evidence.py +738 -0
  38. alysis_code/agent_loop.py +693 -0
  39. alysis_code/agent_runtimes/__init__.py +51 -0
  40. alysis_code/agent_runtimes/base.py +114 -0
  41. alysis_code/agent_runtimes/builtins.py +129 -0
  42. alysis_code/agent_runtimes/codex_cli.py +664 -0
  43. alysis_code/agent_runtimes/host.py +263 -0
  44. alysis_code/agent_runtimes/registry.py +64 -0
  45. alysis_code/agent_runtimes/service.py +150 -0
  46. alysis_code/agentbox_client.py +416 -0
  47. alysis_code/agentbox_integration.py +310 -0
  48. alysis_code/alysis_cloud.py +152 -0
  49. alysis_code/approval_scope.py +276 -0
  50. alysis_code/assets/README.md +33 -0
  51. alysis_code/assets/__init__.py +126 -0
  52. alysis_code/assets/asset_read_core.py +281 -0
  53. alysis_code/assets/budget_allocator.py +456 -0
  54. alysis_code/assets/comprehender.py +759 -0
  55. alysis_code/assets/index.py +654 -0
  56. alysis_code/assets/ingestion.py +275 -0
  57. alysis_code/assets/legacy_migration.py +413 -0
  58. alysis_code/assets/models.py +263 -0
  59. alysis_code/assets/ocr.py +239 -0
  60. alysis_code/assets/owl/ascii/f-000.txt +13 -0
  61. alysis_code/assets/owl/ascii/f-001.txt +13 -0
  62. alysis_code/assets/owl/ascii/f-002.txt +13 -0
  63. alysis_code/assets/owl/ascii/f-003.txt +13 -0
  64. alysis_code/assets/owl/ascii/f-004.txt +13 -0
  65. alysis_code/assets/owl/ascii/f-005.txt +13 -0
  66. alysis_code/assets/owl/ascii/f-006.txt +13 -0
  67. alysis_code/assets/owl/ascii/f-007.txt +13 -0
  68. alysis_code/assets/owl/ascii/f-008.txt +13 -0
  69. alysis_code/assets/owl/ascii/f-009.txt +13 -0
  70. alysis_code/assets/owl/ascii/f-010.txt +13 -0
  71. alysis_code/assets/owl/ascii/f-011.txt +13 -0
  72. alysis_code/assets/owl/ascii/f-012.txt +13 -0
  73. alysis_code/assets/owl/ascii/f-013.txt +13 -0
  74. alysis_code/assets/owl/ascii/f-014.txt +13 -0
  75. alysis_code/assets/owl/ascii/f-015.txt +13 -0
  76. alysis_code/assets/owl/ascii/f-016.txt +13 -0
  77. alysis_code/assets/owl/ascii/f-017.txt +13 -0
  78. alysis_code/assets/owl/ascii/f-018.txt +13 -0
  79. alysis_code/assets/owl/ascii/f-019.txt +13 -0
  80. alysis_code/assets/owl/ascii/f-020.txt +13 -0
  81. alysis_code/assets/owl/index.html +98 -0
  82. alysis_code/assets/owl/show-owl.sh +761 -0
  83. alysis_code/assets/paths.py +49 -0
  84. alysis_code/assets/plan_binding.py +326 -0
  85. alysis_code/assets/planner_context.py +466 -0
  86. alysis_code/assets/planner_tools.py +184 -0
  87. alysis_code/assets/prompts.py +101 -0
  88. alysis_code/assets/replanner_context.py +239 -0
  89. alysis_code/assets/surface.py +521 -0
  90. alysis_code/assets/untrusted_content.py +48 -0
  91. alysis_code/assets/usage_logger.py +94 -0
  92. alysis_code/assets/worker_mirror.py +428 -0
  93. alysis_code/assets/worker_section.py +303 -0
  94. alysis_code/assets/worker_tools.py +468 -0
  95. alysis_code/atomic_io.py +83 -0
  96. alysis_code/auth_diagnostics.py +272 -0
  97. alysis_code/background_runner.py +366 -0
  98. alysis_code/branding.py +270 -0
  99. alysis_code/budget_policy.py +390 -0
  100. alysis_code/build_identity.py +465 -0
  101. alysis_code/builtin_hooks/__init__.py +7 -0
  102. alysis_code/builtin_hooks/notify_done_windows.py +65 -0
  103. alysis_code/bwrap_etc.py +76 -0
  104. alysis_code/cancellation.py +41 -0
  105. alysis_code/capabilities.py +137 -0
  106. alysis_code/chatgpt_codex_static_provider.py +133 -0
  107. alysis_code/cli.py +51 -0
  108. alysis_code/cli_impl/__init__.py +1 -0
  109. alysis_code/cli_impl/assets_cli.py +537 -0
  110. alysis_code/cli_impl/assets_modal.py +412 -0
  111. alysis_code/cli_impl/chat/__init__.py +156 -0
  112. alysis_code/cli_impl/chat/commands.py +2616 -0
  113. alysis_code/cli_impl/chat/loop.py +4508 -0
  114. alysis_code/cli_impl/chat/mid_turn_policy.py +125 -0
  115. alysis_code/cli_impl/chat/rendering.py +444 -0
  116. alysis_code/cli_impl/chat/state.py +124 -0
  117. alysis_code/cli_impl/chat_resume.py +830 -0
  118. alysis_code/cli_impl/chat_slash_completer.py +258 -0
  119. alysis_code/cli_impl/commands/__init__.py +11 -0
  120. alysis_code/cli_impl/commands/_shared.py +89 -0
  121. alysis_code/cli_impl/commands/auth.py +623 -0
  122. alysis_code/cli_impl/commands/chat_resume_helpers.py +1531 -0
  123. alysis_code/cli_impl/commands/chat_state.py +158 -0
  124. alysis_code/cli_impl/commands/chat_status.py +1248 -0
  125. alysis_code/cli_impl/commands/chat_terminal.py +942 -0
  126. alysis_code/cli_impl/commands/chat_tui_panels.py +1018 -0
  127. alysis_code/cli_impl/commands/cli_common.py +1223 -0
  128. alysis_code/cli_impl/commands/cli_surface.py +77 -0
  129. alysis_code/cli_impl/commands/config.py +131 -0
  130. alysis_code/cli_impl/commands/conventions.py +85 -0
  131. alysis_code/cli_impl/commands/execution_helpers.py +350 -0
  132. alysis_code/cli_impl/commands/extensions.py +401 -0
  133. alysis_code/cli_impl/commands/forge.py +1282 -0
  134. alysis_code/cli_impl/commands/forge_asset_view.py +121 -0
  135. alysis_code/cli_impl/commands/forge_helpers.py +1215 -0
  136. alysis_code/cli_impl/commands/hooks.py +737 -0
  137. alysis_code/cli_impl/commands/ide_bridge.py +31 -0
  138. alysis_code/cli_impl/commands/mcp.py +700 -0
  139. alysis_code/cli_impl/commands/profile.py +453 -0
  140. alysis_code/cli_impl/commands/prompt_helpers.py +307 -0
  141. alysis_code/cli_impl/commands/report.py +88 -0
  142. alysis_code/cli_impl/commands/root.py +1118 -0
  143. alysis_code/cli_impl/commands/sandbox.py +184 -0
  144. alysis_code/cli_impl/commands/server.py +54 -0
  145. alysis_code/cli_impl/commands/sessions.py +252 -0
  146. alysis_code/cli_impl/commands/skills.py +404 -0
  147. alysis_code/cli_impl/commands/startup.py +946 -0
  148. alysis_code/cli_impl/commands/tools.py +335 -0
  149. alysis_code/cli_impl/commands/update.py +364 -0
  150. alysis_code/cli_impl/commands/welcome.py +972 -0
  151. alysis_code/cli_impl/config_menu.py +3882 -0
  152. alysis_code/cli_impl/forge.py +4509 -0
  153. alysis_code/cli_impl/forge_recovery.py +485 -0
  154. alysis_code/cli_impl/setup_wizard.py +2409 -0
  155. alysis_code/cli_impl/tui/__init__.py +58 -0
  156. alysis_code/cli_impl/tui/app.py +4551 -0
  157. alysis_code/cli_impl/tui/config.py +32 -0
  158. alysis_code/cli_impl/tui/config_flow.py +2754 -0
  159. alysis_code/cli_impl/tui/config_overlay.py +566 -0
  160. alysis_code/cli_impl/tui/content.py +78 -0
  161. alysis_code/cli_impl/tui/footer.py +218 -0
  162. alysis_code/cli_impl/tui/forge_status.py +136 -0
  163. alysis_code/cli_impl/tui/markdown.py +244 -0
  164. alysis_code/cli_impl/tui/owl.py +109 -0
  165. alysis_code/cli_impl/tui/plan_meta.py +477 -0
  166. alysis_code/cli_impl/tui/setup_app.py +519 -0
  167. alysis_code/cli_impl/tui/setup_flow.py +1622 -0
  168. alysis_code/cli_impl/tui/state.py +101 -0
  169. alysis_code/cli_impl/tui/subagent_identity.py +66 -0
  170. alysis_code/cli_impl/tui/subagent_panel.py +186 -0
  171. alysis_code/cli_impl/tui/surface.py +796 -0
  172. alysis_code/cli_impl/tui/transcript.py +514 -0
  173. alysis_code/cli_impl/tui/update_prompt.py +79 -0
  174. alysis_code/cli_impl/tui/workspace_guard.py +384 -0
  175. alysis_code/clipboard.py +172 -0
  176. alysis_code/code_review.py +1211 -0
  177. alysis_code/compaction/__init__.py +28 -0
  178. alysis_code/compaction/conversation_compactor.py +2932 -0
  179. alysis_code/compaction/importance.py +177 -0
  180. alysis_code/compaction/settings.py +297 -0
  181. alysis_code/compaction/tool_output_offload.py +447 -0
  182. alysis_code/config.py +3509 -0
  183. alysis_code/conflict_auto_resolver.py +895 -0
  184. alysis_code/context/__init__.py +1 -0
  185. alysis_code/context/tool_schema_budgeter.py +220 -0
  186. alysis_code/crash_diagnostics.py +282 -0
  187. alysis_code/custom_tools/README.md +34 -0
  188. alysis_code/custom_tools/__init__.py +43 -0
  189. alysis_code/custom_tools/discovery.py +903 -0
  190. alysis_code/custom_tools/runtime.py +1516 -0
  191. alysis_code/custom_tools/session.py +227 -0
  192. alysis_code/custom_tools/trust.py +232 -0
  193. alysis_code/diff_paths.py +113 -0
  194. alysis_code/direction_change.py +293 -0
  195. alysis_code/dispatch_timing.py +306 -0
  196. alysis_code/durable_service_manager.py +1236 -0
  197. alysis_code/edit_discipline.py +659 -0
  198. alysis_code/error_text.py +73 -0
  199. alysis_code/execution_budget.py +411 -0
  200. alysis_code/execution_context.py +915 -0
  201. alysis_code/execution_deadline.py +1065 -0
  202. alysis_code/execution_shared.py +1904 -0
  203. alysis_code/extensions/README.md +30 -0
  204. alysis_code/extensions/__init__.py +93 -0
  205. alysis_code/extensions/activation.py +138 -0
  206. alysis_code/extensions/install.py +1436 -0
  207. alysis_code/extensions/manifest.py +487 -0
  208. alysis_code/extensions/models.py +74 -0
  209. alysis_code/extensions/paths.py +56 -0
  210. alysis_code/extensions/registry.json +4 -0
  211. alysis_code/extensions/registry.py +52 -0
  212. alysis_code/extensions/state.py +83 -0
  213. alysis_code/extensions/workspace_trust.py +101 -0
  214. alysis_code/failed_task_evidence.py +369 -0
  215. alysis_code/failure_category.py +315 -0
  216. alysis_code/feedback_report.py +1647 -0
  217. alysis_code/file_classification.py +485 -0
  218. alysis_code/forge.py +2064 -0
  219. alysis_code/forge_completion.py +362 -0
  220. alysis_code/forge_events.py +475 -0
  221. alysis_code/frontmatter_utils.py +95 -0
  222. alysis_code/git_evidence.py +1181 -0
  223. alysis_code/git_ops.py +560 -0
  224. alysis_code/git_safe.py +62 -0
  225. alysis_code/git_worktrees.py +190 -0
  226. alysis_code/hooks/README.md +33 -0
  227. alysis_code/hooks/__init__.py +67 -0
  228. alysis_code/hooks/audit.py +171 -0
  229. alysis_code/hooks/config.py +225 -0
  230. alysis_code/hooks/dispatcher.py +1110 -0
  231. alysis_code/hooks/models.py +447 -0
  232. alysis_code/hooks/trust.py +202 -0
  233. alysis_code/host_actions.py +543 -0
  234. alysis_code/host_browser.py +103 -0
  235. alysis_code/ide/__init__.py +5 -0
  236. alysis_code/ide/activity_events.py +399 -0
  237. alysis_code/ide/approvals.py +337 -0
  238. alysis_code/ide/artifacts.py +153 -0
  239. alysis_code/ide/browser_egress_proxy.py +1076 -0
  240. alysis_code/ide/cdp_websocket_transport.py +1192 -0
  241. alysis_code/ide/change_ledger.py +1721 -0
  242. alysis_code/ide/context_blocks.py +979 -0
  243. alysis_code/ide/event_stream.py +531 -0
  244. alysis_code/ide/forge_protocol.py +3112 -0
  245. alysis_code/ide/forge_request_ledger.py +737 -0
  246. alysis_code/ide/health.py +965 -0
  247. alysis_code/ide/managed_browser.py +2251 -0
  248. alysis_code/ide/management_protocol.py +3414 -0
  249. alysis_code/ide/mcp_oauth_coordinator.py +744 -0
  250. alysis_code/ide/mcp_oauth_lifecycle.py +1504 -0
  251. alysis_code/ide/prompt_queue.py +1070 -0
  252. alysis_code/ide/protocol.py +191 -0
  253. alysis_code/ide/resumable_swarm.py +1543 -0
  254. alysis_code/ide/session_search.py +295 -0
  255. alysis_code/ide/stdio_bridge.py +9935 -0
  256. alysis_code/ide/structured_state.py +1579 -0
  257. alysis_code/ide/swarm_protocol.py +816 -0
  258. alysis_code/integration_gate.py +506 -0
  259. alysis_code/interactive_input_guard.py +39 -0
  260. alysis_code/interactive_plan_mode.py +26 -0
  261. alysis_code/internal_artifacts.py +179 -0
  262. alysis_code/knowledge_base.py +1409 -0
  263. alysis_code/knowledge_capture.py +1190 -0
  264. alysis_code/knowledge_librarian.py +605 -0
  265. alysis_code/language_policy.py +34 -0
  266. alysis_code/litellm_static_provider.py +535 -0
  267. alysis_code/llm/__init__.py +1 -0
  268. alysis_code/llm/anthropic_messages.py +2288 -0
  269. alysis_code/llm/base.py +71 -0
  270. alysis_code/llm/cache_capabilities.py +985 -0
  271. alysis_code/llm/cache_control_blocks.py +244 -0
  272. alysis_code/llm/cache_policy.py +388 -0
  273. alysis_code/llm/factory.py +373 -0
  274. alysis_code/llm/gemini_generate_content.py +2652 -0
  275. alysis_code/llm/gemini_interactions.py +739 -0
  276. alysis_code/llm/metadata.py +450 -0
  277. alysis_code/llm/openai_compat.py +2947 -0
  278. alysis_code/llm/openai_responses.py +2604 -0
  279. alysis_code/llm/protocols.py +609 -0
  280. alysis_code/llm/provider_limits.py +525 -0
  281. alysis_code/llm/request_plan.py +389 -0
  282. alysis_code/llm/request_shape.py +238 -0
  283. alysis_code/llm/streaming.py +108 -0
  284. alysis_code/llm/temperature_compat.py +78 -0
  285. alysis_code/llm/types.py +195 -0
  286. alysis_code/llm/usage_normalization.py +222 -0
  287. alysis_code/llm_error_display.py +315 -0
  288. alysis_code/logging_redaction.py +326 -0
  289. alysis_code/managed_host_deadline.py +196 -0
  290. alysis_code/mcp/README.md +33 -0
  291. alysis_code/mcp/__init__.py +24 -0
  292. alysis_code/mcp/client.py +1137 -0
  293. alysis_code/mcp/config.py +597 -0
  294. alysis_code/mcp/errors.py +113 -0
  295. alysis_code/mcp/forge_scope.py +154 -0
  296. alysis_code/mcp/jsonrpc.py +214 -0
  297. alysis_code/mcp/manager.py +2308 -0
  298. alysis_code/mcp/models.py +666 -0
  299. alysis_code/mcp/oauth.py +972 -0
  300. alysis_code/mcp/oauth_runtime.py +310 -0
  301. alysis_code/mcp/oauth_store.py +276 -0
  302. alysis_code/mcp/prompts.py +329 -0
  303. alysis_code/mcp/resources.py +295 -0
  304. alysis_code/mcp/roots.py +106 -0
  305. alysis_code/mcp/server_requests.py +75 -0
  306. alysis_code/mcp/token_store.py +859 -0
  307. alysis_code/mcp/transport_http.py +1338 -0
  308. alysis_code/mcp/transport_stdio.py +1267 -0
  309. alysis_code/mcp/untrusted_content.py +119 -0
  310. alysis_code/merge_conflict_reviewer.py +729 -0
  311. alysis_code/model_catalog/__init__.py +1 -0
  312. alysis_code/model_catalog/chatgpt_codex_subscription_snapshot.json +186 -0
  313. alysis_code/model_catalog/litellm_model_prices_snapshot.json +44715 -0
  314. alysis_code/model_catalog/litellm_model_prices_snapshot.meta.json +17 -0
  315. alysis_code/model_metadata_policy.py +223 -0
  316. alysis_code/model_metadata_utils.py +103 -0
  317. alysis_code/model_registry.py +1420 -0
  318. alysis_code/model_router.py +147 -0
  319. alysis_code/permission_policy.py +1016 -0
  320. alysis_code/personas.py +451 -0
  321. alysis_code/pipeline_facts.py +233 -0
  322. alysis_code/plan_assistant.py +4763 -0
  323. alysis_code/plan_mode.py +393 -0
  324. alysis_code/plan_reconciliation.py +1228 -0
  325. alysis_code/plan_repair.py +652 -0
  326. alysis_code/plan_validation.py +1099 -0
  327. alysis_code/planning_constraints.py +904 -0
  328. alysis_code/policy.py +95 -0
  329. alysis_code/preview_server.py +457 -0
  330. alysis_code/process_reaping.py +566 -0
  331. alysis_code/profile_presets.py +1834 -0
  332. alysis_code/profiles.py +666 -0
  333. alysis_code/provider_auth/__init__.py +29 -0
  334. alysis_code/provider_auth/base.py +99 -0
  335. alysis_code/provider_auth/openai_codex.py +951 -0
  336. alysis_code/provider_auth/registry.py +76 -0
  337. alysis_code/provider_auth/store.py +125 -0
  338. alysis_code/provider_diagnostics.py +1209 -0
  339. alysis_code/provider_model_catalog.py +685 -0
  340. alysis_code/provider_telemetry.py +1699 -0
  341. alysis_code/provider_url.py +75 -0
  342. alysis_code/reasoning_contracts.py +911 -0
  343. alysis_code/remote_sync.py +350 -0
  344. alysis_code/replanning.py +1195 -0
  345. alysis_code/repo_scan.py +1152 -0
  346. alysis_code/request_estimation.py +296 -0
  347. alysis_code/review_gate.py +617 -0
  348. alysis_code/run_lock.py +1141 -0
  349. alysis_code/run_outcome.py +58 -0
  350. alysis_code/run_provenance.py +774 -0
  351. alysis_code/run_state.py +445 -0
  352. alysis_code/runtime_artifacts.py +116 -0
  353. alysis_code/runtime_context_features.py +78 -0
  354. alysis_code/runtime_kind.py +52 -0
  355. alysis_code/safety/__init__.py +11 -0
  356. alysis_code/safety/mcp_sanitize.py +29 -0
  357. alysis_code/safety/safe_http.py +297 -0
  358. alysis_code/safety/subagent_report.py +184 -0
  359. alysis_code/sandbox_doctor.py +682 -0
  360. alysis_code/sandbox_runner.py +1025 -0
  361. alysis_code/sandbox_settings.py +423 -0
  362. alysis_code/serialized_paths.py +355 -0
  363. alysis_code/server/__init__.py +3 -0
  364. alysis_code/server/app.py +367 -0
  365. alysis_code/server/auth.py +34 -0
  366. alysis_code/server/job_config.py +30 -0
  367. alysis_code/server/settings.py +215 -0
  368. alysis_code/server/store.py +193 -0
  369. alysis_code/server/worker_runner.py +657 -0
  370. alysis_code/service_persistence.py +355 -0
  371. alysis_code/session_artifacts.py +108 -0
  372. alysis_code/session_metrics.py +331 -0
  373. alysis_code/session_store.py +624 -0
  374. alysis_code/skills/README.md +34 -0
  375. alysis_code/skills/__init__.py +104 -0
  376. alysis_code/skills/conventions.py +84 -0
  377. alysis_code/skills/discovery.py +176 -0
  378. alysis_code/skills/eval_models.py +232 -0
  379. alysis_code/skills/eval_runner.py +372 -0
  380. alysis_code/skills/evals.py +1344 -0
  381. alysis_code/skills/install.py +293 -0
  382. alysis_code/skills/loader.py +118 -0
  383. alysis_code/skills/matching.py +103 -0
  384. alysis_code/skills/models.py +71 -0
  385. alysis_code/skills/paths.py +56 -0
  386. alysis_code/skills/prompting.py +500 -0
  387. alysis_code/skills/scaffold.py +142 -0
  388. alysis_code/skills/state.py +441 -0
  389. alysis_code/skills/transactions.py +125 -0
  390. alysis_code/skills/validation.py +304 -0
  391. alysis_code/step_budget.py +238 -0
  392. alysis_code/subagent_labels.py +49 -0
  393. alysis_code/subagents.py +1072 -0
  394. alysis_code/surface/__init__.py +80 -0
  395. alysis_code/surface/base.py +305 -0
  396. alysis_code/surface/console.py +387 -0
  397. alysis_code/surface/events.py +372 -0
  398. alysis_code/surface/hidden_surface.py +529 -0
  399. alysis_code/surface/noop_surface.py +219 -0
  400. alysis_code/surface/rich_surface.py +1555 -0
  401. alysis_code/surface/styles.py +67 -0
  402. alysis_code/surface/theme.py +455 -0
  403. alysis_code/surface/types.py +100 -0
  404. alysis_code/swarm_backend.py +926 -0
  405. alysis_code/swarm_orchestrator.py +4020 -0
  406. alysis_code/swarm_scheduler.py +441 -0
  407. alysis_code/swarm_trace.py +429 -0
  408. alysis_code/swarm_worker.py +2119 -0
  409. alysis_code/swarm_write_guard.py +348 -0
  410. alysis_code/task_dependencies.py +170 -0
  411. alysis_code/task_readiness.py +992 -0
  412. alysis_code/task_scope.py +2148 -0
  413. alysis_code/terminal_manager.py +762 -0
  414. alysis_code/terminal_ownership.py +460 -0
  415. alysis_code/text_normalization.py +30 -0
  416. alysis_code/token_budget.py +97 -0
  417. alysis_code/tools/README.md +34 -0
  418. alysis_code/tools/__init__.py +1 -0
  419. alysis_code/tools/artifacts.py +127 -0
  420. alysis_code/tools/availability.py +188 -0
  421. alysis_code/tools/fs.py +1456 -0
  422. alysis_code/tools/git.py +461 -0
  423. alysis_code/tools/history.py +229 -0
  424. alysis_code/tools/http_timeout.py +78 -0
  425. alysis_code/tools/image_generation.py +552 -0
  426. alysis_code/tools/registry.py +2936 -0
  427. alysis_code/tools/repo_map.py +476 -0
  428. alysis_code/tools/search.py +563 -0
  429. alysis_code/tools/shell.py +135 -0
  430. alysis_code/tools/symbols.py +1350 -0
  431. alysis_code/tools/test_discovery.py +643 -0
  432. alysis_code/tools/web.py +482 -0
  433. alysis_code/tools/web_search.py +2012 -0
  434. alysis_code/tools/web_search_dashscope.py +557 -0
  435. alysis_code/tools/web_search_ddgs.py +221 -0
  436. alysis_code/tools/web_search_provider_adapters.py +1429 -0
  437. alysis_code/tools/web_search_tavily.py +194 -0
  438. alysis_code/updates.py +933 -0
  439. alysis_code/usage_tracker.py +1990 -0
  440. alysis_code/verification_command_analysis.py +1004 -0
  441. alysis_code/verification_contract.py +574 -0
  442. alysis_code/verification_failure_summary.py +273 -0
  443. alysis_code/verification_repair.py +385 -0
  444. alysis_code/verify_gate.py +3129 -0
  445. alysis_code/web_research.py +1872 -0
  446. alysis_code/web_search_adapters.py +66 -0
  447. alysis_code/web_search_policy.py +27 -0
  448. alysis_code/workspace_binding.py +389 -0
  449. alysis_code/workspace_binding_ui.py +408 -0
  450. alysis_code/workspace_context.py +273 -0
  451. alysis_code/workspace_isolation.py +138 -0
  452. alysis_code/workspace_provisioning.py +455 -0
  453. alysis_code-0.13.0.dist-info/METADATA +507 -0
  454. alysis_code-0.13.0.dist-info/RECORD +458 -0
  455. alysis_code-0.13.0.dist-info/WHEEL +4 -0
  456. alysis_code-0.13.0.dist-info/entry_points.txt +3 -0
  457. alysis_code-0.13.0.dist-info/licenses/LICENSE +176 -0
  458. alysis_code-0.13.0.dist-info/licenses/NOTICE +4 -0
@@ -0,0 +1,666 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from dataclasses import dataclass, field
5
+ from typing import TYPE_CHECKING, Any
6
+ from urllib.parse import urlsplit
7
+
8
+ from .llm.cache_capabilities import CacheCapabilitySpec
9
+ from .llm.metadata import canonicalize_extra_headers
10
+ from .llm.protocols import (
11
+ OPENAI_COMPAT_PROTOCOL,
12
+ SUPPORTED_LLM_PROTOCOLS,
13
+ normalize_reasoning_trace_adapter,
14
+ validate_reasoning_trace_adapter_for_protocol,
15
+ )
16
+ from .web_search_adapters import normalize_web_search_adapter
17
+
18
+ if TYPE_CHECKING:
19
+ from .config import AppConfig
20
+
21
+ SUPPORTED_PROTOCOLS: frozenset[str] = SUPPORTED_LLM_PROTOCOLS
22
+ DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1"
23
+ _PROFILE_NAME_RE = re.compile(r"^[a-z0-9_-]+$")
24
+ _REASONING_EFFORT_RE = re.compile(r"^[a-z][a-z0-9_-]{0,31}$")
25
+ _UNSET = object()
26
+ SUBSCRIPTION_SELECTION_REQUIRED_KEY = "subscription_model_selection_required"
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class ProfileSpec:
31
+ name: str
32
+ protocol: str = OPENAI_COMPAT_PROTOCOL
33
+ base_url: str = ""
34
+ api_key_env: str | None = None
35
+ auth_provider: str | None = None
36
+ extra_headers: dict[str, str] = field(default_factory=dict)
37
+ default_model: str = ""
38
+ reasoning_effort: str | None = None
39
+ reasoning_trace_adapter: str = "auto"
40
+ web_search_adapter: str = "auto"
41
+ web_search_model: str = ""
42
+ notes: str = ""
43
+ cache_capability: CacheCapabilitySpec | None = None
44
+
45
+ def __post_init__(self) -> None:
46
+ _validate_profile_name(self.name)
47
+ _validate_protocol(self.protocol)
48
+ _validate_web_search_adapter(self.web_search_adapter)
49
+ normalized_reasoning_trace_adapter = validate_reasoning_trace_adapter_for_protocol(
50
+ protocol=self.protocol,
51
+ adapter=self.reasoning_trace_adapter,
52
+ )
53
+ object.__setattr__(
54
+ self,
55
+ "reasoning_trace_adapter",
56
+ normalized_reasoning_trace_adapter,
57
+ )
58
+ if self.base_url:
59
+ _validate_base_url(self.base_url)
60
+ if (
61
+ self.reasoning_effort is not None
62
+ and _REASONING_EFFORT_RE.fullmatch(self.reasoning_effort) is None
63
+ ):
64
+ _raise_config_error("Profile reasoning_effort is not a valid provider effort id.")
65
+ if self.cache_capability is not None and not isinstance(
66
+ self.cache_capability, CacheCapabilitySpec
67
+ ):
68
+ _raise_config_error("Profile cache_capability must be a JSON object.")
69
+ try:
70
+ normalized_headers = canonicalize_extra_headers(self.extra_headers)
71
+ except ValueError as exc:
72
+ _raise_config_error(str(exc))
73
+ object.__setattr__(self, "extra_headers", normalized_headers)
74
+
75
+ def to_dict(self) -> dict[str, Any]:
76
+ data: dict[str, Any] = {
77
+ "protocol": self.protocol,
78
+ "base_url": self.base_url,
79
+ "extra_headers": dict(sorted(self.extra_headers.items())),
80
+ "default_model": self.default_model,
81
+ "web_search_adapter": normalize_web_search_adapter(self.web_search_adapter),
82
+ "web_search_model": self.web_search_model,
83
+ "notes": self.notes,
84
+ }
85
+ if self.api_key_env:
86
+ data["api_key_env"] = self.api_key_env
87
+ if self.auth_provider:
88
+ data["auth_provider"] = self.auth_provider
89
+ if self.reasoning_effort:
90
+ data["reasoning_effort"] = self.reasoning_effort
91
+ reasoning_trace_adapter = normalize_reasoning_trace_adapter(self.reasoning_trace_adapter)
92
+ if reasoning_trace_adapter != "auto":
93
+ data["reasoning_trace_adapter"] = reasoning_trace_adapter
94
+ if self.cache_capability is not None and self.cache_capability.has_values():
95
+ data["cache_capability"] = self.cache_capability.to_dict()
96
+ return data
97
+
98
+ @classmethod
99
+ def from_dict(cls, name: str, data: dict[str, Any]) -> ProfileSpec:
100
+ if not isinstance(data, dict):
101
+ _raise_config_error(f"Profile {name!r} must be a JSON object.")
102
+ cache_capability = _coerce_cache_capability(
103
+ data.get("cache_capability"),
104
+ source=f"Profile {name!r}",
105
+ )
106
+ return cls(
107
+ name=name,
108
+ protocol=str(data.get("protocol") or OPENAI_COMPAT_PROTOCOL).strip(),
109
+ base_url=str(data.get("base_url") or "").strip(),
110
+ api_key_env=_optional_string(data.get("api_key_env")),
111
+ auth_provider=_optional_string(data.get("auth_provider")),
112
+ extra_headers=_coerce_headers(data.get("extra_headers")),
113
+ default_model=str(data.get("default_model") or "").strip(),
114
+ reasoning_effort=_optional_string(data.get("reasoning_effort")),
115
+ reasoning_trace_adapter=normalize_reasoning_trace_adapter(
116
+ _optional_string(data.get("reasoning_trace_adapter"))
117
+ ),
118
+ web_search_adapter=str(data.get("web_search_adapter") or "auto").strip(),
119
+ web_search_model=str(data.get("web_search_model") or "").strip(),
120
+ notes=str(data.get("notes") or ""),
121
+ cache_capability=cache_capability,
122
+ )
123
+
124
+
125
+ def list_profiles(cfg: AppConfig) -> list[ProfileSpec]:
126
+ migrate_legacy_to_profiles(cfg)
127
+ profiles = _profile_dict(cfg)
128
+ return [ProfileSpec.from_dict(name, profiles[name]) for name in sorted(profiles)]
129
+
130
+
131
+ def get_profile(cfg: AppConfig, name: str) -> ProfileSpec | None:
132
+ migrate_legacy_to_profiles(cfg)
133
+ profile_name = _normalize_profile_name(name)
134
+ profiles = _profile_dict(cfg)
135
+ data = profiles.get(profile_name)
136
+ if not isinstance(data, dict):
137
+ return None
138
+ return ProfileSpec.from_dict(profile_name, data)
139
+
140
+
141
+ def get_active_profile(cfg: AppConfig) -> ProfileSpec:
142
+ migrate_legacy_to_profiles(cfg)
143
+ active = str((cfg.extra_fields or {}).get("active_profile") or "").strip()
144
+ if not active:
145
+ _raise_config_error("No active profile configured.")
146
+ profile = get_profile(cfg, active)
147
+ if profile is None:
148
+ _raise_config_error(f"Active profile not found: {active}")
149
+ return profile
150
+
151
+
152
+ def active_subscription_selection_ready(cfg: AppConfig) -> bool:
153
+ """Whether the active auth profile has an explicit /config model+effort choice."""
154
+
155
+ profile = get_active_profile(cfg)
156
+ if not profile.auth_provider:
157
+ return True
158
+ marker = (cfg.extra_fields or {}).get(SUBSCRIPTION_SELECTION_REQUIRED_KEY)
159
+ if marker is True or str(marker or "").strip() == profile.auth_provider:
160
+ return False
161
+ return bool(profile.default_model and profile.reasoning_effort is not None)
162
+
163
+
164
+ def subscription_selection_supported(profile: ProfileSpec, models: Any) -> bool:
165
+ """Check an explicit profile selection against a provider model catalog."""
166
+
167
+ if not profile.default_model or profile.reasoning_effort is None:
168
+ return False
169
+ selected = next(
170
+ (item for item in models if str(getattr(item, "id", "")) == profile.default_model),
171
+ None,
172
+ )
173
+ if selected is None:
174
+ return False
175
+ if profile.reasoning_effort == "auto":
176
+ return True
177
+ supported = {
178
+ str(getattr(item, "id", ""))
179
+ for item in tuple(getattr(selected, "reasoning_efforts", ()) or ())
180
+ }
181
+ return not supported or profile.reasoning_effort in supported
182
+
183
+
184
+ def sync_active_profile_to_config(cfg: AppConfig) -> bool:
185
+ """Mirror the active profile connection defaults onto legacy top-level fields."""
186
+ active = str((cfg.extra_fields or {}).get("active_profile") or "").strip()
187
+ if not active:
188
+ return False
189
+ profile = get_profile(cfg, active)
190
+ if profile is None:
191
+ return False
192
+
193
+ changed = False
194
+ if profile.base_url and not _same_base_url(getattr(cfg, "base_url", ""), profile.base_url):
195
+ cfg.base_url = profile.base_url
196
+ changed = True
197
+ desired_model = (
198
+ profile.default_model if profile.auth_provider else profile.default_model or None
199
+ )
200
+ if desired_model is not None and str(getattr(cfg, "model", "") or "") != desired_model:
201
+ cfg.model = desired_model
202
+ changed = True
203
+ if profile.reasoning_effort is not None or profile.auth_provider:
204
+ effective_effort = (
205
+ None if profile.reasoning_effort in {None, "auto"} else profile.reasoning_effort
206
+ )
207
+ effective_thinking = None if effective_effort is None else effective_effort != "none"
208
+ if (
209
+ getattr(cfg, "llm_reasoning_effort", None) == effective_effort
210
+ and getattr(cfg, "llm_enable_thinking", None) == effective_thinking
211
+ ):
212
+ return changed
213
+ cfg.llm_reasoning_effort = effective_effort
214
+ cfg.llm_enable_thinking = effective_thinking
215
+ changed = True
216
+ return changed
217
+
218
+
219
+ def add_profile(
220
+ cfg: AppConfig,
221
+ profile: ProfileSpec,
222
+ *,
223
+ allow_auth_profile_update: bool = False,
224
+ ) -> None:
225
+ migrate_legacy_to_profiles(cfg)
226
+ profiles = dict(_profile_dict(cfg))
227
+ existing_data = profiles.get(profile.name)
228
+ if isinstance(existing_data, dict):
229
+ existing = ProfileSpec.from_dict(profile.name, existing_data)
230
+ replacing_auth_profile = bool(existing.auth_provider)
231
+ unchanged = existing.to_dict() == profile.to_dict()
232
+ if replacing_auth_profile and not unchanged and not allow_auth_profile_update:
233
+ _raise_config_error(
234
+ f"Profile {profile.name!r} is managed by the AI subscription "
235
+ f"{existing.auth_provider!r} and cannot be replaced by a generic profile edit."
236
+ )
237
+ profiles[profile.name] = profile.to_dict()
238
+ _set_profile_dict(cfg, profiles)
239
+
240
+
241
+ def remove_profile(cfg: AppConfig, name: str) -> None:
242
+ migrate_legacy_to_profiles(cfg)
243
+ profile_name = _normalize_profile_name(name)
244
+ profiles = dict(_profile_dict(cfg))
245
+ if profile_name not in profiles:
246
+ _raise_config_error(f"Profile not found: {profile_name}")
247
+ profiles.pop(profile_name, None)
248
+ _set_profile_dict(cfg, profiles)
249
+ active = str((cfg.extra_fields or {}).get("active_profile") or "").strip()
250
+ if active != profile_name:
251
+ return
252
+ if profiles:
253
+ set_active_profile(cfg, sorted(profiles)[0])
254
+ else:
255
+ cfg.extra_fields.pop("active_profile", None)
256
+
257
+
258
+ def set_active_profile(cfg: AppConfig, name: str) -> None:
259
+ profile_name = _normalize_profile_name(name)
260
+ profile = get_profile(cfg, profile_name)
261
+ if profile is None:
262
+ _raise_config_error(f"Profile not found: {profile_name}")
263
+ previous_profile = str((cfg.extra_fields or {}).get("active_profile") or "").strip()
264
+ if previous_profile and previous_profile != profile.name:
265
+ # Legacy-config hygiene: "router" is a deprecated role_models key from
266
+ # the removed semantic router. It stored a provider-specific model id,
267
+ # so never carry it into a different API profile; drop it on switch.
268
+ for section in ("role_models", "forge_role_models"):
269
+ raw_models = cfg.extra_fields.get(section)
270
+ if not isinstance(raw_models, dict) or "router" not in raw_models:
271
+ continue
272
+ next_models = dict(raw_models)
273
+ next_models.pop("router", None)
274
+ if next_models:
275
+ cfg.extra_fields[section] = next_models
276
+ else:
277
+ cfg.extra_fields.pop(section, None)
278
+ cfg.extra_fields["active_profile"] = profile.name
279
+ if profile.base_url:
280
+ cfg.base_url = profile.base_url
281
+ if profile.default_model or profile.auth_provider:
282
+ cfg.model = profile.default_model
283
+ if profile.reasoning_effort is not None or profile.auth_provider:
284
+ effective_effort = (
285
+ None if profile.reasoning_effort in {None, "auto"} else profile.reasoning_effort
286
+ )
287
+ cfg.llm_reasoning_effort = effective_effort
288
+ cfg.llm_enable_thinking = None if effective_effort is None else effective_effort != "none"
289
+
290
+
291
+ def update_active_profile_defaults(
292
+ cfg: AppConfig,
293
+ *,
294
+ base_url: str | None = None,
295
+ default_model: str | None = None,
296
+ reasoning_effort: str | None | object = _UNSET,
297
+ allow_subscription_selection: bool = False,
298
+ ) -> bool:
299
+ if not isinstance((cfg.extra_fields or {}).get("profiles"), dict):
300
+ return False
301
+ active = str((cfg.extra_fields or {}).get("active_profile") or "").strip()
302
+ if not active:
303
+ return False
304
+ current = get_profile(cfg, active)
305
+ if current is None:
306
+ _raise_config_error(f"Active profile not found: {active}")
307
+ if current.auth_provider:
308
+ if base_url is not None and not _same_base_url(base_url, current.base_url):
309
+ _raise_config_error(
310
+ "Subscription endpoints are owned by their provider adapter; reconnect "
311
+ "through setup instead of overriding base_url."
312
+ )
313
+ selection_changes_requested = default_model is not None or reasoning_effort is not _UNSET
314
+ if selection_changes_requested and not allow_subscription_selection:
315
+ _raise_config_error(
316
+ "Subscription model and reasoning effort are managed in /config → Default Model."
317
+ )
318
+
319
+ next_base_url = current.base_url
320
+ if base_url is not None:
321
+ next_base_url = validate_base_url(base_url, key="base_url", allow_empty=True)
322
+
323
+ next_default_model = current.default_model
324
+ if default_model is not None:
325
+ next_default_model = str(default_model or "").strip()
326
+ next_reasoning_effort = current.reasoning_effort
327
+ if reasoning_effort is not _UNSET:
328
+ next_reasoning_effort = str(reasoning_effort or "auto").strip().lower() or "auto"
329
+
330
+ updated = ProfileSpec(
331
+ name=current.name,
332
+ protocol=current.protocol,
333
+ base_url=next_base_url,
334
+ api_key_env=current.api_key_env,
335
+ auth_provider=current.auth_provider,
336
+ extra_headers=dict(current.extra_headers),
337
+ default_model=next_default_model,
338
+ reasoning_effort=next_reasoning_effort,
339
+ reasoning_trace_adapter=current.reasoning_trace_adapter,
340
+ web_search_adapter=current.web_search_adapter,
341
+ web_search_model=current.web_search_model,
342
+ notes=current.notes,
343
+ cache_capability=current.cache_capability,
344
+ )
345
+ if updated.to_dict() == current.to_dict():
346
+ return False
347
+ add_profile(
348
+ cfg,
349
+ updated,
350
+ allow_auth_profile_update=bool(current.auth_provider),
351
+ )
352
+ if base_url is not None:
353
+ cfg.base_url = updated.base_url
354
+ if default_model is not None:
355
+ cfg.model = updated.default_model
356
+ if reasoning_effort is not _UNSET:
357
+ effective_effort = None if updated.reasoning_effort == "auto" else updated.reasoning_effort
358
+ cfg.llm_reasoning_effort = effective_effort
359
+ cfg.llm_enable_thinking = None if effective_effort is None else effective_effort != "none"
360
+ return True
361
+
362
+
363
+ def update_profile(
364
+ cfg: AppConfig,
365
+ name: str,
366
+ *,
367
+ allow_auth_profile_update: bool = False,
368
+ allow_subscription_selection: bool = False,
369
+ **fields: Any,
370
+ ) -> None:
371
+ profile_name = _normalize_profile_name(name)
372
+ current = get_profile(cfg, profile_name)
373
+ if current is None:
374
+ _raise_config_error(f"Profile not found: {profile_name}")
375
+ if current.auth_provider:
376
+ connection_fields = {
377
+ "protocol",
378
+ "base_url",
379
+ "api_key_env",
380
+ "auth_provider",
381
+ "extra_headers",
382
+ "reasoning_trace_adapter",
383
+ }
384
+ if connection_fields.intersection(fields) and not allow_auth_profile_update:
385
+ _raise_config_error(
386
+ "Subscription connection fields are owned by their provider adapter."
387
+ )
388
+ if {"default_model", "reasoning_effort"}.intersection(fields) and not (
389
+ allow_subscription_selection or allow_auth_profile_update
390
+ ):
391
+ _raise_config_error(
392
+ "Subscription model and reasoning effort are managed in /config → Default Model."
393
+ )
394
+ values = {
395
+ "name": current.name,
396
+ "protocol": current.protocol,
397
+ "base_url": current.base_url,
398
+ "api_key_env": current.api_key_env,
399
+ "auth_provider": current.auth_provider,
400
+ "extra_headers": dict(current.extra_headers),
401
+ "default_model": current.default_model,
402
+ "reasoning_effort": current.reasoning_effort,
403
+ "reasoning_trace_adapter": current.reasoning_trace_adapter,
404
+ "web_search_adapter": current.web_search_adapter,
405
+ "web_search_model": current.web_search_model,
406
+ "notes": current.notes,
407
+ "cache_capability": current.cache_capability,
408
+ }
409
+ values.update(fields)
410
+ updated = ProfileSpec(**values)
411
+ add_profile(
412
+ cfg,
413
+ updated,
414
+ allow_auth_profile_update=bool(current.auth_provider),
415
+ )
416
+ if str((cfg.extra_fields or {}).get("active_profile") or "") == profile_name:
417
+ set_active_profile(cfg, profile_name)
418
+
419
+
420
+ def rename_profile(cfg: AppConfig, old: str, new: str) -> None:
421
+ old_name = _normalize_profile_name(old)
422
+ new_name = _normalize_profile_name(new)
423
+ if old_name == new_name:
424
+ return
425
+ current = get_profile(cfg, old_name)
426
+ if current is None:
427
+ _raise_config_error(f"Profile not found: {old_name}")
428
+ if get_profile(cfg, new_name) is not None:
429
+ _raise_config_error(f"Profile already exists: {new_name}")
430
+ profiles = dict(_profile_dict(cfg))
431
+ profiles.pop(old_name, None)
432
+ profiles[new_name] = ProfileSpec(
433
+ name=new_name,
434
+ protocol=current.protocol,
435
+ base_url=current.base_url,
436
+ api_key_env=current.api_key_env,
437
+ auth_provider=current.auth_provider,
438
+ extra_headers=dict(current.extra_headers),
439
+ default_model=current.default_model,
440
+ reasoning_effort=current.reasoning_effort,
441
+ reasoning_trace_adapter=current.reasoning_trace_adapter,
442
+ web_search_adapter=current.web_search_adapter,
443
+ web_search_model=current.web_search_model,
444
+ notes=current.notes,
445
+ cache_capability=current.cache_capability,
446
+ ).to_dict()
447
+ _set_profile_dict(cfg, profiles)
448
+ if str((cfg.extra_fields or {}).get("active_profile") or "") == old_name:
449
+ set_active_profile(cfg, new_name)
450
+
451
+
452
+ def _rename_cloud_profile(extra_fields: dict[str, Any]) -> bool:
453
+ """Rename the hosted Pro profile from its pre-rebrand key.
454
+
455
+ Pre-rebrand installs stored the hosted provider as a profile literally named
456
+ "sylliptor", including in ``active_profile``. Left alone, a Pro user would
457
+ open the CLI to find their subscription profile missing.
458
+ """
459
+ from .config import CLOUD_PROFILE_KEY, LEGACY_CLOUD_PROFILE_KEY
460
+
461
+ profiles = extra_fields.get("profiles")
462
+ if not isinstance(profiles, dict) or LEGACY_CLOUD_PROFILE_KEY not in profiles:
463
+ return False
464
+ if CLOUD_PROFILE_KEY in profiles:
465
+ # Both present: the current one is authoritative, drop the stale entry.
466
+ profiles.pop(LEGACY_CLOUD_PROFILE_KEY, None)
467
+ else:
468
+ entry = dict(profiles.pop(LEGACY_CLOUD_PROFILE_KEY))
469
+ entry["name"] = CLOUD_PROFILE_KEY
470
+ profiles[CLOUD_PROFILE_KEY] = entry
471
+
472
+ if str(extra_fields.get("active_profile") or "") == LEGACY_CLOUD_PROFILE_KEY:
473
+ extra_fields["active_profile"] = CLOUD_PROFILE_KEY
474
+ extra_fields["profiles"] = profiles
475
+ return True
476
+
477
+
478
+ def migrate_legacy_to_profiles(cfg: AppConfig) -> bool:
479
+ extra_fields = dict(getattr(cfg, "extra_fields", {}) or {})
480
+ if isinstance(extra_fields.get("profiles"), dict):
481
+ renamed = _rename_cloud_profile(extra_fields)
482
+ cfg.extra_fields = extra_fields
483
+ return renamed
484
+
485
+ base_url = str(getattr(cfg, "base_url", "") or "").strip()
486
+ if not base_url or _same_base_url(base_url, DEFAULT_OPENAI_BASE_URL):
487
+ base_url = DEFAULT_OPENAI_BASE_URL
488
+ api_key_env = None if _legacy_api_key_present() else "OPENAI_API_KEY"
489
+ else:
490
+ api_key_env = None
491
+ profile = ProfileSpec(
492
+ name="default",
493
+ protocol=OPENAI_COMPAT_PROTOCOL,
494
+ base_url=base_url,
495
+ api_key_env=api_key_env,
496
+ default_model=str(getattr(cfg, "model", "") or "").strip(),
497
+ reasoning_effort=(
498
+ str(getattr(cfg, "llm_reasoning_effort", "") or "").strip().lower() or None
499
+ ),
500
+ notes="Migrated from legacy base_url/model config.",
501
+ )
502
+ extra_fields["profiles"] = {"default": profile.to_dict()}
503
+ extra_fields["active_profile"] = "default"
504
+ cfg.extra_fields = extra_fields
505
+ return True
506
+
507
+
508
+ def _profile_dict(cfg: AppConfig) -> dict[str, dict[str, Any]]:
509
+ raw = (cfg.extra_fields or {}).get("profiles")
510
+ if not isinstance(raw, dict):
511
+ return {}
512
+ profiles: dict[str, dict[str, Any]] = {}
513
+ for name, data in raw.items():
514
+ profile_name = _normalize_profile_name(str(name))
515
+ if isinstance(data, dict):
516
+ profiles[profile_name] = dict(data)
517
+ return profiles
518
+
519
+
520
+ def _set_profile_dict(cfg: AppConfig, profiles: dict[str, dict[str, Any]]) -> None:
521
+ extra_fields = dict(cfg.extra_fields or {})
522
+ extra_fields["profiles"] = dict(sorted(profiles.items()))
523
+ cfg.extra_fields = extra_fields
524
+
525
+
526
+ def _normalize_profile_name(name: str) -> str:
527
+ normalized = str(name or "").strip().lower()
528
+ _validate_profile_name(normalized)
529
+ return normalized
530
+
531
+
532
+ def _validate_profile_name(name: str) -> None:
533
+ normalized = str(name or "").strip()
534
+ if not normalized:
535
+ _raise_config_error("Profile name must be non-empty.")
536
+ if normalized != normalized.lower() or not _PROFILE_NAME_RE.fullmatch(normalized):
537
+ _raise_config_error(
538
+ "Profile name must use lowercase letters, digits, hyphens, or underscores."
539
+ )
540
+
541
+
542
+ def _validate_protocol(protocol: str) -> None:
543
+ if str(protocol or "").strip() not in SUPPORTED_PROTOCOLS:
544
+ allowed = ", ".join(sorted(SUPPORTED_PROTOCOLS))
545
+ _raise_config_error(f"Profile protocol must be one of: {allowed}")
546
+
547
+
548
+ def _validate_web_search_adapter(adapter: str) -> None:
549
+ try:
550
+ normalize_web_search_adapter(adapter)
551
+ except ValueError as exc:
552
+ _raise_config_error(str(exc))
553
+
554
+
555
+ def _validate_base_url(base_url: str) -> None:
556
+ validate_base_url(base_url, key="Profile base_url")
557
+
558
+
559
+ def validate_base_url(
560
+ base_url: str,
561
+ *,
562
+ key: str = "base_url",
563
+ allow_empty: bool = False,
564
+ ) -> str:
565
+ normalized = str(base_url or "").strip()
566
+ if not normalized:
567
+ if allow_empty:
568
+ return ""
569
+ _raise_config_error(f"{key} must be non-empty.")
570
+ if any(ch.isspace() for ch in normalized):
571
+ _raise_config_error(f"{key} must not contain whitespace.")
572
+ try:
573
+ parsed = urlsplit(normalized)
574
+ except ValueError:
575
+ _raise_config_error(f"{key} must be a valid http:// or https:// URL.")
576
+ if parsed.scheme.lower() not in {"http", "https"} or not parsed.netloc:
577
+ _raise_config_error(f"{key} must be a valid http:// or https:// URL.")
578
+ if parsed.query or parsed.fragment:
579
+ _raise_config_error(f"{key} must not include query strings or fragments.")
580
+ return normalized
581
+
582
+
583
+ def resolve_effective_base_url(*, cfg: AppConfig, profile: ProfileSpec | None = None) -> str:
584
+ resolved_profile = profile or get_active_profile(cfg)
585
+ profile_base_url = validate_base_url(
586
+ resolved_profile.base_url,
587
+ key=f"Profile {resolved_profile.name} base_url",
588
+ allow_empty=True,
589
+ ).rstrip("/")
590
+ if profile_base_url:
591
+ return profile_base_url
592
+ cfg_base_url = validate_base_url(
593
+ str(getattr(cfg, "base_url", "") or ""),
594
+ key="base_url",
595
+ allow_empty=True,
596
+ ).rstrip("/")
597
+ default_base_url = DEFAULT_OPENAI_BASE_URL.rstrip("/")
598
+ return cfg_base_url or default_base_url
599
+
600
+
601
+ def connection_fingerprint(cfg: AppConfig) -> tuple[str, ...]:
602
+ """Return connection fields that require rebuilding an LLM client.
603
+
604
+ API-key profiles can safely apply model/reasoning changes to their existing
605
+ client in place. A trace-adapter change always requires rebuilding the
606
+ concrete client capability, while a subscription model and reasoning effort
607
+ are a provider-managed pair that also require a rebuild.
608
+ """
609
+
610
+ profile = get_active_profile(cfg)
611
+ execution = getattr(cfg, "execution", None)
612
+ subscription_model = profile.default_model if profile.auth_provider else ""
613
+ subscription_reasoning = profile.reasoning_effort if profile.auth_provider else ""
614
+ return (
615
+ str(getattr(execution, "backend", "native") or "native").strip(),
616
+ str(getattr(execution, "runtime", None) or "").strip(),
617
+ profile.name,
618
+ str(profile.protocol or OPENAI_COMPAT_PROTOCOL).strip(),
619
+ resolve_effective_base_url(cfg=cfg, profile=profile),
620
+ str(profile.auth_provider or "").strip(),
621
+ str(subscription_model or "").strip(),
622
+ str(subscription_reasoning or "").strip(),
623
+ str(profile.reasoning_trace_adapter or "auto").strip(),
624
+ )
625
+
626
+
627
+ def _coerce_cache_capability(raw: Any, *, source: str) -> CacheCapabilitySpec | None:
628
+ try:
629
+ return CacheCapabilitySpec.from_mapping(raw, source=source)
630
+ except ValueError as exc:
631
+ _raise_config_error(str(exc))
632
+
633
+
634
+ def _coerce_headers(raw: Any) -> dict[str, str]:
635
+ if raw is None:
636
+ return {}
637
+ if not isinstance(raw, dict):
638
+ _raise_config_error("Profile extra_headers must be a JSON object.")
639
+ try:
640
+ return canonicalize_extra_headers(raw)
641
+ except ValueError as exc:
642
+ _raise_config_error(str(exc))
643
+
644
+
645
+ def _optional_string(raw: Any) -> str | None:
646
+ value = str(raw or "").strip()
647
+ return value or None
648
+
649
+
650
+ def _same_base_url(left: str, right: str) -> bool:
651
+ return str(left or "").strip().rstrip("/") == str(right or "").strip().rstrip("/")
652
+
653
+
654
+ def _legacy_api_key_present() -> bool:
655
+ try:
656
+ from .config import load_persisted_api_key
657
+
658
+ return bool(load_persisted_api_key())
659
+ except Exception:
660
+ return False
661
+
662
+
663
+ def _raise_config_error(message: str) -> None:
664
+ from .config import ConfigError
665
+
666
+ raise ConfigError(message)
@@ -0,0 +1,29 @@
1
+ from .base import (
2
+ ProviderAccountStatus,
3
+ ProviderAuthAdapter,
4
+ ProviderAuthError,
5
+ ProviderAuthSetupOption,
6
+ ProviderLoginRequiredError,
7
+ ProviderModel,
8
+ ProviderReasoningEffort,
9
+ )
10
+ from .registry import (
11
+ create_provider_auth,
12
+ provider_auth_ids,
13
+ provider_auth_setup_options,
14
+ register_provider_auth,
15
+ )
16
+
17
+ __all__ = [
18
+ "ProviderAccountStatus",
19
+ "ProviderAuthAdapter",
20
+ "ProviderAuthError",
21
+ "ProviderAuthSetupOption",
22
+ "ProviderLoginRequiredError",
23
+ "ProviderModel",
24
+ "ProviderReasoningEffort",
25
+ "create_provider_auth",
26
+ "provider_auth_ids",
27
+ "provider_auth_setup_options",
28
+ "register_provider_auth",
29
+ ]