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,543 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ from collections.abc import Callable, Collection
6
+ from typing import Any, TypeAlias
7
+
8
+ HOST_ACTION_PROTOCOL_VERSION = "1"
9
+ HOST_ACTIONS: tuple[str, ...] = (
10
+ "tasks.list",
11
+ "tasks.run",
12
+ "tasks.status",
13
+ "tasks.terminate",
14
+ "debug.list",
15
+ "debug.start",
16
+ "debug.stop",
17
+ "debug.status",
18
+ )
19
+ HOST_ACTION_SET = frozenset(HOST_ACTIONS)
20
+ HOST_ACTION_TOOL_NAMES: dict[str, str] = {
21
+ "tasks.list": "ide_task_list",
22
+ "tasks.run": "ide_task_run",
23
+ "tasks.status": "ide_task_status",
24
+ "tasks.terminate": "ide_task_terminate",
25
+ "debug.list": "ide_debug_list",
26
+ "debug.start": "ide_debug_start",
27
+ "debug.stop": "ide_debug_stop",
28
+ "debug.status": "ide_debug_status",
29
+ }
30
+ HOST_ACTION_MAX_ARGUMENT_BYTES = 8 * 1024
31
+ HOST_ACTION_MAX_RESULT_BYTES = 64 * 1024
32
+ HOST_ACTION_MAX_LIST_ITEMS = 100
33
+ HOST_ACTION_MAX_IDENTIFIER_CHARS = 256
34
+ HOST_ACTION_MAX_LABEL_CHARS = 512
35
+ HOST_ACTION_MAX_ERROR_MESSAGE_CHARS = 512
36
+ HOST_ACTION_ERROR_CODE_PATTERN = re.compile(r"^[a-z][a-z0-9_.-]{0,63}$")
37
+
38
+
39
+ class HostActionError(RuntimeError):
40
+ def __init__(
41
+ self,
42
+ code: str,
43
+ message: str,
44
+ *,
45
+ retryable: bool = False,
46
+ host_error_code: str | None = None,
47
+ ) -> None:
48
+ self.code = str(code or "host_action_failed")
49
+ self.message = str(message or "The IDE host action failed.")
50
+ self.retryable = bool(retryable)
51
+ self.host_error_code = str(host_error_code or "") or None
52
+ super().__init__(self.message)
53
+
54
+ def to_result_payload(self) -> dict[str, Any]:
55
+ payload: dict[str, Any] = {
56
+ "error": self.message,
57
+ "error_code": self.code,
58
+ "retryable": self.retryable,
59
+ }
60
+ if self.host_error_code:
61
+ payload["host_error_code"] = self.host_error_code
62
+ return payload
63
+
64
+
65
+ HostActionHandler: TypeAlias = Callable[[str, dict[str, Any]], dict[str, Any]]
66
+
67
+
68
+ def normalized_host_action_capabilities(actions: Collection[str] | None) -> frozenset[str]:
69
+ if actions is None:
70
+ return frozenset()
71
+ return frozenset(
72
+ action.strip()
73
+ for action in actions
74
+ if isinstance(action, str) and action.strip() in HOST_ACTION_SET
75
+ )
76
+
77
+
78
+ def normalize_host_action_arguments(action: str, arguments: dict[str, Any]) -> dict[str, Any]:
79
+ normalized_action = str(action or "").strip()
80
+ if normalized_action not in HOST_ACTION_SET:
81
+ raise HostActionError(
82
+ "unsupported_host_action",
83
+ "The requested IDE host action is not supported.",
84
+ )
85
+ if not isinstance(arguments, dict):
86
+ raise HostActionError(
87
+ "invalid_host_action_arguments",
88
+ "IDE host action arguments must be an object.",
89
+ )
90
+
91
+ if normalized_action == "tasks.list":
92
+ _reject_extra_keys(arguments, {"task_type"})
93
+ task_type = _optional_identifier(arguments.get("task_type"), "task_type")
94
+ normalized = {"task_type": task_type} if task_type else {}
95
+ elif normalized_action == "tasks.run":
96
+ _reject_extra_keys(arguments, {"task_id"})
97
+ normalized = {"task_id": _required_identifier(arguments.get("task_id"), "task_id")}
98
+ elif normalized_action == "tasks.status":
99
+ _reject_extra_keys(arguments, {"execution_id"})
100
+ execution_id = _optional_identifier(arguments.get("execution_id"), "execution_id")
101
+ normalized = {"execution_id": execution_id} if execution_id else {}
102
+ elif normalized_action == "tasks.terminate":
103
+ _reject_extra_keys(arguments, {"execution_id"})
104
+ normalized = {
105
+ "execution_id": _required_identifier(arguments.get("execution_id"), "execution_id")
106
+ }
107
+ elif normalized_action == "debug.list":
108
+ _reject_extra_keys(arguments, set())
109
+ normalized = {}
110
+ elif normalized_action == "debug.start":
111
+ _reject_extra_keys(arguments, {"configuration_id"})
112
+ normalized = {
113
+ "configuration_id": _required_identifier(
114
+ arguments.get("configuration_id"), "configuration_id"
115
+ )
116
+ }
117
+ elif normalized_action == "debug.stop":
118
+ _reject_extra_keys(arguments, {"debug_session_id"})
119
+ normalized = {
120
+ "debug_session_id": _required_identifier(
121
+ arguments.get("debug_session_id"), "debug_session_id"
122
+ )
123
+ }
124
+ else:
125
+ _reject_extra_keys(arguments, {"debug_session_id"})
126
+ debug_session_id = _optional_identifier(
127
+ arguments.get("debug_session_id"), "debug_session_id"
128
+ )
129
+ normalized = {"debug_session_id": debug_session_id} if debug_session_id else {}
130
+
131
+ if json_size_bytes(normalized) > HOST_ACTION_MAX_ARGUMENT_BYTES:
132
+ raise HostActionError(
133
+ "host_action_arguments_too_large",
134
+ "IDE host action arguments exceed the allowed size.",
135
+ )
136
+ return normalized
137
+
138
+
139
+ def normalize_host_action_result(action: str, result: dict[str, Any]) -> dict[str, Any]:
140
+ normalized_action = str(action or "").strip()
141
+ if normalized_action not in HOST_ACTION_SET:
142
+ raise HostActionError(
143
+ "unsupported_host_action",
144
+ "The requested IDE host action is not supported.",
145
+ )
146
+ if not isinstance(result, dict):
147
+ raise HostActionError(
148
+ "invalid_host_action_result",
149
+ "IDE host action result must be an object.",
150
+ )
151
+ if json_size_bytes(result) > HOST_ACTION_MAX_RESULT_BYTES:
152
+ raise HostActionError(
153
+ "host_action_result_too_large",
154
+ "IDE host action result exceeds the allowed size.",
155
+ )
156
+
157
+ if normalized_action == "tasks.list":
158
+ normalized = _normalize_task_list_result(result)
159
+ elif normalized_action == "tasks.run":
160
+ _reject_extra_keys(
161
+ result,
162
+ {"execution_id", "task_id", "state", "exit_code", "diagnostics_delta"},
163
+ )
164
+ normalized = {
165
+ "execution_id": _required_identifier(result.get("execution_id"), "execution_id"),
166
+ "task_id": _required_identifier(result.get("task_id"), "task_id"),
167
+ "state": _enum_value(result.get("state"), "state", {"started", "completed", "failed"}),
168
+ }
169
+ if result.get("exit_code") is not None:
170
+ normalized["exit_code"] = _bounded_int(result.get("exit_code"), "exit_code")
171
+ if result.get("diagnostics_delta") is not None:
172
+ normalized["diagnostics_delta"] = _normalize_diagnostics_delta(
173
+ result.get("diagnostics_delta")
174
+ )
175
+ elif normalized_action == "tasks.status":
176
+ normalized = _normalize_task_status_result(result)
177
+ elif normalized_action == "tasks.terminate":
178
+ _reject_extra_keys(result, {"execution_id", "terminated", "state"})
179
+ normalized = {
180
+ "execution_id": _required_identifier(result.get("execution_id"), "execution_id"),
181
+ "terminated": _required_bool(result.get("terminated"), "terminated"),
182
+ "state": _enum_value(
183
+ result.get("state"), "state", {"terminated", "not_found", "already_ended"}
184
+ ),
185
+ }
186
+ elif normalized_action == "debug.list":
187
+ normalized = _normalize_debug_list_result(result)
188
+ elif normalized_action == "debug.start":
189
+ _reject_extra_keys(result, {"debug_session_id", "configuration_id", "state"})
190
+ normalized = {
191
+ "debug_session_id": _required_identifier(
192
+ result.get("debug_session_id"), "debug_session_id"
193
+ ),
194
+ "configuration_id": _required_identifier(
195
+ result.get("configuration_id"), "configuration_id"
196
+ ),
197
+ "state": _enum_value(result.get("state"), "state", {"started", "failed"}),
198
+ }
199
+ elif normalized_action == "debug.stop":
200
+ _reject_extra_keys(result, {"debug_session_id", "stopped", "state"})
201
+ normalized = {
202
+ "debug_session_id": _required_identifier(
203
+ result.get("debug_session_id"), "debug_session_id"
204
+ ),
205
+ "stopped": _required_bool(result.get("stopped"), "stopped"),
206
+ "state": _enum_value(
207
+ result.get("state"), "state", {"stopped", "not_found", "already_ended"}
208
+ ),
209
+ }
210
+ else:
211
+ normalized = _normalize_debug_status_result(result)
212
+
213
+ if json_size_bytes(normalized) > HOST_ACTION_MAX_RESULT_BYTES:
214
+ raise HostActionError(
215
+ "host_action_result_too_large",
216
+ "IDE host action result exceeds the allowed size.",
217
+ )
218
+ return normalized
219
+
220
+
221
+ def json_size_bytes(value: Any) -> int:
222
+ try:
223
+ encoded = json.dumps(
224
+ value,
225
+ ensure_ascii=False,
226
+ allow_nan=False,
227
+ separators=(",", ":"),
228
+ sort_keys=True,
229
+ ).encode("utf-8")
230
+ except (TypeError, ValueError) as exc:
231
+ raise HostActionError(
232
+ "invalid_host_action_payload",
233
+ "IDE host action payload must contain finite JSON values.",
234
+ ) from exc
235
+ return len(encoded)
236
+
237
+
238
+ def normalize_host_error(error: dict[str, Any]) -> tuple[str, str, bool]:
239
+ if not isinstance(error, dict):
240
+ raise HostActionError(
241
+ "invalid_host_action_error",
242
+ "IDE host action error must be an object.",
243
+ )
244
+ _reject_extra_keys(error, {"code", "message", "retryable"})
245
+ raw_code = error.get("code")
246
+ code = raw_code.strip() if isinstance(raw_code, str) else ""
247
+ if not HOST_ACTION_ERROR_CODE_PATTERN.fullmatch(code):
248
+ raise HostActionError(
249
+ "invalid_host_action_error",
250
+ "IDE host action error code is invalid.",
251
+ )
252
+ raw_message = error.get("message")
253
+ message = raw_message.strip() if isinstance(raw_message, str) else ""
254
+ if (
255
+ not message
256
+ or len(message) > HOST_ACTION_MAX_ERROR_MESSAGE_CHARS
257
+ or _has_disallowed_text_control(message)
258
+ ):
259
+ raise HostActionError(
260
+ "invalid_host_action_error",
261
+ "IDE host action error message is invalid.",
262
+ )
263
+ retryable = error.get("retryable", False)
264
+ if not isinstance(retryable, bool):
265
+ raise HostActionError(
266
+ "invalid_host_action_error",
267
+ "IDE host action retryable must be a boolean.",
268
+ )
269
+ return code, message, retryable
270
+
271
+
272
+ def _normalize_task_list_result(result: dict[str, Any]) -> dict[str, Any]:
273
+ _reject_extra_keys(result, {"tasks", "truncated"})
274
+ items = result.get("tasks")
275
+ if not isinstance(items, list):
276
+ raise HostActionError("invalid_host_action_result", "tasks must be an array.")
277
+ normalized_items: list[dict[str, Any]] = []
278
+ for raw in items[:HOST_ACTION_MAX_LIST_ITEMS]:
279
+ if not isinstance(raw, dict):
280
+ raise HostActionError("invalid_host_action_result", "Each task must be an object.")
281
+ _reject_extra_keys(raw, {"id", "label", "type", "source", "detail"})
282
+ item: dict[str, Any] = {
283
+ "id": _required_identifier(raw.get("id"), "task.id"),
284
+ "label": _required_label(raw.get("label"), "task.label"),
285
+ }
286
+ for key in ("type", "source", "detail"):
287
+ value = _optional_label(raw.get(key), f"task.{key}")
288
+ if value:
289
+ item[key] = value
290
+ normalized_items.append(item)
291
+ return {
292
+ "tasks": normalized_items,
293
+ "truncated": bool(
294
+ _optional_bool(result.get("truncated"), "truncated")
295
+ or len(items) > len(normalized_items)
296
+ ),
297
+ }
298
+
299
+
300
+ def _normalize_task_status_result(result: dict[str, Any]) -> dict[str, Any]:
301
+ _reject_extra_keys(result, {"executions", "truncated"})
302
+ items = result.get("executions")
303
+ if not isinstance(items, list):
304
+ raise HostActionError("invalid_host_action_result", "executions must be an array.")
305
+ normalized_items: list[dict[str, Any]] = []
306
+ for raw in items[:HOST_ACTION_MAX_LIST_ITEMS]:
307
+ if not isinstance(raw, dict):
308
+ raise HostActionError(
309
+ "invalid_host_action_result", "Each task execution must be an object."
310
+ )
311
+ _reject_extra_keys(
312
+ raw,
313
+ {"execution_id", "task_id", "state", "exit_code", "diagnostics_delta"},
314
+ )
315
+ item: dict[str, Any] = {
316
+ "execution_id": _required_identifier(raw.get("execution_id"), "execution_id"),
317
+ "task_id": _required_identifier(raw.get("task_id"), "task_id"),
318
+ "state": _enum_value(
319
+ raw.get("state"),
320
+ "task_execution.state",
321
+ {"running", "completed", "failed", "terminated"},
322
+ ),
323
+ }
324
+ if raw.get("exit_code") is not None:
325
+ item["exit_code"] = _bounded_int(raw.get("exit_code"), "exit_code")
326
+ if raw.get("diagnostics_delta") is not None:
327
+ item["diagnostics_delta"] = _normalize_diagnostics_delta(raw.get("diagnostics_delta"))
328
+ normalized_items.append(item)
329
+ return {
330
+ "executions": normalized_items,
331
+ "truncated": bool(
332
+ _optional_bool(result.get("truncated"), "truncated")
333
+ or len(items) > len(normalized_items)
334
+ ),
335
+ }
336
+
337
+
338
+ def _normalize_debug_list_result(result: dict[str, Any]) -> dict[str, Any]:
339
+ _reject_extra_keys(result, {"configurations", "truncated"})
340
+ items = result.get("configurations")
341
+ if not isinstance(items, list):
342
+ raise HostActionError("invalid_host_action_result", "configurations must be an array.")
343
+ normalized_items: list[dict[str, Any]] = []
344
+ for raw in items[:HOST_ACTION_MAX_LIST_ITEMS]:
345
+ if not isinstance(raw, dict):
346
+ raise HostActionError(
347
+ "invalid_host_action_result", "Each debug configuration must be an object."
348
+ )
349
+ _reject_extra_keys(raw, {"id", "name", "type", "request", "workspace_folder"})
350
+ item: dict[str, Any] = {
351
+ "id": _required_identifier(raw.get("id"), "configuration.id"),
352
+ "name": _required_label(raw.get("name"), "configuration.name"),
353
+ "type": _required_label(raw.get("type"), "configuration.type"),
354
+ }
355
+ request = _optional_label(raw.get("request"), "configuration.request")
356
+ if request:
357
+ item["request"] = request
358
+ workspace_folder = _optional_label(
359
+ raw.get("workspace_folder"), "configuration.workspace_folder"
360
+ )
361
+ if workspace_folder:
362
+ item["workspace_folder"] = workspace_folder
363
+ normalized_items.append(item)
364
+ return {
365
+ "configurations": normalized_items,
366
+ "truncated": bool(
367
+ _optional_bool(result.get("truncated"), "truncated")
368
+ or len(items) > len(normalized_items)
369
+ ),
370
+ }
371
+
372
+
373
+ def _normalize_debug_status_result(result: dict[str, Any]) -> dict[str, Any]:
374
+ _reject_extra_keys(result, {"sessions", "truncated"})
375
+ items = result.get("sessions")
376
+ if not isinstance(items, list):
377
+ raise HostActionError("invalid_host_action_result", "sessions must be an array.")
378
+ normalized_items: list[dict[str, Any]] = []
379
+ for raw in items[:HOST_ACTION_MAX_LIST_ITEMS]:
380
+ if not isinstance(raw, dict):
381
+ raise HostActionError(
382
+ "invalid_host_action_result", "Each debug session must be an object."
383
+ )
384
+ _reject_extra_keys(raw, {"id", "name", "type", "state", "workspace_folder"})
385
+ item: dict[str, Any] = {
386
+ "id": _required_identifier(raw.get("id"), "debug_session.id"),
387
+ "name": _required_label(raw.get("name"), "debug_session.name"),
388
+ "type": _required_label(raw.get("type"), "debug_session.type"),
389
+ "state": _enum_value(raw.get("state"), "debug_session.state", {"running", "stopped"}),
390
+ }
391
+ workspace_folder = _optional_label(
392
+ raw.get("workspace_folder"), "debug_session.workspace_folder"
393
+ )
394
+ if workspace_folder:
395
+ item["workspace_folder"] = workspace_folder
396
+ normalized_items.append(item)
397
+ return {
398
+ "sessions": normalized_items,
399
+ "truncated": bool(
400
+ _optional_bool(result.get("truncated"), "truncated")
401
+ or len(items) > len(normalized_items)
402
+ ),
403
+ }
404
+
405
+
406
+ def _normalize_diagnostics_delta(value: Any) -> dict[str, Any]:
407
+ if not isinstance(value, dict):
408
+ raise HostActionError("invalid_host_action_result", "diagnostics_delta must be an object.")
409
+ _reject_extra_keys(value, {"added", "removed", "changed", "total", "truncated", "items"})
410
+ items = value.get("items", [])
411
+ if not isinstance(items, list):
412
+ raise HostActionError(
413
+ "invalid_host_action_result", "diagnostics_delta.items must be an array."
414
+ )
415
+ normalized_items: list[dict[str, Any]] = []
416
+ for raw in items[:50]:
417
+ if not isinstance(raw, dict):
418
+ raise HostActionError(
419
+ "invalid_host_action_result", "Each diagnostic delta item must be an object."
420
+ )
421
+ _reject_extra_keys(raw, {"uri", "line", "severity", "message", "source"})
422
+ raw_uri = raw.get("uri")
423
+ uri = raw_uri.strip() if isinstance(raw_uri, str) else ""
424
+ if not uri or len(uri) > 2048 or any(ord(char) < 32 or ord(char) == 127 for char in uri):
425
+ raise HostActionError(
426
+ "invalid_host_action_result", "diagnostic uri must be bounded non-empty text."
427
+ )
428
+ line = raw.get("line")
429
+ if isinstance(line, bool) or not isinstance(line, int) or not 1 <= line < 2**31:
430
+ raise HostActionError(
431
+ "invalid_host_action_result", "diagnostic line must be a positive integer."
432
+ )
433
+ item: dict[str, Any] = {
434
+ "uri": uri,
435
+ "line": line,
436
+ "severity": _enum_value(
437
+ raw.get("severity"),
438
+ "diagnostic.severity",
439
+ {"error", "warning", "information", "hint"},
440
+ ),
441
+ "message": _required_label(raw.get("message"), "diagnostic.message"),
442
+ }
443
+ source = _optional_label(raw.get("source"), "diagnostic.source")
444
+ if source:
445
+ item["source"] = source
446
+ normalized_items.append(item)
447
+ return {
448
+ "added": _nonnegative_int(value.get("added"), "diagnostics_delta.added"),
449
+ "removed": _nonnegative_int(value.get("removed"), "diagnostics_delta.removed"),
450
+ "changed": _nonnegative_int(value.get("changed"), "diagnostics_delta.changed"),
451
+ "total": _nonnegative_int(value.get("total"), "diagnostics_delta.total"),
452
+ "truncated": bool(
453
+ _optional_bool(value.get("truncated"), "diagnostics_delta.truncated")
454
+ or len(items) > len(normalized_items)
455
+ ),
456
+ "items": normalized_items,
457
+ }
458
+
459
+
460
+ def _reject_extra_keys(value: dict[str, Any], allowed: set[str]) -> None:
461
+ if extra := sorted(set(value) - allowed):
462
+ preview = ", ".join(str(key)[:64] for key in extra[:5])
463
+ if len(extra) > 5:
464
+ preview += ", ..."
465
+ raise HostActionError(
466
+ "invalid_host_action_payload",
467
+ "IDE host action payload contains unsupported field(s): " + preview,
468
+ )
469
+
470
+
471
+ def _required_identifier(value: Any, field: str) -> str:
472
+ text = value.strip() if isinstance(value, str) else ""
473
+ if (
474
+ not text
475
+ or len(text) > HOST_ACTION_MAX_IDENTIFIER_CHARS
476
+ or any(ord(char) < 32 or ord(char) == 127 for char in text)
477
+ ):
478
+ raise HostActionError(
479
+ "invalid_host_action_payload", f"{field} must be a bounded non-empty identifier."
480
+ )
481
+ return text
482
+
483
+
484
+ def _optional_identifier(value: Any, field: str) -> str | None:
485
+ if value is None:
486
+ return None
487
+ return _required_identifier(value, field)
488
+
489
+
490
+ def _required_label(value: Any, field: str) -> str:
491
+ text = value.strip() if isinstance(value, str) else ""
492
+ if not text or len(text) > HOST_ACTION_MAX_LABEL_CHARS or _has_disallowed_text_control(text):
493
+ raise HostActionError(
494
+ "invalid_host_action_payload", f"{field} must be bounded non-empty text."
495
+ )
496
+ return text
497
+
498
+
499
+ def _optional_label(value: Any, field: str) -> str | None:
500
+ if value is None:
501
+ return None
502
+ return _required_label(value, field)
503
+
504
+
505
+ def _enum_value(value: Any, field: str, allowed: set[str]) -> str:
506
+ text = value.strip() if isinstance(value, str) else ""
507
+ if text not in allowed:
508
+ raise HostActionError(
509
+ "invalid_host_action_payload",
510
+ f"{field} must be one of: " + ", ".join(sorted(allowed)),
511
+ )
512
+ return text
513
+
514
+
515
+ def _has_disallowed_text_control(value: str) -> bool:
516
+ return any(
517
+ (ord(char) < 32 and char not in {"\t", "\n", "\r"}) or ord(char) == 127 for char in value
518
+ )
519
+
520
+
521
+ def _required_bool(value: Any, field: str) -> bool:
522
+ if not isinstance(value, bool):
523
+ raise HostActionError("invalid_host_action_payload", f"{field} must be a boolean.")
524
+ return value
525
+
526
+
527
+ def _optional_bool(value: Any, field: str) -> bool:
528
+ if value is None:
529
+ return False
530
+ return _required_bool(value, field)
531
+
532
+
533
+ def _bounded_int(value: Any, field: str) -> int:
534
+ if isinstance(value, bool) or not isinstance(value, int) or not -(2**31) <= value < 2**31:
535
+ raise HostActionError("invalid_host_action_payload", f"{field} must be a 32-bit integer.")
536
+ return value
537
+
538
+
539
+ def _nonnegative_int(value: Any, field: str) -> int:
540
+ normalized = _bounded_int(value, field)
541
+ if normalized < 0:
542
+ raise HostActionError("invalid_host_action_payload", f"{field} must be non-negative.")
543
+ return normalized
@@ -0,0 +1,103 @@
1
+ """Open URLs in the user's graphical browser across native and WSL hosts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import shutil
7
+ import subprocess
8
+ import webbrowser
9
+ from pathlib import Path
10
+
11
+ _LAUNCH_TIMEOUT_SECONDS = 8.0
12
+
13
+
14
+ def is_wsl() -> bool:
15
+ """Return whether the current Python process is running inside WSL."""
16
+
17
+ if os.name == "nt":
18
+ return False
19
+ if os.environ.get("WSL_DISTRO_NAME") or os.environ.get("WSL_INTEROP"):
20
+ return True
21
+ try:
22
+ return "microsoft" in Path("/proc/version").read_text(encoding="utf-8").lower()
23
+ except OSError:
24
+ return False
25
+
26
+
27
+ def open_url(url: str) -> bool:
28
+ """Open *url* in the host browser, including the Windows browser from WSL."""
29
+
30
+ target = str(url or "").strip()
31
+ if not target:
32
+ return False
33
+ if is_wsl() and _open_url_from_wsl(target):
34
+ return True
35
+ try:
36
+ return bool(webbrowser.open(target))
37
+ except Exception: # noqa: BLE001 - browser launch is best-effort
38
+ return False
39
+
40
+
41
+ def _open_url_from_wsl(url: str) -> bool:
42
+ for command in _wsl_browser_commands(url):
43
+ try:
44
+ result = subprocess.run(
45
+ command,
46
+ check=False,
47
+ stdin=subprocess.DEVNULL,
48
+ stdout=subprocess.DEVNULL,
49
+ stderr=subprocess.DEVNULL,
50
+ timeout=_LAUNCH_TIMEOUT_SECONDS,
51
+ )
52
+ except (OSError, subprocess.SubprocessError):
53
+ continue
54
+ if result.returncode == 0:
55
+ return True
56
+ return False
57
+
58
+
59
+ def _wsl_browser_commands(url: str) -> tuple[tuple[str, ...], ...]:
60
+ commands: list[tuple[str, ...]] = []
61
+ wslview = shutil.which("wslview")
62
+ if wslview:
63
+ commands.append((wslview, url))
64
+
65
+ powershell = _windows_executable(
66
+ "powershell.exe",
67
+ Path("/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe"),
68
+ )
69
+ if powershell:
70
+ commands.append(
71
+ (
72
+ powershell,
73
+ "-NoLogo",
74
+ "-NoProfile",
75
+ "-NonInteractive",
76
+ "-Command",
77
+ "Start-Process -FilePath $args[0]",
78
+ url,
79
+ )
80
+ )
81
+
82
+ protocol_handler = _windows_executable(
83
+ "rundll32.exe",
84
+ Path("/mnt/c/Windows/System32/rundll32.exe"),
85
+ )
86
+ if protocol_handler:
87
+ commands.append((protocol_handler, "url.dll,FileProtocolHandler", url))
88
+ return tuple(commands)
89
+
90
+
91
+ def _windows_executable(name: str, fallback: Path) -> str | None:
92
+ resolved = shutil.which(name)
93
+ if resolved:
94
+ return resolved
95
+ try:
96
+ if fallback.is_file():
97
+ return os.fspath(fallback)
98
+ except OSError:
99
+ pass
100
+ return None
101
+
102
+
103
+ __all__ = ["is_wsl", "open_url"]
@@ -0,0 +1,5 @@
1
+ """Alysis Code IDE protocol bridge."""
2
+
3
+ from .protocol import PROTOCOL_VERSION
4
+
5
+ __all__ = ["PROTOCOL_VERSION"]