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,272 @@
1
+ """Machine-readable auth diagnostics for supervising applications.
2
+
3
+ A GUI that spawns the CLI cannot infer auth state from human console text, and a
4
+ spawned process does not necessarily see the same environment — most notably the
5
+ OS keyring — as the interactive shell the user logged in from. Everything here is
6
+ non-secret and safe to hand to a supervising app.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ import shutil
13
+ import sys
14
+ from dataclasses import dataclass
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+ from .branding import env_get
19
+ from .config import config_path
20
+ from .mcp.token_store import (
21
+ KeyringOutcome,
22
+ TokenPayloadLoadResult,
23
+ fallback_key_source,
24
+ keyring_availability,
25
+ keyring_backend_name,
26
+ last_keyring_outcome,
27
+ load_token_payload_result,
28
+ planned_key_source,
29
+ stored_key_source,
30
+ )
31
+
32
+ CONTEXT_INTERACTIVE = "interactive"
33
+ CONTEXT_SPAWNED = "spawned"
34
+
35
+ # A GUI-spawned process on macOS typically inherits only the system default
36
+ # PATH, which is how a CLI that works in a terminal can fail when spawned.
37
+ _MINIMAL_PATH_ENTRY_COUNT = 4
38
+ _CI_ENV_KEYS = ("CI", "GITHUB_ACTIONS", "BUILDKITE", "TEAMCITY_VERSION", "JENKINS_URL")
39
+
40
+
41
+ @dataclass(frozen=True, slots=True)
42
+ class CredentialStoreHealth:
43
+ """Non-secret health of one encrypted credential store."""
44
+
45
+ name: str
46
+ path: str
47
+ exists: bool
48
+ readable: bool
49
+ key_source: str | None = None
50
+ planned_key_source: str | None = None
51
+ entry_count: int | None = None
52
+ legacy_plaintext: bool = False
53
+ error: str | None = None
54
+
55
+ def to_payload(self) -> dict[str, Any]:
56
+ return {
57
+ "name": self.name,
58
+ "path": self.path,
59
+ "exists": self.exists,
60
+ "readable": self.readable,
61
+ "key_source": self.key_source,
62
+ "planned_key_source": self.planned_key_source,
63
+ "entry_count": self.entry_count,
64
+ "legacy_plaintext": self.legacy_plaintext,
65
+ "error": self.error,
66
+ }
67
+
68
+
69
+ def provider_token_store_path() -> Path:
70
+ """Return the native provider credential vault path."""
71
+
72
+ # Imported lazily: provider_auth.store imports the MCP token store, and a
73
+ # module-level import here would make auth diagnostics depend on the whole
74
+ # provider auth package just to name a file.
75
+ from .provider_auth.store import provider_token_store_path as _path
76
+
77
+ return _path()
78
+
79
+
80
+ def mcp_token_store_path() -> Path:
81
+ """Return the MCP OAuth credential store path."""
82
+
83
+ from .mcp.oauth_store import mcp_oauth_token_store_path
84
+
85
+ return mcp_oauth_token_store_path()
86
+
87
+
88
+ def keyring_snapshot() -> dict[str, Any]:
89
+ """Probe the OS keyring and describe what a credential write would use."""
90
+
91
+ probe = keyring_availability()
92
+ observed = last_keyring_outcome()
93
+ return {
94
+ "available": probe.available,
95
+ "backend": probe.backend,
96
+ "env_override": os.environ.get("PYTHON_KEYRING_BACKEND") or None,
97
+ "reason": probe.reason,
98
+ "error_type": probe.error_type,
99
+ "detail": probe.detail,
100
+ "observed_this_process": None if observed is None else _outcome_payload(observed),
101
+ }
102
+
103
+
104
+ def _outcome_payload(outcome: KeyringOutcome) -> dict[str, Any]:
105
+ return {
106
+ "available": outcome.available,
107
+ "reason": outcome.reason,
108
+ "error_type": outcome.error_type,
109
+ "backend": outcome.backend,
110
+ "detail": outcome.detail,
111
+ }
112
+
113
+
114
+ def credential_backend_fields() -> dict[str, Any]:
115
+ """Return the keyring fields every auth status payload carries.
116
+
117
+ A supervising app needs these to explain why auth state differs between the
118
+ user's terminal and the process it spawned, instead of guessing.
119
+
120
+ A degradation actually hit while serving this command is ground truth and wins
121
+ over the backend classification. Absent that, the classification is used
122
+ without a read probe, so reporting auth state never risks prompting the user
123
+ to unlock a keyring.
124
+ """
125
+
126
+ observed = last_keyring_outcome()
127
+ if observed is not None and not observed.available:
128
+ probe = observed
129
+ else:
130
+ probe = keyring_availability(probe_read=False)
131
+ return {
132
+ "keyring_available": probe.available,
133
+ "keyring_backend": probe.backend or keyring_backend_name(),
134
+ "credential_fallback": None if probe.available else fallback_key_source(),
135
+ }
136
+
137
+
138
+ def credential_store_health(name: str, path: Path) -> CredentialStoreHealth:
139
+ """Report whether one encrypted credential store can actually be read."""
140
+
141
+ planned = planned_key_source()
142
+ if not path.exists():
143
+ return CredentialStoreHealth(
144
+ name=name,
145
+ path=str(path),
146
+ exists=False,
147
+ readable=True,
148
+ planned_key_source=planned,
149
+ entry_count=0,
150
+ )
151
+ stored = stored_key_source(path)
152
+ try:
153
+ result: TokenPayloadLoadResult = load_token_payload_result(path)
154
+ except Exception as exc: # noqa: BLE001 - store backends fail in OS-specific ways
155
+ return CredentialStoreHealth(
156
+ name=name,
157
+ path=str(path),
158
+ exists=True,
159
+ readable=False,
160
+ key_source=stored,
161
+ planned_key_source=planned,
162
+ error=f"{exc.__class__.__name__}: {exc}",
163
+ )
164
+ return CredentialStoreHealth(
165
+ name=name,
166
+ path=str(path),
167
+ exists=True,
168
+ readable=True,
169
+ key_source=stored,
170
+ planned_key_source=planned,
171
+ entry_count=len(result.payload),
172
+ legacy_plaintext=result.legacy_plaintext,
173
+ )
174
+
175
+
176
+ def credential_store_healths() -> tuple[CredentialStoreHealth, ...]:
177
+ """Report health for every encrypted credential store auth depends on."""
178
+
179
+ return (
180
+ credential_store_health("provider_auth", provider_token_store_path()),
181
+ credential_store_health("mcp_oauth", mcp_token_store_path()),
182
+ )
183
+
184
+
185
+ def _isatty(stream_name: str) -> bool:
186
+ stream = getattr(sys, stream_name, None)
187
+ if stream is None:
188
+ return False
189
+ try:
190
+ return bool(stream.isatty())
191
+ except Exception: # noqa: BLE001 - replaced streams need not implement isatty
192
+ return False
193
+
194
+
195
+ def context_snapshot() -> dict[str, Any]:
196
+ """Describe whether this process looks interactive or spawned by another app."""
197
+
198
+ stdin_tty = _isatty("stdin")
199
+ stdout_tty = _isatty("stdout")
200
+ stderr_tty = _isatty("stderr")
201
+ ci = sorted(key for key in _CI_ENV_KEYS if os.environ.get(key))
202
+ kind = CONTEXT_INTERACTIVE if stdin_tty and stdout_tty else CONTEXT_SPAWNED
203
+ return {
204
+ "kind": kind,
205
+ "stdin_tty": stdin_tty,
206
+ "stdout_tty": stdout_tty,
207
+ "stderr_tty": stderr_tty,
208
+ "term": os.environ.get("TERM") or None,
209
+ "ci_env": ci,
210
+ "ssh_session": bool(os.environ.get("SSH_TTY") or os.environ.get("SSH_CONNECTION")),
211
+ }
212
+
213
+
214
+ def _path_entries() -> list[str]:
215
+ raw = os.environ.get("PATH") or ""
216
+ return [entry for entry in raw.split(os.pathsep) if entry]
217
+
218
+
219
+ def path_snapshot() -> dict[str, Any]:
220
+ """Describe where the CLI was resolved from and whether PATH looks truncated."""
221
+
222
+ entries = _path_entries()
223
+ return {
224
+ "entry_count": len(entries),
225
+ "looks_minimal": len(entries) <= _MINIMAL_PATH_ENTRY_COUNT,
226
+ "resolved_cli": shutil.which("alysis"),
227
+ "argv0": (sys.argv[0] or None) if sys.argv else None,
228
+ "python_executable": sys.executable or None,
229
+ }
230
+
231
+
232
+ def environment_snapshot() -> dict[str, Any]:
233
+ """Report the auth-relevant environment a supervising app cannot see."""
234
+
235
+ return {
236
+ "home": os.path.expanduser("~"),
237
+ "home_env": os.environ.get("HOME") or os.environ.get("USERPROFILE") or None,
238
+ "config_dir": str(config_path().parent),
239
+ "config_dir_override": env_get("ALYSIS_CONFIG_DIR") or None,
240
+ "config_path": str(config_path()),
241
+ "config_exists": config_path().exists(),
242
+ "platform": sys.platform,
243
+ "path": path_snapshot(),
244
+ }
245
+
246
+
247
+ def auth_doctor_payload() -> dict[str, Any]:
248
+ """Build the full ``alysis doctor auth --json`` report."""
249
+
250
+ return {
251
+ "context": context_snapshot(),
252
+ "environment": environment_snapshot(),
253
+ "keyring": keyring_snapshot(),
254
+ "credential_stores": [health.to_payload() for health in credential_store_healths()],
255
+ }
256
+
257
+
258
+ __all__ = [
259
+ "CONTEXT_INTERACTIVE",
260
+ "CONTEXT_SPAWNED",
261
+ "CredentialStoreHealth",
262
+ "auth_doctor_payload",
263
+ "context_snapshot",
264
+ "credential_backend_fields",
265
+ "credential_store_health",
266
+ "credential_store_healths",
267
+ "environment_snapshot",
268
+ "keyring_snapshot",
269
+ "mcp_token_store_path",
270
+ "path_snapshot",
271
+ "provider_token_store_path",
272
+ ]
@@ -0,0 +1,366 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import platform
5
+ import shutil
6
+ import subprocess
7
+ import threading
8
+ import uuid
9
+ from collections.abc import Callable
10
+ from dataclasses import dataclass
11
+ from pathlib import Path
12
+ from typing import Literal, Protocol
13
+
14
+ from .branding import default_sandbox_docker_image
15
+ from .config import AppConfig, ConfigError
16
+ from .sandbox_runner import (
17
+ _SENSITIVE_ENV_KEYS,
18
+ _build_bwrap_argv,
19
+ _build_docker_argv,
20
+ _docker_cleanup_container,
21
+ _emit_warning,
22
+ _supports_bwrap_unshare_cgroup,
23
+ )
24
+ from .sandbox_settings import ShellSandboxSettings, resolve_shell_sandbox_settings
25
+ from .service_persistence import apply_non_interactive_defaults
26
+
27
+ BackgroundTerminationMode = Literal["process_group", "direct"]
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class BackgroundProcessSpawn:
32
+ popen: subprocess.Popen[bytes]
33
+ cleanup: Callable[[], None]
34
+ started_argv: tuple[str, ...]
35
+ termination_mode: BackgroundTerminationMode = "process_group"
36
+ orphan_cleanup_kind: str | None = None
37
+ orphan_cleanup_id: str | None = None
38
+
39
+
40
+ class BackgroundShellRunner(Protocol):
41
+ def start(
42
+ self,
43
+ *,
44
+ root: Path,
45
+ cwd: Path,
46
+ cmd: str,
47
+ env_overrides: dict[str, str] | None = None,
48
+ ) -> BackgroundProcessSpawn: ...
49
+
50
+
51
+ def _background_env(env_overrides: dict[str, str] | None) -> dict[str, str]:
52
+ blocked = {key.upper() for key in _SENSITIVE_ENV_KEYS}
53
+ env = {key: value for key, value in os.environ.items() if key.upper() not in blocked}
54
+ return _apply_env_overrides(env, env_overrides)
55
+
56
+
57
+ def _apply_env_overrides(
58
+ env: dict[str, str],
59
+ env_overrides: dict[str, str] | None,
60
+ ) -> dict[str, str]:
61
+ blocked = {key.upper() for key in _SENSITIVE_ENV_KEYS}
62
+ # Defaults first so an explicit override still wins. Shared by the
63
+ # background runners and the durable-service manager, so this covers every
64
+ # non-foreground spawn in one place.
65
+ merged = apply_non_interactive_defaults(env)
66
+ for key, value in (env_overrides or {}).items():
67
+ if key.upper() in blocked:
68
+ continue
69
+ merged[key] = value
70
+ return merged
71
+
72
+
73
+ def _noop_cleanup() -> None:
74
+ return None
75
+
76
+
77
+ @dataclass(frozen=True)
78
+ class HostBackgroundRunner:
79
+ def start(
80
+ self,
81
+ *,
82
+ root: Path,
83
+ cwd: Path,
84
+ cmd: str,
85
+ env_overrides: dict[str, str] | None = None,
86
+ ) -> BackgroundProcessSpawn:
87
+ kwargs: dict[str, object] = {}
88
+ if os.name == "nt": # pragma: no cover - exercised on Windows
89
+ kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
90
+ else:
91
+ kwargs["start_new_session"] = True
92
+ popen = subprocess.Popen(
93
+ cmd,
94
+ shell=True,
95
+ cwd=str(cwd),
96
+ stdin=subprocess.DEVNULL,
97
+ stdout=subprocess.PIPE,
98
+ stderr=subprocess.PIPE,
99
+ env=_background_env(env_overrides),
100
+ bufsize=0,
101
+ **kwargs,
102
+ )
103
+ return BackgroundProcessSpawn(
104
+ popen=popen,
105
+ cleanup=_noop_cleanup,
106
+ started_argv=(cmd,),
107
+ )
108
+
109
+
110
+ @dataclass(frozen=True)
111
+ class DisabledBackgroundRunner:
112
+ reason: str = "Background shell execution is disabled."
113
+
114
+ def start(
115
+ self,
116
+ *,
117
+ root: Path,
118
+ cwd: Path,
119
+ cmd: str,
120
+ env_overrides: dict[str, str] | None = None,
121
+ ) -> BackgroundProcessSpawn:
122
+ raise RuntimeError(self.reason)
123
+
124
+
125
+ class LazyBackgroundShellRunner:
126
+ def __init__(self, loader: Callable[[], BackgroundShellRunner]) -> None:
127
+ self._loader = loader
128
+ self._runner: BackgroundShellRunner | None = None
129
+ self._load_error: Exception | None = None
130
+ self._lock = threading.Lock()
131
+
132
+ def _resolve_runner(self) -> BackgroundShellRunner:
133
+ if self._runner is not None:
134
+ return self._runner
135
+ if self._load_error is not None:
136
+ raise self._load_error
137
+ with self._lock:
138
+ if self._runner is not None:
139
+ return self._runner
140
+ if self._load_error is not None:
141
+ raise self._load_error
142
+ try:
143
+ runner = self._loader()
144
+ except Exception as exc:
145
+ self._load_error = exc
146
+ raise
147
+ self._runner = runner
148
+ return runner
149
+
150
+ def start(
151
+ self,
152
+ *,
153
+ root: Path,
154
+ cwd: Path,
155
+ cmd: str,
156
+ env_overrides: dict[str, str] | None = None,
157
+ ) -> BackgroundProcessSpawn:
158
+ return self._resolve_runner().start(
159
+ root=root,
160
+ cwd=cwd,
161
+ cmd=cmd,
162
+ env_overrides=env_overrides,
163
+ )
164
+
165
+
166
+ @dataclass(frozen=True)
167
+ class BwrapBackgroundRunner:
168
+ network: str = "off"
169
+ clear_env: bool = True
170
+ profile: str = "hardened"
171
+
172
+ def start(
173
+ self,
174
+ *,
175
+ root: Path,
176
+ cwd: Path,
177
+ cmd: str,
178
+ env_overrides: dict[str, str] | None = None,
179
+ ) -> BackgroundProcessSpawn:
180
+ root_abs = root.resolve()
181
+ argv, run_env = _build_bwrap_argv(
182
+ root=root_abs,
183
+ cwd=cwd,
184
+ cmd=cmd,
185
+ network=self.network,
186
+ clear_env=self.clear_env,
187
+ profile=self.profile,
188
+ unshare_cgroup=_supports_bwrap_unshare_cgroup(),
189
+ )
190
+ popen = subprocess.Popen(
191
+ argv,
192
+ stdin=subprocess.DEVNULL,
193
+ stdout=subprocess.PIPE,
194
+ stderr=subprocess.PIPE,
195
+ cwd=str(root_abs),
196
+ env=_apply_env_overrides(run_env, env_overrides),
197
+ bufsize=0,
198
+ start_new_session=True,
199
+ )
200
+ return BackgroundProcessSpawn(
201
+ popen=popen,
202
+ cleanup=_noop_cleanup,
203
+ started_argv=tuple(argv),
204
+ )
205
+
206
+
207
+ @dataclass(frozen=True)
208
+ class DockerBackgroundRunner:
209
+ network: str = "off"
210
+ docker_image: str = default_sandbox_docker_image("dev")
211
+ clear_env: bool = True
212
+ pids_limit: int | None = None
213
+ memory_limit: str | None = None
214
+ cpus: str | None = None
215
+ read_only_rootfs: bool = False
216
+ protect_repo_meta: bool = True
217
+ env_allowlist: tuple[str, ...] = ()
218
+ warning_callback: Callable[[str], None] | None = None
219
+
220
+ def start(
221
+ self,
222
+ *,
223
+ root: Path,
224
+ cwd: Path,
225
+ cmd: str,
226
+ env_overrides: dict[str, str] | None = None,
227
+ ) -> BackgroundProcessSpawn:
228
+ root_abs = root.resolve()
229
+ container_name = f"alysis-bgsbx-{uuid.uuid4().hex[:12]}"
230
+ argv, run_env = _build_docker_argv(
231
+ root=root_abs,
232
+ cwd=cwd,
233
+ cmd=cmd,
234
+ container_name=container_name,
235
+ network=self.network,
236
+ docker_image=self.docker_image,
237
+ clear_env=self.clear_env,
238
+ pids_limit=self.pids_limit,
239
+ memory_limit=self.memory_limit,
240
+ cpus=self.cpus,
241
+ read_only_rootfs=self.read_only_rootfs,
242
+ protect_repo_meta=self.protect_repo_meta,
243
+ env_allowlist=self.env_allowlist,
244
+ )
245
+ run_env = _apply_env_overrides(run_env, env_overrides)
246
+
247
+ def cleanup() -> None:
248
+ _docker_cleanup_container(
249
+ container_name,
250
+ cwd=str(root_abs),
251
+ env=run_env,
252
+ warning_callback=self.warning_callback,
253
+ reason="background process cleanup",
254
+ quiet=True,
255
+ )
256
+
257
+ popen = subprocess.Popen(
258
+ argv,
259
+ stdin=subprocess.DEVNULL,
260
+ stdout=subprocess.PIPE,
261
+ stderr=subprocess.PIPE,
262
+ cwd=str(root_abs),
263
+ env=run_env,
264
+ bufsize=0,
265
+ )
266
+ return BackgroundProcessSpawn(
267
+ popen=popen,
268
+ cleanup=cleanup,
269
+ started_argv=tuple(argv),
270
+ termination_mode="direct",
271
+ orphan_cleanup_kind="docker-container",
272
+ orphan_cleanup_id=container_name,
273
+ )
274
+
275
+
276
+ def build_background_shell_runner_from_settings(
277
+ settings: ShellSandboxSettings,
278
+ root: Path,
279
+ warning_callback: Callable[[str], None] | None = None,
280
+ ) -> BackgroundShellRunner:
281
+ _ = root.resolve()
282
+
283
+ if settings.mode == "off":
284
+ return HostBackgroundRunner()
285
+
286
+ is_linux = platform.system().lower() == "linux"
287
+ has_bwrap = is_linux and shutil.which("bwrap") is not None
288
+ has_docker = shutil.which("docker") is not None
289
+
290
+ def fallback_or_error(*, reason: str) -> BackgroundShellRunner:
291
+ if settings.mode == "warn":
292
+ disabled_reason = (
293
+ f"Background shell sandbox unavailable ({reason}); host fallback is disabled."
294
+ )
295
+ _emit_warning(disabled_reason, warning_callback=warning_callback)
296
+ return DisabledBackgroundRunner(reason=disabled_reason)
297
+ raise ConfigError(
298
+ "Shell sandbox strict mode is enabled, but no usable backend is available "
299
+ f"for background processes: {reason}. Install bubblewrap (Linux) or Docker, "
300
+ "or set ALYSIS_SHELL_SANDBOX_MODE=off for explicit unsafe host execution."
301
+ )
302
+
303
+ if settings.backend == "auto":
304
+ if has_bwrap:
305
+ return BwrapBackgroundRunner(
306
+ network=settings.network,
307
+ clear_env=settings.clear_env,
308
+ profile=settings.bwrap_profile,
309
+ )
310
+ if has_docker:
311
+ return _docker_background_runner_from_settings(
312
+ settings,
313
+ warning_callback=warning_callback,
314
+ )
315
+ return fallback_or_error(reason="auto backend could not find bwrap or docker")
316
+
317
+ if settings.backend == "bwrap":
318
+ if has_bwrap:
319
+ return BwrapBackgroundRunner(
320
+ network=settings.network,
321
+ clear_env=settings.clear_env,
322
+ profile=settings.bwrap_profile,
323
+ )
324
+ return fallback_or_error(reason="bwrap backend selected, but bubblewrap is not available")
325
+
326
+ if settings.backend == "docker":
327
+ if has_docker:
328
+ return _docker_background_runner_from_settings(
329
+ settings,
330
+ warning_callback=warning_callback,
331
+ )
332
+ return fallback_or_error(reason="docker backend selected, but docker is not available")
333
+
334
+ raise ConfigError(f"Unhandled shell sandbox backend: {settings.backend}")
335
+
336
+
337
+ def _docker_background_runner_from_settings(
338
+ settings: ShellSandboxSettings,
339
+ *,
340
+ warning_callback: Callable[[str], None] | None,
341
+ ) -> DockerBackgroundRunner:
342
+ return DockerBackgroundRunner(
343
+ network=settings.network,
344
+ docker_image=settings.docker_image,
345
+ clear_env=settings.clear_env,
346
+ pids_limit=settings.docker_pids_limit,
347
+ memory_limit=settings.docker_memory,
348
+ cpus=settings.docker_cpus,
349
+ read_only_rootfs=settings.docker_read_only,
350
+ protect_repo_meta=settings.protect_repo_meta,
351
+ env_allowlist=settings.docker_env_allowlist,
352
+ warning_callback=warning_callback,
353
+ )
354
+
355
+
356
+ def build_background_shell_runner(
357
+ cfg: AppConfig,
358
+ root: Path,
359
+ warning_callback: Callable[[str], None] | None = None,
360
+ ) -> BackgroundShellRunner:
361
+ settings = resolve_shell_sandbox_settings(cfg)
362
+ return build_background_shell_runner_from_settings(
363
+ settings,
364
+ root,
365
+ warning_callback=warning_callback,
366
+ )