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,903 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ import copy
5
+ import hashlib
6
+ import json
7
+ import os
8
+ import re
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from ..branding import canonical_user_config_dir, env_get, env_get_from
14
+ from ..runtime_kind import RuntimeKind, runtime_kind_values
15
+
16
+ _DEFAULT_TIMEOUT_S = 15.0
17
+ _MAX_TIMEOUT_S = 300.0
18
+ _MAX_MANIFEST_BYTES = 32_000
19
+ _MAX_SCHEMA_BYTES = 24_000
20
+ _MAX_DESCRIPTION_CHARS = 600
21
+ _MAX_REQUIRED_ENV_VARS = 32
22
+ _MAX_SECRET_REFS = 32
23
+ _MAX_NETWORK_HOSTS = 64
24
+ _RESERVED_NAME_PREFIXES = ("mcp__",)
25
+ _RESERVED_EXACT_NAMES = frozenset({"mcp_resources_list", "mcp_resource_read"})
26
+ _VALID_ISOLATION_VALUES = {"subprocess"}
27
+ _SUPPORTED_MANIFEST_VERSION = 1
28
+ _VALID_NETWORK_ACCESS_VALUES = {
29
+ "unspecified",
30
+ "none",
31
+ "local",
32
+ "restricted",
33
+ "unrestricted",
34
+ }
35
+ _VALID_FILESYSTEM_SCOPE_VALUES = {
36
+ "unspecified",
37
+ "none",
38
+ "tool_dir",
39
+ "workspace",
40
+ "unrestricted",
41
+ }
42
+ _VALID_PROCESS_SPAWN_VALUES = {
43
+ "unspecified",
44
+ "none",
45
+ "unrestricted",
46
+ }
47
+ _DEFAULT_ENABLED_IN = (
48
+ RuntimeKind.INTERACTIVE_CHAT.value,
49
+ RuntimeKind.ONE_SHOT.value,
50
+ RuntimeKind.FORGE_EXEC.value,
51
+ )
52
+ _ENV_NAME_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
53
+
54
+
55
+ @dataclass(frozen=True)
56
+ class CustomToolCapabilities:
57
+ read_only: bool = False
58
+ destructive: bool = False
59
+ network_access: str = "unspecified"
60
+ network_hosts: tuple[str, ...] = ()
61
+ filesystem_read_scope: str = "unspecified"
62
+ filesystem_write_scope: str = "unspecified"
63
+ process_spawn: str = "unspecified"
64
+ secret_refs: tuple[str, ...] = ()
65
+
66
+ def to_dict(self) -> dict[str, Any]:
67
+ return {
68
+ "read_only": self.read_only,
69
+ "destructive": self.destructive,
70
+ "network_access": self.network_access,
71
+ "network_hosts": list(self.network_hosts),
72
+ "filesystem_read_scope": self.filesystem_read_scope,
73
+ "filesystem_write_scope": self.filesystem_write_scope,
74
+ "process_spawn": self.process_spawn,
75
+ "secret_refs": list(self.secret_refs),
76
+ }
77
+
78
+
79
+ @dataclass(frozen=True)
80
+ class CustomToolSpec:
81
+ name: str
82
+ description: str
83
+ input_schema: dict[str, Any]
84
+ manifest_version: int
85
+ capabilities: CustomToolCapabilities
86
+ output_schema: dict[str, Any] | None
87
+ timeout_s: float
88
+ required_env: tuple[str, ...]
89
+ enabled_in: tuple[str, ...]
90
+ isolation: str
91
+ source_scope: str
92
+ source_path: Path
93
+ relative_tool_path: str
94
+ file_hash: str
95
+ missing_env: tuple[str, ...]
96
+
97
+ @property
98
+ def normalized_name(self) -> str:
99
+ return self.name.casefold()
100
+
101
+ def metadata(self, *, include_output_schema: bool = False) -> dict[str, Any]:
102
+ payload: dict[str, Any] = {
103
+ "name": self.name,
104
+ "manifest_version": self.manifest_version,
105
+ "source_scope": self.source_scope,
106
+ "relative_tool_path": self.relative_tool_path,
107
+ "file_hash": self.file_hash,
108
+ "capabilities": self.capabilities.to_dict(),
109
+ "has_output_schema": self.output_schema is not None,
110
+ }
111
+ if include_output_schema and self.output_schema is not None:
112
+ payload["output_schema"] = copy.deepcopy(self.output_schema)
113
+ return payload
114
+
115
+
116
+ @dataclass(frozen=True)
117
+ class CustomToolIssue:
118
+ source_scope: str
119
+ source_path: Path
120
+ relative_tool_path: str
121
+ code: str
122
+ message: str
123
+ tool_name: str = ""
124
+
125
+
126
+ @dataclass(frozen=True)
127
+ class CustomToolDiscoveryResult:
128
+ global_tools: tuple[CustomToolSpec, ...]
129
+ project_tools: tuple[CustomToolSpec, ...]
130
+ effective_tools: tuple[CustomToolSpec, ...]
131
+ shadowed_tools: tuple[CustomToolSpec, ...]
132
+ issues: tuple[CustomToolIssue, ...]
133
+
134
+ def effective_tools_by_name(self) -> dict[str, CustomToolSpec]:
135
+ return {tool.normalized_name: tool for tool in self.effective_tools}
136
+
137
+
138
+ def _config_dir() -> Path:
139
+ override = env_get("ALYSIS_CONFIG_DIR")
140
+ if override:
141
+ return Path(override)
142
+ return canonical_user_config_dir()
143
+
144
+
145
+ def project_custom_tools_root(workspace_root: Path) -> Path:
146
+ return workspace_root.resolve() / ".alysis" / "tools"
147
+
148
+
149
+ def global_custom_tools_root(*, user_config_dir: Path | None = None) -> Path:
150
+ base = _resolved_config_dir(user_config_dir=user_config_dir)
151
+ return base / "tools"
152
+
153
+
154
+ def _resolved_config_dir(*, user_config_dir: Path | None = None) -> Path:
155
+ base = user_config_dir.expanduser() if user_config_dir is not None else _config_dir()
156
+ return base.resolve()
157
+
158
+
159
+ def discover_custom_tools(
160
+ *,
161
+ workspace_root: Path,
162
+ built_in_tool_names: set[str] | None = None,
163
+ user_config_dir: Path | None = None,
164
+ env: dict[str, str] | None = None,
165
+ ) -> CustomToolDiscoveryResult:
166
+ reserved_names = {
167
+ str(name or "").strip().casefold()
168
+ for name in (built_in_tool_names or set())
169
+ if str(name or "").strip()
170
+ }
171
+ effective_env = dict(os.environ if env is None else env)
172
+ resolved_workspace_root = workspace_root.resolve()
173
+ resolved_config_dir = _resolved_config_dir(user_config_dir=user_config_dir)
174
+ global_root = global_custom_tools_root(user_config_dir=user_config_dir)
175
+ project_root = project_custom_tools_root(workspace_root)
176
+
177
+ global_tools, global_issues = _discover_scope(
178
+ root=global_root,
179
+ source_scope="global",
180
+ owning_base=resolved_config_dir,
181
+ reserved_names=reserved_names,
182
+ env=effective_env,
183
+ )
184
+ project_tools, project_issues = _discover_scope(
185
+ root=project_root,
186
+ source_scope="project",
187
+ owning_base=resolved_workspace_root,
188
+ reserved_names=reserved_names,
189
+ env=effective_env,
190
+ )
191
+
192
+ effective: dict[str, CustomToolSpec] = {tool.normalized_name: tool for tool in global_tools}
193
+ shadowed: list[CustomToolSpec] = []
194
+ for tool in project_tools:
195
+ previous = effective.get(tool.normalized_name)
196
+ if previous is not None and previous.source_scope == "global":
197
+ shadowed.append(previous)
198
+ effective[tool.normalized_name] = tool
199
+
200
+ return CustomToolDiscoveryResult(
201
+ global_tools=tuple(global_tools),
202
+ project_tools=tuple(project_tools),
203
+ effective_tools=tuple(
204
+ sorted(effective.values(), key=lambda tool: (tool.name.casefold(), tool.source_scope))
205
+ ),
206
+ shadowed_tools=tuple(sorted(shadowed, key=lambda tool: tool.name.casefold())),
207
+ issues=tuple(
208
+ sorted(
209
+ [*global_issues, *project_issues],
210
+ key=lambda issue: (
211
+ issue.tool_name.casefold() if issue.tool_name else "~",
212
+ issue.source_scope,
213
+ issue.relative_tool_path.casefold(),
214
+ ),
215
+ )
216
+ ),
217
+ )
218
+
219
+
220
+ def _discover_scope(
221
+ *,
222
+ root: Path,
223
+ source_scope: str,
224
+ owning_base: Path,
225
+ reserved_names: set[str],
226
+ env: dict[str, str],
227
+ ) -> tuple[list[CustomToolSpec], list[CustomToolIssue]]:
228
+ if not _resolved_path_is_within_scope(root, owning_base):
229
+ return [], [
230
+ _tool_issue(
231
+ source_scope=source_scope,
232
+ source_path=root,
233
+ relative_tool_path=_issue_relative_tool_path(
234
+ root,
235
+ owning_base=owning_base,
236
+ source_scope=source_scope,
237
+ ),
238
+ code="path_escape",
239
+ message=_path_escape_message(
240
+ resolved_path=root.resolve(),
241
+ owning_base=owning_base,
242
+ source_scope=source_scope,
243
+ kind="custom tool root",
244
+ ),
245
+ )
246
+ ]
247
+ if not root.exists() or not root.is_dir():
248
+ return [], []
249
+ try:
250
+ candidates = sorted(child for child in root.rglob("*.py") if child.is_file())
251
+ except OSError as exc:
252
+ return [], [
253
+ CustomToolIssue(
254
+ source_scope=source_scope,
255
+ source_path=root,
256
+ relative_tool_path=_issue_relative_tool_path(
257
+ root,
258
+ owning_base=owning_base,
259
+ source_scope=source_scope,
260
+ ),
261
+ code="scan_failed",
262
+ message=f"failed to scan tool root: {exc}",
263
+ )
264
+ ]
265
+
266
+ valid_specs: list[CustomToolSpec] = []
267
+ issues: list[CustomToolIssue] = []
268
+ seen_valid_names: dict[str, CustomToolSpec] = {}
269
+ for path in candidates:
270
+ if path.suffix != ".py":
271
+ continue
272
+ spec, issue = _load_tool_spec(
273
+ path=path,
274
+ source_scope=source_scope,
275
+ root=root,
276
+ owning_base=owning_base,
277
+ reserved_names=reserved_names,
278
+ env=env,
279
+ )
280
+ if issue is not None:
281
+ issues.append(issue)
282
+ continue
283
+ assert spec is not None
284
+ duplicate = seen_valid_names.get(spec.normalized_name)
285
+ if duplicate is not None:
286
+ issues.append(
287
+ CustomToolIssue(
288
+ source_scope=source_scope,
289
+ source_path=spec.source_path,
290
+ relative_tool_path=spec.relative_tool_path,
291
+ code="duplicate_name",
292
+ message=(
293
+ f"duplicate custom tool name '{spec.name}' in {source_scope} scope; "
294
+ f"already provided by {duplicate.relative_tool_path}"
295
+ ),
296
+ tool_name=spec.name,
297
+ )
298
+ )
299
+ continue
300
+ seen_valid_names[spec.normalized_name] = spec
301
+ valid_specs.append(spec)
302
+ return valid_specs, issues
303
+
304
+
305
+ def _load_tool_spec(
306
+ *,
307
+ path: Path,
308
+ source_scope: str,
309
+ root: Path,
310
+ owning_base: Path,
311
+ reserved_names: set[str],
312
+ env: dict[str, str],
313
+ ) -> tuple[CustomToolSpec | None, CustomToolIssue | None]:
314
+ relative_tool_path = _issue_relative_tool_path(
315
+ path,
316
+ owning_base=owning_base,
317
+ source_scope=source_scope,
318
+ )
319
+ if not _resolved_path_is_within_scope(path, owning_base):
320
+ return None, _tool_issue(
321
+ source_scope=source_scope,
322
+ source_path=path,
323
+ relative_tool_path=relative_tool_path,
324
+ code="path_escape",
325
+ message=_path_escape_message(
326
+ resolved_path=path.resolve(),
327
+ owning_base=owning_base,
328
+ source_scope=source_scope,
329
+ kind="custom tool source path",
330
+ ),
331
+ )
332
+ if path.is_symlink():
333
+ return None, _tool_issue(
334
+ source_scope=source_scope,
335
+ source_path=path,
336
+ relative_tool_path=relative_tool_path,
337
+ code="symlink_rejected",
338
+ message="symlinked tool files are not supported",
339
+ )
340
+ try:
341
+ raw_bytes = path.read_bytes()
342
+ except OSError as exc:
343
+ return None, _tool_issue(
344
+ source_scope=source_scope,
345
+ source_path=path,
346
+ relative_tool_path=relative_tool_path,
347
+ code="read_failed",
348
+ message=f"failed to read tool file: {exc}",
349
+ )
350
+ try:
351
+ source_text = raw_bytes.decode("utf-8")
352
+ except UnicodeDecodeError:
353
+ return None, _tool_issue(
354
+ source_scope=source_scope,
355
+ source_path=path,
356
+ relative_tool_path=relative_tool_path,
357
+ code="invalid_utf8",
358
+ message="tool file must be valid UTF-8",
359
+ )
360
+ if len(raw_bytes) > _MAX_MANIFEST_BYTES * 4:
361
+ return None, _tool_issue(
362
+ source_scope=source_scope,
363
+ source_path=path,
364
+ relative_tool_path=relative_tool_path,
365
+ code="file_too_large",
366
+ message="tool file is too large",
367
+ )
368
+ try:
369
+ tree = ast.parse(source_text, filename=os.fspath(path))
370
+ except SyntaxError as exc:
371
+ return None, _tool_issue(
372
+ source_scope=source_scope,
373
+ source_path=path,
374
+ relative_tool_path=relative_tool_path,
375
+ code="syntax_error",
376
+ message=f"syntax error: {exc.msg}",
377
+ )
378
+ manifest_value, manifest_error = _extract_tool_manifest(tree)
379
+ if manifest_error is not None:
380
+ return None, _tool_issue(
381
+ source_scope=source_scope,
382
+ source_path=path,
383
+ relative_tool_path=relative_tool_path,
384
+ code=manifest_error[0],
385
+ message=manifest_error[1],
386
+ )
387
+ assert manifest_value is not None
388
+ if not _has_valid_run_function(tree):
389
+ tool_name = str(manifest_value.get("name") or "")
390
+ return None, _tool_issue(
391
+ source_scope=source_scope,
392
+ source_path=path,
393
+ relative_tool_path=relative_tool_path,
394
+ code="missing_run",
395
+ message="tool file must define top-level def run(args)",
396
+ tool_name=tool_name,
397
+ )
398
+ try:
399
+ spec = _build_tool_spec(
400
+ manifest=manifest_value,
401
+ raw_bytes=raw_bytes,
402
+ source_scope=source_scope,
403
+ source_path=path,
404
+ relative_tool_path=_spec_relative_tool_path(
405
+ path,
406
+ root=root,
407
+ owning_base=owning_base,
408
+ source_scope=source_scope,
409
+ ),
410
+ reserved_names=reserved_names,
411
+ env=env,
412
+ )
413
+ except ValueError as exc:
414
+ tool_name = ""
415
+ raw_name = manifest_value.get("name")
416
+ if isinstance(raw_name, str):
417
+ tool_name = raw_name
418
+ return None, _tool_issue(
419
+ source_scope=source_scope,
420
+ source_path=path,
421
+ relative_tool_path=relative_tool_path,
422
+ code="invalid_manifest",
423
+ message=str(exc),
424
+ tool_name=tool_name,
425
+ )
426
+ return spec, None
427
+
428
+
429
+ def _extract_tool_manifest(
430
+ tree: ast.Module,
431
+ ) -> tuple[dict[str, Any] | None, tuple[str, str] | None]:
432
+ manifest_nodes: list[ast.AST] = []
433
+ for statement in tree.body:
434
+ if isinstance(statement, ast.Assign):
435
+ for target in statement.targets:
436
+ if isinstance(target, ast.Name) and target.id == "TOOL":
437
+ manifest_nodes.append(statement.value)
438
+ elif isinstance(statement, ast.AnnAssign):
439
+ if isinstance(statement.target, ast.Name) and statement.target.id == "TOOL":
440
+ manifest_nodes.append(statement.value)
441
+ if not manifest_nodes:
442
+ return None, ("missing_manifest", "tool file must define top-level TOOL = {...}")
443
+ if len(manifest_nodes) > 1:
444
+ return None, ("duplicate_manifest", "tool file defines TOOL more than once")
445
+ try:
446
+ manifest_value = ast.literal_eval(manifest_nodes[0])
447
+ except (SyntaxError, ValueError) as exc:
448
+ return None, ("invalid_manifest", f"TOOL must be a top-level literal dictionary: {exc}")
449
+ if not isinstance(manifest_value, dict):
450
+ return None, ("invalid_manifest", "TOOL must be a top-level literal dictionary")
451
+ try:
452
+ manifest_bytes = _stable_json_size(manifest_value)
453
+ except TypeError as exc:
454
+ return None, ("invalid_manifest", f"TOOL must be JSON-serializable: {exc}")
455
+ if manifest_bytes > _MAX_MANIFEST_BYTES:
456
+ return None, ("invalid_manifest", "TOOL manifest is too large")
457
+ return copy.deepcopy(manifest_value), None
458
+
459
+
460
+ def _has_valid_run_function(tree: ast.Module) -> bool:
461
+ for statement in tree.body:
462
+ if not isinstance(statement, ast.FunctionDef):
463
+ continue
464
+ if statement.name != "run":
465
+ continue
466
+ args = statement.args
467
+ positional = [*args.posonlyargs, *args.args]
468
+ if len(positional) != 1:
469
+ return False
470
+ if args.vararg is not None or args.kwarg is not None or args.kwonlyargs:
471
+ return False
472
+ return True
473
+ return False
474
+
475
+
476
+ def _build_tool_spec(
477
+ *,
478
+ manifest: dict[str, Any],
479
+ raw_bytes: bytes,
480
+ source_scope: str,
481
+ source_path: Path,
482
+ relative_tool_path: str,
483
+ reserved_names: set[str],
484
+ env: dict[str, str],
485
+ ) -> CustomToolSpec:
486
+ required_keys = {"name", "description", "input_schema"}
487
+ allowed_keys = {
488
+ "manifest_version",
489
+ "name",
490
+ "description",
491
+ "input_schema",
492
+ "output_schema",
493
+ "capabilities",
494
+ "timeout_s",
495
+ "required_env",
496
+ "enabled_in",
497
+ "isolation",
498
+ }
499
+ missing_keys = sorted(required_keys - set(manifest))
500
+ if missing_keys:
501
+ raise ValueError(f"missing required TOOL keys: {', '.join(missing_keys)}")
502
+ unknown_keys = sorted(set(manifest) - allowed_keys)
503
+ if unknown_keys:
504
+ raise ValueError(f"unsupported TOOL keys: {', '.join(unknown_keys)}")
505
+
506
+ name = str(manifest.get("name") or "").strip()
507
+ if not name:
508
+ raise ValueError("TOOL.name must be a non-empty string")
509
+ normalized_name = name.casefold()
510
+ if normalized_name in _RESERVED_EXACT_NAMES:
511
+ raise ValueError(f"TOOL.name collides with reserved host tool name: {name}")
512
+ if normalized_name in reserved_names:
513
+ raise ValueError(f"TOOL.name collides with built-in tool name: {name}")
514
+ for prefix in _RESERVED_NAME_PREFIXES:
515
+ if normalized_name.startswith(prefix):
516
+ raise ValueError(f"TOOL.name uses reserved prefix '{prefix}': {name}")
517
+
518
+ description = str(manifest.get("description") or "").strip()
519
+ if not description:
520
+ raise ValueError("TOOL.description must be a non-empty string")
521
+ if len(description) > _MAX_DESCRIPTION_CHARS:
522
+ raise ValueError("TOOL.description is too long")
523
+
524
+ manifest_version = _validate_manifest_version(manifest.get("manifest_version", 1))
525
+ input_schema = _validate_input_schema(manifest.get("input_schema"))
526
+ output_schema = _validate_output_schema(manifest.get("output_schema"))
527
+ timeout_s = _coerce_timeout(manifest.get("timeout_s", _DEFAULT_TIMEOUT_S))
528
+ required_env = _validate_required_env(manifest.get("required_env", ()))
529
+ capabilities = _validate_capabilities(
530
+ manifest.get("capabilities", {}),
531
+ required_env=required_env,
532
+ )
533
+ enabled_in = _validate_enabled_in(manifest.get("enabled_in", _DEFAULT_ENABLED_IN))
534
+ isolation = _validate_isolation(manifest.get("isolation", "subprocess"))
535
+ file_hash = hashlib.sha256(raw_bytes).hexdigest()
536
+ missing_env = tuple(
537
+ name for name in required_env if not str(env_get_from(env, name) or "").strip()
538
+ )
539
+
540
+ return CustomToolSpec(
541
+ name=name,
542
+ description=description,
543
+ input_schema=input_schema,
544
+ manifest_version=manifest_version,
545
+ capabilities=capabilities,
546
+ output_schema=output_schema,
547
+ timeout_s=timeout_s,
548
+ required_env=required_env,
549
+ enabled_in=enabled_in,
550
+ isolation=isolation,
551
+ source_scope=source_scope,
552
+ source_path=source_path.resolve(),
553
+ relative_tool_path=relative_tool_path,
554
+ file_hash=file_hash,
555
+ missing_env=missing_env,
556
+ )
557
+
558
+
559
+ def _validate_manifest_version(value: Any) -> int:
560
+ if isinstance(value, bool) or not isinstance(value, int):
561
+ raise ValueError("TOOL.manifest_version must be an integer")
562
+ if value != _SUPPORTED_MANIFEST_VERSION:
563
+ raise ValueError(f"unsupported TOOL.manifest_version: {value}")
564
+ return value
565
+
566
+
567
+ def _validate_input_schema(value: Any) -> dict[str, Any]:
568
+ if not isinstance(value, dict):
569
+ raise ValueError("TOOL.input_schema must be a JSON object")
570
+ if value.get("type") != "object":
571
+ raise ValueError("TOOL.input_schema root type must be 'object'")
572
+ properties = value.get("properties", {})
573
+ if not isinstance(properties, dict):
574
+ raise ValueError("TOOL.input_schema.properties must be an object")
575
+ required = value.get("required", [])
576
+ if not isinstance(required, list) or any(not isinstance(item, str) for item in required):
577
+ raise ValueError("TOOL.input_schema.required must be an array of strings")
578
+ try:
579
+ schema_bytes = _stable_json_size(value)
580
+ except TypeError as exc:
581
+ raise ValueError(f"TOOL.input_schema must be JSON-serializable: {exc}") from exc
582
+ if schema_bytes > _MAX_SCHEMA_BYTES:
583
+ raise ValueError("TOOL.input_schema is too large")
584
+ return copy.deepcopy(value)
585
+
586
+
587
+ def _validate_output_schema(value: Any) -> dict[str, Any] | None:
588
+ if value is None:
589
+ return None
590
+ if not isinstance(value, dict):
591
+ raise ValueError("TOOL.output_schema must be a JSON object")
592
+ if value.get("type") != "object":
593
+ raise ValueError("TOOL.output_schema root type must be 'object'")
594
+ try:
595
+ schema_bytes = _stable_json_size(value)
596
+ except TypeError as exc:
597
+ raise ValueError(f"TOOL.output_schema must be JSON-serializable: {exc}") from exc
598
+ if schema_bytes > _MAX_SCHEMA_BYTES:
599
+ raise ValueError("TOOL.output_schema is too large")
600
+ return copy.deepcopy(value)
601
+
602
+
603
+ def _validate_capabilities(
604
+ value: Any,
605
+ *,
606
+ required_env: tuple[str, ...],
607
+ ) -> CustomToolCapabilities:
608
+ if value in (None, ()):
609
+ value = {}
610
+ if not isinstance(value, dict):
611
+ raise ValueError("TOOL.capabilities must be a JSON object")
612
+ allowed_keys = {
613
+ "read_only",
614
+ "destructive",
615
+ "network_access",
616
+ "network_hosts",
617
+ "network",
618
+ "filesystem",
619
+ "filesystem_read_scope",
620
+ "filesystem_write_scope",
621
+ "process_spawn",
622
+ "secret_refs",
623
+ }
624
+ unknown_keys = sorted(set(value) - allowed_keys)
625
+ if unknown_keys:
626
+ raise ValueError(f"unsupported TOOL.capabilities keys: {', '.join(unknown_keys)}")
627
+
628
+ filesystem = value.get("filesystem", {})
629
+ if filesystem in (None, ()):
630
+ filesystem = {}
631
+ if not isinstance(filesystem, dict):
632
+ raise ValueError("TOOL.capabilities.filesystem must be a JSON object")
633
+ filesystem_allowed = {"read", "write", "read_scope", "write_scope"}
634
+ filesystem_unknown = sorted(set(filesystem) - filesystem_allowed)
635
+ if filesystem_unknown:
636
+ raise ValueError(
637
+ f"unsupported TOOL.capabilities.filesystem keys: {', '.join(filesystem_unknown)}"
638
+ )
639
+
640
+ read_only = _coerce_bool(value.get("read_only", False), "TOOL.capabilities.read_only")
641
+ destructive = _coerce_bool(value.get("destructive", False), "TOOL.capabilities.destructive")
642
+ if read_only and destructive:
643
+ raise ValueError("TOOL.capabilities cannot be both read_only and destructive")
644
+
645
+ network_access = _validate_enum_value(
646
+ value.get("network_access", value.get("network", "unspecified")),
647
+ allowed=_VALID_NETWORK_ACCESS_VALUES,
648
+ label="TOOL.capabilities.network_access",
649
+ )
650
+ network_hosts = _validate_network_hosts(value.get("network_hosts", ()))
651
+ if network_access == "restricted" and not network_hosts:
652
+ raise ValueError(
653
+ "TOOL.capabilities.network_hosts is required when network_access is restricted"
654
+ )
655
+ if network_access != "restricted" and network_hosts:
656
+ raise ValueError(
657
+ "TOOL.capabilities.network_hosts is only valid with restricted network access"
658
+ )
659
+ filesystem_read_scope = _validate_enum_value(
660
+ value.get(
661
+ "filesystem_read_scope",
662
+ filesystem.get("read", filesystem.get("read_scope", "unspecified")),
663
+ ),
664
+ allowed=_VALID_FILESYSTEM_SCOPE_VALUES,
665
+ label="TOOL.capabilities.filesystem_read_scope",
666
+ )
667
+ filesystem_write_scope = _validate_enum_value(
668
+ value.get(
669
+ "filesystem_write_scope",
670
+ filesystem.get("write", filesystem.get("write_scope", "unspecified")),
671
+ ),
672
+ allowed=_VALID_FILESYSTEM_SCOPE_VALUES,
673
+ label="TOOL.capabilities.filesystem_write_scope",
674
+ )
675
+ if read_only and filesystem_write_scope not in {"none", "unspecified"}:
676
+ raise ValueError(
677
+ "TOOL.capabilities.read_only requires filesystem_write_scope to be none or unspecified"
678
+ )
679
+
680
+ process_spawn = _validate_enum_value(
681
+ value.get("process_spawn", "unspecified"),
682
+ allowed=_VALID_PROCESS_SPAWN_VALUES,
683
+ label="TOOL.capabilities.process_spawn",
684
+ )
685
+ secret_refs = _validate_secret_refs(value.get("secret_refs", ()))
686
+ if required_env:
687
+ secret_refs = _dedupe_strings([*secret_refs, *required_env])
688
+
689
+ return CustomToolCapabilities(
690
+ read_only=read_only,
691
+ destructive=destructive,
692
+ network_access=network_access,
693
+ network_hosts=network_hosts,
694
+ filesystem_read_scope=filesystem_read_scope,
695
+ filesystem_write_scope=filesystem_write_scope,
696
+ process_spawn=process_spawn,
697
+ secret_refs=secret_refs,
698
+ )
699
+
700
+
701
+ def _coerce_bool(value: Any, label: str) -> bool:
702
+ if not isinstance(value, bool):
703
+ raise ValueError(f"{label} must be true or false")
704
+ return value
705
+
706
+
707
+ def _validate_enum_value(value: Any, *, allowed: set[str], label: str) -> str:
708
+ normalized = str(value or "").strip().lower()
709
+ if normalized not in allowed:
710
+ raise ValueError(f"{label} must be one of: {', '.join(sorted(allowed))}")
711
+ return normalized
712
+
713
+
714
+ def _validate_secret_refs(value: Any) -> tuple[str, ...]:
715
+ if value in (None, ()):
716
+ return ()
717
+ if not isinstance(value, list):
718
+ raise ValueError("TOOL.capabilities.secret_refs must be an array of env var names")
719
+ names: list[str] = []
720
+ for item in value:
721
+ name = str(item or "").strip()
722
+ if not name:
723
+ raise ValueError("TOOL.capabilities.secret_refs cannot contain empty values")
724
+ if not _ENV_NAME_RE.fullmatch(name):
725
+ raise ValueError(f"TOOL.capabilities.secret_refs contains invalid env var name: {name}")
726
+ names.append(name)
727
+ if len(names) > _MAX_SECRET_REFS:
728
+ raise ValueError("TOOL.capabilities.secret_refs contains too many entries")
729
+ return _dedupe_strings(names)
730
+
731
+
732
+ def _validate_network_hosts(value: Any) -> tuple[str, ...]:
733
+ if value in (None, ()):
734
+ return ()
735
+ if not isinstance(value, list):
736
+ raise ValueError("TOOL.capabilities.network_hosts must be an array of host strings")
737
+ hosts: list[str] = []
738
+ for item in value:
739
+ if not isinstance(item, str):
740
+ raise ValueError("TOOL.capabilities.network_hosts must contain only strings")
741
+ host = item.strip()
742
+ if not host:
743
+ raise ValueError("TOOL.capabilities.network_hosts cannot contain empty values")
744
+ if "*" in host:
745
+ raise ValueError("TOOL.capabilities.network_hosts does not support wildcards")
746
+ if any(char.isspace() for char in host) or "/" in host:
747
+ raise ValueError(
748
+ "TOOL.capabilities.network_hosts entries must be exact host or IP strings"
749
+ )
750
+ hosts.append(host)
751
+ if len(hosts) > _MAX_NETWORK_HOSTS:
752
+ raise ValueError("TOOL.capabilities.network_hosts contains too many entries")
753
+ return _dedupe_strings(hosts)
754
+
755
+
756
+ def _dedupe_strings(values: list[str]) -> tuple[str, ...]:
757
+ result: list[str] = []
758
+ seen: set[str] = set()
759
+ for value in values:
760
+ if value in seen:
761
+ continue
762
+ seen.add(value)
763
+ result.append(value)
764
+ return tuple(result)
765
+
766
+
767
+ def _validate_required_env(value: Any) -> tuple[str, ...]:
768
+ if value in (None, ()):
769
+ return ()
770
+ if not isinstance(value, list):
771
+ raise ValueError("TOOL.required_env must be an array of env var names")
772
+ names: list[str] = []
773
+ seen: set[str] = set()
774
+ for item in value:
775
+ name = str(item or "").strip()
776
+ if not name:
777
+ raise ValueError("TOOL.required_env cannot contain empty values")
778
+ if not _ENV_NAME_RE.fullmatch(name):
779
+ raise ValueError(f"TOOL.required_env contains invalid env var name: {name}")
780
+ if name in seen:
781
+ continue
782
+ seen.add(name)
783
+ names.append(name)
784
+ if len(names) > _MAX_REQUIRED_ENV_VARS:
785
+ raise ValueError("TOOL.required_env contains too many entries")
786
+ return tuple(names)
787
+
788
+
789
+ def _validate_enabled_in(value: Any) -> tuple[str, ...]:
790
+ if not isinstance(value, list) and not isinstance(value, tuple):
791
+ raise ValueError("TOOL.enabled_in must be an array of runtime kinds")
792
+ allowed = set(runtime_kind_values())
793
+ items: list[str] = []
794
+ seen: set[str] = set()
795
+ for item in value:
796
+ runtime = str(item or "").strip().lower()
797
+ if runtime not in allowed:
798
+ raise ValueError(
799
+ "TOOL.enabled_in contains invalid runtime kind: "
800
+ f"{item!r}. Expected one of: {', '.join(runtime_kind_values())}"
801
+ )
802
+ if runtime in seen:
803
+ continue
804
+ seen.add(runtime)
805
+ items.append(runtime)
806
+ if not items:
807
+ raise ValueError("TOOL.enabled_in cannot be empty")
808
+ return tuple(items)
809
+
810
+
811
+ def _validate_isolation(value: Any) -> str:
812
+ isolation = str(value or "").strip().lower()
813
+ if isolation not in _VALID_ISOLATION_VALUES:
814
+ raise ValueError("TOOL.isolation must be subprocess; inprocess is not supported")
815
+ return isolation
816
+
817
+
818
+ def _coerce_timeout(value: Any) -> float:
819
+ try:
820
+ timeout = float(value)
821
+ except (TypeError, ValueError) as exc:
822
+ raise ValueError("TOOL.timeout_s must be a positive number") from exc
823
+ if timeout <= 0:
824
+ raise ValueError("TOOL.timeout_s must be positive")
825
+ if timeout > _MAX_TIMEOUT_S:
826
+ raise ValueError(f"TOOL.timeout_s must be <= {_MAX_TIMEOUT_S}")
827
+ return timeout
828
+
829
+
830
+ def _spec_relative_tool_path(
831
+ path: Path,
832
+ root: Path,
833
+ owning_base: Path,
834
+ source_scope: str,
835
+ ) -> str:
836
+ if source_scope == "project":
837
+ return _issue_relative_tool_path(path, owning_base=owning_base, source_scope=source_scope)
838
+ return path.resolve().relative_to(root.resolve()).as_posix()
839
+
840
+
841
+ def _issue_relative_tool_path(
842
+ path: Path,
843
+ *,
844
+ owning_base: Path,
845
+ source_scope: str,
846
+ ) -> str:
847
+ try:
848
+ return path.relative_to(owning_base).as_posix()
849
+ except ValueError:
850
+ scope_root = _scope_root_label(source_scope)
851
+ name = path.name.strip()
852
+ if not name or name == Path(scope_root).name:
853
+ return scope_root
854
+ return f"{scope_root}/{name}"
855
+
856
+
857
+ def _scope_root_label(source_scope: str) -> str:
858
+ return ".alysis/tools" if source_scope == "project" else "tools"
859
+
860
+
861
+ def _resolved_path_is_within_scope(path: Path, owning_base: Path) -> bool:
862
+ try:
863
+ path.resolve().relative_to(owning_base)
864
+ except ValueError:
865
+ return False
866
+ return True
867
+
868
+
869
+ def _path_escape_message(
870
+ *,
871
+ resolved_path: Path,
872
+ owning_base: Path,
873
+ source_scope: str,
874
+ kind: str,
875
+ ) -> str:
876
+ scope_label = "workspace-owned" if source_scope == "project" else "config-owned"
877
+ return (
878
+ f"resolved {kind} is outside the {scope_label} custom tools scope: "
879
+ f"{resolved_path} (expected under {owning_base})"
880
+ )
881
+
882
+
883
+ def _tool_issue(
884
+ *,
885
+ source_scope: str,
886
+ source_path: Path,
887
+ relative_tool_path: str,
888
+ code: str,
889
+ message: str,
890
+ tool_name: str = "",
891
+ ) -> CustomToolIssue:
892
+ return CustomToolIssue(
893
+ source_scope=source_scope,
894
+ source_path=source_path.resolve(),
895
+ relative_tool_path=relative_tool_path,
896
+ code=code,
897
+ message=message,
898
+ tool_name=tool_name,
899
+ )
900
+
901
+
902
+ def _stable_json_size(value: object) -> int:
903
+ return len(json.dumps(value, ensure_ascii=True, sort_keys=True, separators=(",", ":")))