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,682 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import platform
5
+ import shutil
6
+ import subprocess
7
+ import tempfile
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+ from typing import Literal
11
+
12
+ from .branding import default_sandbox_docker_image, env_get
13
+ from .config import AppConfig
14
+ from .sandbox_runner import build_shell_runner_from_settings
15
+ from .sandbox_settings import ShellSandboxSettings, resolve_shell_sandbox_settings
16
+
17
+ SandboxCheckStatus = Literal["ok", "missing", "failed", "skipped", "disabled"]
18
+
19
+ _DOCKER_INFO_TIMEOUT_S = 10
20
+ _DOCKER_IMAGE_INSPECT_TIMEOUT_S = 20
21
+ _DOCKER_PULL_TIMEOUT_S = 900
22
+ _SANDBOX_SMOKE_TIMEOUT_S = 15
23
+ _READY_SMOKE_OUTPUT = "alysis-sandbox-ready"
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class SandboxCheck:
28
+ name: str
29
+ status: SandboxCheckStatus
30
+ detail: str
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class SandboxDiagnostic:
35
+ ready: bool
36
+ status: str
37
+ configured_mode: str
38
+ configured_backend: str
39
+ selected_backend: str | None
40
+ docker_image: str
41
+ server_image: str
42
+ checks: tuple[SandboxCheck, ...]
43
+ next_steps: tuple[str, ...]
44
+ can_pull: bool = False
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class SandboxImagePullResult:
49
+ image: str
50
+ ok: bool
51
+ output: str
52
+
53
+
54
+ @dataclass(frozen=True)
55
+ class SandboxPullResult:
56
+ ok: bool
57
+ results: tuple[SandboxImagePullResult, ...]
58
+ error: str | None = None
59
+
60
+
61
+ @dataclass(frozen=True)
62
+ class BubblewrapInstallPlan:
63
+ manager: str
64
+ command: tuple[str, ...]
65
+ display: str
66
+
67
+
68
+ @dataclass(frozen=True)
69
+ class BubblewrapInstallResult:
70
+ ok: bool
71
+ command: str
72
+ detail: str
73
+
74
+
75
+ _BWRAP_INSTALL_MANAGERS: tuple[tuple[str, tuple[str, ...]], ...] = (
76
+ ("apt-get", ("apt-get", "install", "-y", "bubblewrap")),
77
+ ("dnf", ("dnf", "install", "-y", "bubblewrap")),
78
+ ("pacman", ("pacman", "-S", "--noconfirm", "bubblewrap")),
79
+ ("zypper", ("zypper", "--non-interactive", "install", "bubblewrap")),
80
+ ("apk", ("apk", "add", "bubblewrap")),
81
+ )
82
+ _BWRAP_INSTALL_TIMEOUT_S = 300
83
+
84
+
85
+ def default_sandbox_images(*, include_server: bool = True) -> tuple[str, ...]:
86
+ images = [default_sandbox_docker_image("dev")]
87
+ if include_server:
88
+ images.append(resolve_server_sandbox_image())
89
+ return tuple(dict.fromkeys(images))
90
+
91
+
92
+ def configured_sandbox_images(cfg: AppConfig, *, include_server: bool = True) -> tuple[str, ...]:
93
+ settings = resolve_shell_sandbox_settings(cfg)
94
+ images = [settings.docker_image]
95
+ if include_server:
96
+ images.append(resolve_server_sandbox_image())
97
+ return tuple(dict.fromkeys(images))
98
+
99
+
100
+ def resolve_server_sandbox_image() -> str:
101
+ return env_get(
102
+ "ALYSIS_SERVER_DOCKER_IMAGE",
103
+ default=env_get(
104
+ "ALYSIS_SHELL_SANDBOX_DOCKER_IMAGE",
105
+ default=default_sandbox_docker_image("server"),
106
+ ),
107
+ ) or default_sandbox_docker_image("server")
108
+
109
+
110
+ def diagnose_sandbox(
111
+ cfg: AppConfig,
112
+ *,
113
+ include_smoke: bool = False,
114
+ include_server_image: bool = True,
115
+ ) -> SandboxDiagnostic:
116
+ settings = resolve_shell_sandbox_settings(cfg)
117
+ server_image = resolve_server_sandbox_image()
118
+ checks: list[SandboxCheck] = []
119
+
120
+ if settings.mode == "off":
121
+ checks.append(
122
+ SandboxCheck(
123
+ "sandbox mode",
124
+ "disabled",
125
+ "shell_sandbox.mode=off; commands will run on the host shell.",
126
+ )
127
+ )
128
+ return SandboxDiagnostic(
129
+ ready=True,
130
+ status="disabled",
131
+ configured_mode=settings.mode,
132
+ configured_backend=settings.backend,
133
+ selected_backend=None,
134
+ docker_image=settings.docker_image,
135
+ server_image=server_image,
136
+ checks=tuple(checks),
137
+ next_steps=(
138
+ "Sandboxing is disabled by configuration. Re-enable strict mode for safer command execution.",
139
+ ),
140
+ )
141
+
142
+ selected_backend = _select_configured_backend(settings)
143
+ server_worker_backend = _resolve_server_worker_backend()
144
+ needs_server_docker_image = include_server_image and (
145
+ selected_backend == "docker" or server_worker_backend == "docker"
146
+ )
147
+ is_linux = _is_linux()
148
+ bwrap_path = shutil.which("bwrap") if is_linux else None
149
+ docker_path = shutil.which("docker")
150
+
151
+ if bwrap_path:
152
+ checks.append(SandboxCheck("bubblewrap", "ok", bwrap_path))
153
+ elif is_linux:
154
+ checks.append(
155
+ SandboxCheck("bubblewrap", "missing", "bubblewrap (`bwrap`) is not installed.")
156
+ )
157
+ else:
158
+ checks.append(
159
+ SandboxCheck(
160
+ "bubblewrap",
161
+ "skipped",
162
+ "bubblewrap backend is only supported on Linux.",
163
+ )
164
+ )
165
+
166
+ if docker_path:
167
+ checks.append(SandboxCheck("Docker CLI", "ok", docker_path))
168
+ else:
169
+ checks.append(
170
+ SandboxCheck("Docker CLI", "missing", "Docker is not installed or not on PATH.")
171
+ )
172
+
173
+ if selected_backend is None:
174
+ checks.append(
175
+ SandboxCheck(
176
+ "selected backend",
177
+ "failed",
178
+ "No usable sandbox backend was found for shell_sandbox.backend=auto.",
179
+ )
180
+ )
181
+ return _diagnostic(
182
+ settings=settings,
183
+ server_image=server_image,
184
+ selected_backend=None,
185
+ checks=checks,
186
+ next_steps=_install_backend_steps(),
187
+ )
188
+
189
+ checks.append(
190
+ SandboxCheck(
191
+ "selected backend",
192
+ "ok",
193
+ selected_backend,
194
+ )
195
+ )
196
+
197
+ if selected_backend == "bwrap":
198
+ if not bwrap_path:
199
+ next_steps = (
200
+ _bwrap_linux_only_steps()
201
+ if _check_status(checks, "bubblewrap") == "skipped"
202
+ else _bwrap_missing_steps(settings=settings)
203
+ )
204
+ return _diagnostic(
205
+ settings=settings,
206
+ server_image=server_image,
207
+ selected_backend=selected_backend,
208
+ checks=checks,
209
+ next_steps=next_steps,
210
+ )
211
+ if include_smoke:
212
+ _append_smoke_check(checks=checks, settings=settings)
213
+ if needs_server_docker_image:
214
+ docker_info = _run_docker_info()
215
+ checks.append(docker_info)
216
+ if docker_info.status == "ok":
217
+ checks.append(_docker_image_check(server_image, label="server sandbox image"))
218
+ return _diagnostic(
219
+ settings=settings,
220
+ server_image=server_image,
221
+ selected_backend=selected_backend,
222
+ checks=checks,
223
+ next_steps=_next_steps_for_checks(checks, selected_backend=selected_backend),
224
+ )
225
+
226
+ docker_info = _run_docker_info()
227
+ checks.append(docker_info)
228
+ if docker_info.status != "ok":
229
+ return _diagnostic(
230
+ settings=settings,
231
+ server_image=server_image,
232
+ selected_backend=selected_backend,
233
+ checks=checks,
234
+ next_steps=_docker_daemon_steps(docker_path=docker_path),
235
+ )
236
+
237
+ checks.append(_docker_image_check(settings.docker_image, label="sandbox image"))
238
+ if needs_server_docker_image:
239
+ checks.append(_docker_image_check(server_image, label="server sandbox image"))
240
+ if include_smoke and _check_status(checks, "sandbox image") == "ok":
241
+ _append_smoke_check(checks=checks, settings=settings)
242
+
243
+ return _diagnostic(
244
+ settings=settings,
245
+ server_image=server_image,
246
+ selected_backend=selected_backend,
247
+ checks=checks,
248
+ next_steps=_next_steps_for_checks(checks, selected_backend=selected_backend),
249
+ )
250
+
251
+
252
+ def pull_sandbox_images(
253
+ images: tuple[str, ...] | list[str] | None = None,
254
+ *,
255
+ timeout_s: int = _DOCKER_PULL_TIMEOUT_S,
256
+ ) -> SandboxPullResult:
257
+ selected_images = tuple(dict.fromkeys(images or default_sandbox_images()))
258
+ if not selected_images:
259
+ return SandboxPullResult(ok=True, results=())
260
+
261
+ docker_path = shutil.which("docker")
262
+ if not docker_path:
263
+ return SandboxPullResult(
264
+ ok=False,
265
+ results=(),
266
+ error=(
267
+ "Docker is not installed. Install Docker Desktop, open it, then run "
268
+ "`alysis sandbox pull` again."
269
+ ),
270
+ )
271
+
272
+ docker_info = _run_docker_info()
273
+ if docker_info.status != "ok":
274
+ return SandboxPullResult(
275
+ ok=False,
276
+ results=(),
277
+ error=_docker_not_running_message(),
278
+ )
279
+
280
+ results: list[SandboxImagePullResult] = []
281
+ for image in selected_images:
282
+ try:
283
+ proc = subprocess.run(
284
+ ["docker", "pull", image],
285
+ check=False,
286
+ capture_output=True,
287
+ text=True,
288
+ timeout=timeout_s,
289
+ )
290
+ except subprocess.TimeoutExpired:
291
+ results.append(
292
+ SandboxImagePullResult(
293
+ image=image,
294
+ ok=False,
295
+ output=f"docker pull timed out after {timeout_s}s",
296
+ )
297
+ )
298
+ continue
299
+ except OSError as exc:
300
+ results.append(
301
+ SandboxImagePullResult(
302
+ image=image,
303
+ ok=False,
304
+ output=_compact_detail(str(exc)),
305
+ )
306
+ )
307
+ continue
308
+ output = _compact_detail(_combined_output(proc.stdout, proc.stderr), max_chars=4000)
309
+ results.append(
310
+ SandboxImagePullResult(
311
+ image=image,
312
+ ok=proc.returncode == 0,
313
+ output=output,
314
+ )
315
+ )
316
+ return SandboxPullResult(ok=all(item.ok for item in results), results=tuple(results))
317
+
318
+
319
+ def _needs_sudo() -> bool:
320
+ geteuid = getattr(os, "geteuid", None)
321
+ if not callable(geteuid):
322
+ return True
323
+ try:
324
+ return geteuid() != 0
325
+ except OSError:
326
+ return True
327
+
328
+
329
+ def detect_bubblewrap_install_plan() -> BubblewrapInstallPlan | None:
330
+ """Return how to install Bubblewrap on this host, or ``None`` if not applicable.
331
+
332
+ Returns ``None`` when not on Linux, when ``bwrap`` is already installed, when no
333
+ supported package manager is found, or when elevation is needed but ``sudo`` is
334
+ unavailable.
335
+ """
336
+ if not _is_linux() or shutil.which("bwrap"):
337
+ return None
338
+ needs_sudo = _needs_sudo()
339
+ sudo_path = shutil.which("sudo")
340
+ if needs_sudo and not sudo_path:
341
+ return None
342
+ for manager, base_cmd in _BWRAP_INSTALL_MANAGERS:
343
+ if not shutil.which(manager):
344
+ continue
345
+ command = ("sudo", *base_cmd) if needs_sudo else base_cmd
346
+ return BubblewrapInstallPlan(
347
+ manager=manager,
348
+ command=command,
349
+ display=" ".join(command),
350
+ )
351
+ return None
352
+
353
+
354
+ def install_bubblewrap(
355
+ *,
356
+ plan: BubblewrapInstallPlan | None = None,
357
+ timeout_s: int = _BWRAP_INSTALL_TIMEOUT_S,
358
+ ) -> BubblewrapInstallResult:
359
+ """Install Bubblewrap via the host package manager.
360
+
361
+ The package manager runs with inherited stdio so an interactive ``sudo`` password
362
+ prompt and progress output reach the user directly.
363
+ """
364
+ plan = plan or detect_bubblewrap_install_plan()
365
+ if plan is None:
366
+ return BubblewrapInstallResult(
367
+ ok=False,
368
+ command="",
369
+ detail=(
370
+ "No supported Linux package manager was found to install Bubblewrap "
371
+ "automatically. Install `bubblewrap` manually, or use Docker."
372
+ ),
373
+ )
374
+ try:
375
+ proc = subprocess.run(list(plan.command), check=False, timeout=timeout_s)
376
+ except subprocess.TimeoutExpired:
377
+ return BubblewrapInstallResult(
378
+ ok=False,
379
+ command=plan.display,
380
+ detail=f"install timed out after {timeout_s}s",
381
+ )
382
+ except OSError as exc:
383
+ return BubblewrapInstallResult(ok=False, command=plan.display, detail=str(exc))
384
+ if proc.returncode != 0:
385
+ return BubblewrapInstallResult(
386
+ ok=False,
387
+ command=plan.display,
388
+ detail=f"package manager exited with code {proc.returncode}",
389
+ )
390
+ if not shutil.which("bwrap"):
391
+ return BubblewrapInstallResult(
392
+ ok=False,
393
+ command=plan.display,
394
+ detail="bubblewrap (`bwrap`) is still not on PATH after the install completed",
395
+ )
396
+ return BubblewrapInstallResult(ok=True, command=plan.display, detail="bubblewrap installed")
397
+
398
+
399
+ def format_sandbox_problem_message(result: SandboxDiagnostic) -> str:
400
+ if result.ready:
401
+ if result.status == "disabled":
402
+ return "Alysis Code sandboxing is disabled by configuration."
403
+ return "Alysis Code sandbox is ready."
404
+
405
+ lines = [
406
+ "Alysis Code needs a safe runner before it can execute tests and shell commands.",
407
+ "",
408
+ "A safe runner isolates command execution from the rest of your computer.",
409
+ "",
410
+ "Status: not ready",
411
+ ]
412
+ if result.selected_backend:
413
+ lines.append(f"Selected backend: {result.selected_backend}")
414
+ lines.append("")
415
+ lines.append("Next step:")
416
+ for step in result.next_steps:
417
+ lines.append(f"- {step}")
418
+ return "\n".join(lines)
419
+
420
+
421
+ def _diagnostic(
422
+ *,
423
+ settings: ShellSandboxSettings,
424
+ server_image: str,
425
+ selected_backend: str | None,
426
+ checks: list[SandboxCheck],
427
+ next_steps: tuple[str, ...],
428
+ ) -> SandboxDiagnostic:
429
+ ready = _checks_ready(checks, selected_backend=selected_backend)
430
+ can_pull = any(check.name.endswith("image") and check.status == "missing" for check in checks)
431
+ return SandboxDiagnostic(
432
+ ready=ready,
433
+ status="ready" if ready else "not_ready",
434
+ configured_mode=settings.mode,
435
+ configured_backend=settings.backend,
436
+ selected_backend=selected_backend,
437
+ docker_image=settings.docker_image,
438
+ server_image=server_image,
439
+ checks=tuple(checks),
440
+ next_steps=next_steps,
441
+ can_pull=can_pull,
442
+ )
443
+
444
+
445
+ def _select_configured_backend(settings: ShellSandboxSettings) -> str | None:
446
+ if settings.backend == "bwrap":
447
+ return "bwrap"
448
+ if settings.backend == "docker":
449
+ return "docker"
450
+
451
+ if _is_linux() and shutil.which("bwrap"):
452
+ return "bwrap"
453
+ if shutil.which("docker"):
454
+ return "docker"
455
+ return None
456
+
457
+
458
+ def _is_linux() -> bool:
459
+ return platform.system().lower() == "linux"
460
+
461
+
462
+ def _resolve_server_worker_backend() -> str:
463
+ raw = str(env_get("ALYSIS_SERVER_WORKER_BACKEND") or "").strip().lower()
464
+ if raw in {"bwrap", "docker"}:
465
+ return raw
466
+ if _is_linux():
467
+ return "bwrap"
468
+ return "docker"
469
+
470
+
471
+ def _run_docker_info() -> SandboxCheck:
472
+ if not shutil.which("docker"):
473
+ return SandboxCheck("Docker daemon", "skipped", "Docker CLI is not installed.")
474
+ try:
475
+ proc = subprocess.run(
476
+ ["docker", "info"],
477
+ check=False,
478
+ capture_output=True,
479
+ text=True,
480
+ timeout=_DOCKER_INFO_TIMEOUT_S,
481
+ )
482
+ except (OSError, subprocess.TimeoutExpired) as exc:
483
+ return SandboxCheck("Docker daemon", "failed", str(exc))
484
+ if proc.returncode == 0:
485
+ return SandboxCheck("Docker daemon", "ok", "running")
486
+ return SandboxCheck(
487
+ "Docker daemon",
488
+ "failed",
489
+ _docker_info_failure_detail(stdout=proc.stdout, stderr=proc.stderr),
490
+ )
491
+
492
+
493
+ def _docker_image_check(image: str, *, label: str) -> SandboxCheck:
494
+ try:
495
+ proc = subprocess.run(
496
+ ["docker", "image", "inspect", image],
497
+ check=False,
498
+ capture_output=True,
499
+ text=True,
500
+ timeout=_DOCKER_IMAGE_INSPECT_TIMEOUT_S,
501
+ )
502
+ except (OSError, subprocess.TimeoutExpired) as exc:
503
+ return SandboxCheck(label, "failed", str(exc))
504
+ if proc.returncode == 0:
505
+ return SandboxCheck(label, "ok", image)
506
+ return SandboxCheck(label, "missing", f"{image} is not downloaded locally.")
507
+
508
+
509
+ def _append_smoke_check(*, checks: list[SandboxCheck], settings: ShellSandboxSettings) -> None:
510
+ try:
511
+ with tempfile.TemporaryDirectory(prefix="alysis-sandbox-smoke-") as raw_tmp:
512
+ root = Path(raw_tmp)
513
+ runner = build_shell_runner_from_settings(settings, root, warning_callback=None)
514
+ result = runner.run(
515
+ root=root,
516
+ cwd=root,
517
+ cmd=f"printf {_READY_SMOKE_OUTPUT}",
518
+ timeout_s=_SANDBOX_SMOKE_TIMEOUT_S,
519
+ )
520
+ except (OSError, RuntimeError, subprocess.TimeoutExpired) as exc:
521
+ checks.append(SandboxCheck("sandbox smoke test", "failed", str(exc)))
522
+ return
523
+ output = _combined_output(result.stdout, result.stderr).strip()
524
+ if result.returncode == 0 and output == _READY_SMOKE_OUTPUT:
525
+ checks.append(SandboxCheck("sandbox smoke test", "ok", "command executed in sandbox"))
526
+ return
527
+ detail = output or f"exit code {result.returncode}"
528
+ checks.append(SandboxCheck("sandbox smoke test", "failed", detail))
529
+
530
+
531
+ def _checks_ready(checks: list[SandboxCheck], *, selected_backend: str | None) -> bool:
532
+ if selected_backend is None:
533
+ return False
534
+ required_names = {"selected backend"}
535
+ if selected_backend == "bwrap":
536
+ required_names.add("bubblewrap")
537
+ elif selected_backend == "docker":
538
+ required_names.update({"Docker CLI", "Docker daemon", "sandbox image"})
539
+ if _check_status(checks, "Docker daemon") is not None:
540
+ required_names.update({"Docker CLI", "Docker daemon"})
541
+ if _check_status(checks, "server sandbox image") is not None:
542
+ required_names.add("server sandbox image")
543
+ if any(check.status == "failed" for check in checks):
544
+ return False
545
+ for check in checks:
546
+ if check.name in required_names and check.status != "ok":
547
+ return False
548
+ smoke_status = _check_status(checks, "sandbox smoke test")
549
+ return smoke_status in {None, "ok"}
550
+
551
+
552
+ def _check_status(checks: list[SandboxCheck], name: str) -> SandboxCheckStatus | None:
553
+ for check in checks:
554
+ if check.name == name:
555
+ return check.status
556
+ return None
557
+
558
+
559
+ def _next_steps_for_checks(
560
+ checks: list[SandboxCheck],
561
+ *,
562
+ selected_backend: str,
563
+ ) -> tuple[str, ...]:
564
+ if _check_status(checks, "Docker daemon") not in {None, "ok"}:
565
+ return _docker_daemon_steps(docker_path=shutil.which("docker"))
566
+
567
+ if selected_backend == "bwrap":
568
+ if _check_status(checks, "bubblewrap") == "skipped":
569
+ return _bwrap_linux_only_steps()
570
+ if _check_status(checks, "bubblewrap") != "ok":
571
+ return _bwrap_missing_steps(settings=None)
572
+ if _check_status(checks, "sandbox smoke test") == "failed":
573
+ return (
574
+ "Bubblewrap is installed but the sandbox smoke test failed. Check kernel user namespace support or set ALYSIS_SHELL_SANDBOX_BACKEND=docker.",
575
+ "Run `alysis doctor sandbox` again after fixing the host sandbox runtime.",
576
+ )
577
+ if _check_status(checks, "server sandbox image") == "missing":
578
+ return (
579
+ "Run `alysis sandbox pull --server` to download Alysis Code's server worker image.",
580
+ )
581
+ return ("Sandbox is ready.",)
582
+
583
+ if _check_status(checks, "Docker CLI") != "ok":
584
+ return ("Install Docker Desktop, open it, then run `alysis doctor sandbox`.",)
585
+ if _check_status(checks, "Docker daemon") != "ok":
586
+ return _docker_daemon_steps(docker_path=shutil.which("docker"))
587
+ if _check_status(checks, "sandbox image") == "missing":
588
+ return ("Run `alysis sandbox pull` to download Alysis Code's safe runner image.",)
589
+ if _check_status(checks, "sandbox smoke test") == "failed":
590
+ return (
591
+ "Docker is running, but the sandbox smoke test failed. Run `alysis doctor sandbox` and inspect the smoke test detail.",
592
+ )
593
+ if _check_status(checks, "server sandbox image") == "missing":
594
+ return (
595
+ "Run `alysis sandbox pull --server` to download Alysis Code's server worker image.",
596
+ )
597
+ return ("Sandbox is ready.",)
598
+
599
+
600
+ def _install_backend_steps() -> tuple[str, ...]:
601
+ if platform.system().lower() == "linux":
602
+ return (
603
+ "Install Bubblewrap with your Linux package manager, then run `alysis doctor sandbox`.",
604
+ "Or install Docker, start Docker, then run `alysis sandbox pull`.",
605
+ )
606
+ return ("Install Docker Desktop, open it, then run `alysis sandbox pull`.",)
607
+
608
+
609
+ def _bwrap_missing_steps(settings: ShellSandboxSettings | None) -> tuple[str, ...]:
610
+ backend_note = ""
611
+ if settings is not None and settings.backend == "bwrap":
612
+ backend_note = " because shell_sandbox.backend=bwrap is configured"
613
+ return (
614
+ f"Install Bubblewrap (`bwrap`){backend_note}, then run `alysis doctor sandbox`.",
615
+ "Or set ALYSIS_SHELL_SANDBOX_BACKEND=docker and run `alysis sandbox pull`.",
616
+ )
617
+
618
+
619
+ def _bwrap_linux_only_steps() -> tuple[str, ...]:
620
+ return (
621
+ "Bubblewrap is Linux-only. Install Docker Desktop, set ALYSIS_SHELL_SANDBOX_BACKEND=docker, then run `alysis sandbox pull`.",
622
+ )
623
+
624
+
625
+ def _docker_daemon_steps(*, docker_path: str | None) -> tuple[str, ...]:
626
+ if docker_path:
627
+ return (
628
+ "Docker is installed, but it is not running. Open Docker Desktop or start the Docker service, then run `alysis doctor sandbox`.",
629
+ )
630
+ return ("Install Docker Desktop, open it, then run `alysis sandbox pull`.",)
631
+
632
+
633
+ def _docker_not_running_message() -> str:
634
+ return (
635
+ "Docker is installed, but it is not running. Open Docker Desktop or start the Docker "
636
+ "service, then run `alysis sandbox pull` again."
637
+ )
638
+
639
+
640
+ def _combined_output(stdout: str | None, stderr: str | None) -> str:
641
+ return "\n".join(part for part in ((stdout or "").strip(), (stderr or "").strip()) if part)
642
+
643
+
644
+ def _docker_info_failure_detail(*, stdout: str | None, stderr: str | None) -> str:
645
+ combined = _combined_output(stdout, stderr)
646
+ important_lines: list[str] = []
647
+ for raw_line in combined.splitlines():
648
+ line = raw_line.strip()
649
+ lowered = line.lower()
650
+ if any(
651
+ marker in lowered
652
+ for marker in (
653
+ "cannot connect",
654
+ "failed to connect",
655
+ "docker daemon",
656
+ "permission denied",
657
+ )
658
+ ):
659
+ important_lines.append(line)
660
+ if important_lines:
661
+ return _compact_detail(" ".join(important_lines))
662
+ return _compact_detail(combined)
663
+
664
+
665
+ def _compact_detail(value: str, *, max_chars: int = 700) -> str:
666
+ text = value.strip()
667
+ if not text:
668
+ return "(no output)"
669
+ if len(text) <= max_chars:
670
+ return text
671
+ return text[: max_chars - 3].rstrip() + "..."
672
+
673
+
674
+ def sandbox_env_summary() -> dict[str, str | None]:
675
+ keys = (
676
+ "ALYSIS_SHELL_SANDBOX_MODE",
677
+ "ALYSIS_SHELL_SANDBOX_BACKEND",
678
+ "ALYSIS_SHELL_SANDBOX_NETWORK",
679
+ "ALYSIS_SHELL_SANDBOX_DOCKER_IMAGE",
680
+ "ALYSIS_VERIFY_SANDBOX_MODE",
681
+ )
682
+ return {key: os.environ.get(key) for key in keys}