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,447 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from dataclasses import dataclass, field
5
+ from typing import Any, Literal, cast
6
+
7
+ from pydantic import (
8
+ BaseModel,
9
+ ConfigDict,
10
+ Field,
11
+ StrictBool,
12
+ StrictInt,
13
+ StrictStr,
14
+ field_validator,
15
+ model_validator,
16
+ )
17
+
18
+ from ..runtime_kind import normalize_runtime_kind
19
+
20
+ CanonicalHookEventName = Literal[
21
+ "PreToolUse",
22
+ "PostToolUse",
23
+ "PostWrite",
24
+ "SessionEnd",
25
+ "SessionStart",
26
+ "TurnComplete",
27
+ "UserPromptSubmit",
28
+ "Notification",
29
+ "PreCompact",
30
+ "SubagentStop",
31
+ ]
32
+ HookEventName = Literal[
33
+ "PreToolUse",
34
+ "PostToolUse",
35
+ "PostWrite",
36
+ "SessionEnd",
37
+ "SessionStart",
38
+ "TurnComplete",
39
+ "Stop",
40
+ "UserPromptSubmit",
41
+ "Notification",
42
+ "PreCompact",
43
+ "SubagentStop",
44
+ ]
45
+ HookFailurePolicy = Literal["warn", "continue", "block"]
46
+ HookSessionSource = Literal["startup", "resume", "fork"]
47
+
48
+ _VALID_HOOK_EVENTS: set[str] = {
49
+ "PreToolUse",
50
+ "PostToolUse",
51
+ "PostWrite",
52
+ "SessionEnd",
53
+ "SessionStart",
54
+ "TurnComplete",
55
+ "Stop",
56
+ "UserPromptSubmit",
57
+ "Notification",
58
+ "PreCompact",
59
+ "SubagentStop",
60
+ }
61
+ _CANONICAL_EVENT_NAME_BY_ALIAS: dict[str, CanonicalHookEventName] = {
62
+ "PreToolUse": "PreToolUse",
63
+ "PostToolUse": "PostToolUse",
64
+ "PostWrite": "PostWrite",
65
+ "SessionEnd": "SessionEnd",
66
+ "SessionStart": "SessionStart",
67
+ "TurnComplete": "TurnComplete",
68
+ "Stop": "TurnComplete",
69
+ "UserPromptSubmit": "UserPromptSubmit",
70
+ "Notification": "Notification",
71
+ "PreCompact": "PreCompact",
72
+ "SubagentStop": "SubagentStop",
73
+ }
74
+ _HOOK_ID_RE = re.compile(r"^[a-z0-9][a-z0-9._-]*$")
75
+ _VALID_FAILURE_POLICIES = {"warn", "continue", "block"}
76
+ _VALID_SESSION_SOURCES: frozenset[str] = frozenset({"startup", "resume", "fork"})
77
+
78
+
79
+ def canonicalize_hook_event_name(event_name: str) -> CanonicalHookEventName:
80
+ normalized = str(event_name or "").strip()
81
+ if normalized not in _VALID_HOOK_EVENTS:
82
+ raise ValueError(f"unsupported hook event: {event_name}")
83
+ return _CANONICAL_EVENT_NAME_BY_ALIAS[normalized]
84
+
85
+
86
+ def _normalize_optional_text(value: object, *, field_name: str) -> str:
87
+ if not isinstance(value, str):
88
+ raise TypeError(f"{field_name} must be a string.")
89
+ return value.strip()
90
+
91
+
92
+ def _normalize_timeout(value: object, *, field_name: str) -> float:
93
+ if isinstance(value, bool):
94
+ raise TypeError(f"{field_name} must be a positive number.")
95
+ if not isinstance(value, int | float):
96
+ raise TypeError(f"{field_name} must be a positive number.")
97
+ timeout = float(value)
98
+ if timeout <= 0:
99
+ raise ValueError(f"{field_name} must be > 0.")
100
+ return timeout
101
+
102
+
103
+ def _normalize_optional_metadata_text(value: object, *, field_name: str) -> str | None:
104
+ if value is None:
105
+ return None
106
+ text = _normalize_optional_text(value, field_name=field_name)
107
+ return text or None
108
+
109
+
110
+ def _normalize_priority(value: object, *, field_name: str) -> int:
111
+ if isinstance(value, bool) or not isinstance(value, int):
112
+ raise TypeError(f"{field_name} must be an integer.")
113
+ return value
114
+
115
+
116
+ def _normalize_failure_policy(value: object, *, field_name: str) -> HookFailurePolicy:
117
+ text = _normalize_optional_text(value, field_name=field_name).lower()
118
+ if text not in _VALID_FAILURE_POLICIES:
119
+ raise ValueError(f"{field_name} must be one of: warn, continue, block.")
120
+ return cast(HookFailurePolicy, text)
121
+
122
+
123
+ def _normalize_runtime_kinds(value: object) -> list[str] | None:
124
+ if value is None:
125
+ return None
126
+ items = [value] if isinstance(value, str) else value
127
+ if not isinstance(items, list | tuple):
128
+ raise TypeError("runtimeKinds must be a string or list of runtime kinds.")
129
+ normalized: list[str] = []
130
+ seen: set[str] = set()
131
+ for item in items:
132
+ kind = normalize_runtime_kind(item).value
133
+ if kind in seen:
134
+ continue
135
+ seen.add(kind)
136
+ normalized.append(kind)
137
+ return normalized or None
138
+
139
+
140
+ def _normalize_session_sources(value: object) -> list[HookSessionSource] | None:
141
+ if value is None:
142
+ return None
143
+ items = [value] if isinstance(value, str) else value
144
+ if not isinstance(items, list | tuple):
145
+ raise TypeError("sessionSource must be a string or list of strings.")
146
+ normalized: list[HookSessionSource] = []
147
+ seen: set[str] = set()
148
+ for item in items:
149
+ if not isinstance(item, str):
150
+ raise TypeError("sessionSource entries must be strings.")
151
+ source = item.strip().lower()
152
+ if source not in _VALID_SESSION_SOURCES:
153
+ raise ValueError("sessionSource must contain only: startup, resume, fork.")
154
+ if source in seen:
155
+ continue
156
+ seen.add(source)
157
+ normalized.append(cast(HookSessionSource, source))
158
+ return normalized or None
159
+
160
+
161
+ class _StrictModel(BaseModel):
162
+ model_config = ConfigDict(extra="forbid", populate_by_name=True)
163
+
164
+
165
+ _ENV_KEY_RE = re.compile(r"^[A-Z][A-Z0-9_]*$")
166
+
167
+
168
+ class CommandHookSpec(_StrictModel):
169
+ type: Literal["command"] = "command"
170
+ command: StrictStr
171
+ timeout: float = 10.0
172
+ enabled: StrictBool = True
173
+ id: StrictStr | None = None
174
+ description: StrictStr | None = None
175
+ priority: StrictInt = 0
176
+ failure_policy: HookFailurePolicy = Field(default="warn", alias="failurePolicy")
177
+ runtime_kinds: list[StrictStr] | None = Field(default=None, alias="runtimeKinds")
178
+ session_source: list[HookSessionSource] | None = Field(default=None, alias="sessionSource")
179
+ env_passthrough: Literal["all", "safe", "explicit"] = Field(
180
+ default="safe", alias="envPassthrough"
181
+ )
182
+ env_allow: list[StrictStr] | None = Field(default=None, alias="envAllow")
183
+ env: dict[StrictStr, StrictStr] | None = None
184
+ receives_full_payload: StrictBool = Field(default=False, alias="receivesFullPayload")
185
+
186
+ @model_validator(mode="before")
187
+ @classmethod
188
+ def _apply_timeout_ms_alias(cls, value: object) -> object:
189
+ if not isinstance(value, dict):
190
+ return value
191
+ raw = dict(value)
192
+ timeout_ms = raw.pop("timeoutMs", None)
193
+ if timeout_ms is not None:
194
+ if "timeout" in raw:
195
+ raise ValueError("Specify only one of timeout or timeoutMs.")
196
+ raw["timeout"] = _normalize_timeout(timeout_ms, field_name="timeoutMs") / 1000.0
197
+ return raw
198
+
199
+ @field_validator("command", mode="before")
200
+ @classmethod
201
+ def _validate_command(cls, value: object) -> str:
202
+ command = _normalize_optional_text(value, field_name="command")
203
+ if not command:
204
+ raise ValueError("command cannot be empty.")
205
+ return command
206
+
207
+ @field_validator("timeout", mode="before")
208
+ @classmethod
209
+ def _validate_timeout(cls, value: object) -> float:
210
+ return _normalize_timeout(value, field_name="timeout")
211
+
212
+ @field_validator("id", mode="before")
213
+ @classmethod
214
+ def _validate_id(cls, value: object) -> str | None:
215
+ hook_id = _normalize_optional_metadata_text(value, field_name="id")
216
+ if hook_id is None:
217
+ return None
218
+ if not _HOOK_ID_RE.fullmatch(hook_id):
219
+ raise ValueError("id must match ^[a-z0-9][a-z0-9._-]*$.")
220
+ return hook_id
221
+
222
+ @field_validator("description", mode="before")
223
+ @classmethod
224
+ def _validate_description(cls, value: object) -> str | None:
225
+ return _normalize_optional_metadata_text(value, field_name="description")
226
+
227
+ @field_validator("priority", mode="before")
228
+ @classmethod
229
+ def _validate_priority(cls, value: object) -> int:
230
+ return _normalize_priority(value, field_name="priority")
231
+
232
+ @field_validator("failure_policy", mode="before")
233
+ @classmethod
234
+ def _validate_failure_policy(cls, value: object) -> HookFailurePolicy:
235
+ return _normalize_failure_policy(value, field_name="failurePolicy")
236
+
237
+ @field_validator("runtime_kinds", mode="before")
238
+ @classmethod
239
+ def _validate_runtime_kinds(cls, value: object) -> list[str] | None:
240
+ return _normalize_runtime_kinds(value)
241
+
242
+ @field_validator("session_source", mode="before")
243
+ @classmethod
244
+ def _validate_session_source(cls, value: object) -> list[HookSessionSource] | None:
245
+ return _normalize_session_sources(value)
246
+
247
+ @field_validator("env_passthrough", mode="before")
248
+ @classmethod
249
+ def _validate_env_passthrough(cls, value: object) -> str:
250
+ if value is None:
251
+ return "safe"
252
+ if not isinstance(value, str):
253
+ raise TypeError("envPassthrough must be a string.")
254
+ lowered = value.strip().lower()
255
+ if lowered not in {"all", "safe", "explicit"}:
256
+ raise ValueError("envPassthrough must be one of: all, safe, explicit.")
257
+ return lowered
258
+
259
+ @field_validator("env_allow", mode="before")
260
+ @classmethod
261
+ def _validate_env_allow(cls, value: object) -> list[str] | None:
262
+ if value is None:
263
+ return None
264
+ if not isinstance(value, list):
265
+ raise TypeError("envAllow must be a list of environment variable names.")
266
+ normalized: list[str] = []
267
+ seen: set[str] = set()
268
+ for item in value:
269
+ if not isinstance(item, str):
270
+ raise TypeError("envAllow entries must be strings.")
271
+ name = item.strip()
272
+ if not _ENV_KEY_RE.fullmatch(name):
273
+ raise ValueError("envAllow entries must match ^[A-Z][A-Z0-9_]*$.")
274
+ if name in seen:
275
+ continue
276
+ seen.add(name)
277
+ normalized.append(name)
278
+ return normalized or None
279
+
280
+ @field_validator("env", mode="before")
281
+ @classmethod
282
+ def _validate_env_dict(cls, value: object) -> dict[str, str] | None:
283
+ if value is None:
284
+ return None
285
+ if not isinstance(value, dict):
286
+ raise TypeError("env must be a mapping of environment variable names to strings.")
287
+ normalized: dict[str, str] = {}
288
+ for key, raw_value in value.items():
289
+ if not isinstance(key, str):
290
+ raise TypeError("env keys must be strings.")
291
+ if not _ENV_KEY_RE.fullmatch(key):
292
+ raise ValueError("env keys must match ^[A-Z][A-Z0-9_]*$.")
293
+ if not isinstance(raw_value, str):
294
+ raise TypeError("env values must be strings.")
295
+ normalized[key] = raw_value
296
+ return normalized or None
297
+
298
+
299
+ class HookMatcherGroup(_StrictModel):
300
+ matcher: StrictStr = ""
301
+ hooks: list[CommandHookSpec] = Field(default_factory=list)
302
+ enabled: StrictBool = True
303
+
304
+ @field_validator("matcher", mode="before")
305
+ @classmethod
306
+ def _validate_matcher(cls, value: object) -> str:
307
+ if value is None:
308
+ return ""
309
+ return _normalize_optional_text(value, field_name="matcher")
310
+
311
+ @model_validator(mode="after")
312
+ def _validate_unique_hook_ids(self) -> HookMatcherGroup:
313
+ seen: set[str] = set()
314
+ for hook in self.hooks:
315
+ if hook.id is None:
316
+ continue
317
+ if hook.id in seen:
318
+ raise ValueError(f"duplicate hook id in matcher group: {hook.id}")
319
+ seen.add(hook.id)
320
+ return self
321
+
322
+
323
+ class HookConfigFile(_StrictModel):
324
+ schema_version: int = 1
325
+ hooks: dict[str, list[HookMatcherGroup]] = Field(default_factory=dict)
326
+
327
+ @field_validator("schema_version")
328
+ @classmethod
329
+ def _validate_schema_version(cls, value: int) -> int:
330
+ if value != 1:
331
+ raise ValueError("schema_version must be 1.")
332
+ return value
333
+
334
+ @field_validator("hooks")
335
+ @classmethod
336
+ def _validate_hooks(cls, value: dict[str, list[HookMatcherGroup]]) -> dict[str, Any]:
337
+ normalized: dict[str, list[HookMatcherGroup]] = {}
338
+ for event_name, groups in value.items():
339
+ canonical = canonicalize_hook_event_name(event_name)
340
+ normalized.setdefault(canonical, []).extend(groups)
341
+ return normalized
342
+
343
+
344
+ @dataclass(frozen=True)
345
+ class HookDispatchResult:
346
+ blocked: bool = False
347
+ reason: str = ""
348
+ modified_input: dict[str, Any] | None = None
349
+ modified_prompt: str | None = None
350
+ additional_system_messages: tuple[str, ...] = ()
351
+ additional_user_messages: tuple[str, ...] = ()
352
+ system_notices: tuple[str, ...] = ()
353
+ stop_reason: str = ""
354
+ permission_decision: str = ""
355
+ permission_decision_reason: str = ""
356
+ ask_requested: bool = False
357
+ ask_reason: str = ""
358
+
359
+
360
+ @dataclass(frozen=True)
361
+ class ParsedHookOutput:
362
+ blocked: bool = False
363
+ reason: str = ""
364
+ modified_input: dict[str, Any] | None = None
365
+ modified_prompt: str | None = None
366
+ additional_system_messages: tuple[str, ...] = ()
367
+ additional_user_messages: tuple[str, ...] = ()
368
+ system_notices: tuple[str, ...] = ()
369
+ halt: bool = False
370
+ stop_reason: str = ""
371
+ allow_short_circuit: bool = False
372
+ suppress_output: bool = False
373
+ permission_decision: str = ""
374
+ permission_decision_reason: str = ""
375
+ stdout_chars: int = 0
376
+ stderr_chars: int = 0
377
+ status: str = "ok"
378
+ warnings: tuple[str, ...] = ()
379
+ ask_requested: bool = False
380
+ ask_reason: str = ""
381
+
382
+
383
+ @dataclass(frozen=True)
384
+ class HookInvocationContext:
385
+ event_name: CanonicalHookEventName
386
+ source_path: str
387
+ source_scope: str
388
+ matcher: str
389
+ hook_id: str
390
+ priority: int
391
+ failure_policy: HookFailurePolicy
392
+ command: str
393
+ timeout_s: float
394
+ trusted: bool = True
395
+ returncode: int | None = None
396
+ blocked: bool = False
397
+ modified_input: bool = False
398
+ modified_input_fields: tuple[str, ...] = ()
399
+ modified_prompt: bool = False
400
+ modified_prompt_chars: int = 0
401
+ additional_system_message_count: int = 0
402
+ additional_user_message_count: int = 0
403
+ system_notices_count: int = 0
404
+ halt_requested: bool = False
405
+ allow_short_circuited: bool = False
406
+ suppress_output: bool = False
407
+ permission_decision: str = ""
408
+ stop_reason: str = ""
409
+ stdout_chars: int = 0
410
+ stderr_chars: int = 0
411
+ duration_ms: int = 0
412
+ status: str = "ok"
413
+ warnings: tuple[str, ...] = ()
414
+ payload_truncated: bool = False
415
+ payload_bytes: int = 0
416
+
417
+
418
+ @dataclass
419
+ class MutableHookDispatchState:
420
+ blocked: bool = False
421
+ reason: str = ""
422
+ modified_input: dict[str, Any] | None = None
423
+ modified_prompt: str | None = None
424
+ additional_system_messages: list[str] = field(default_factory=list)
425
+ additional_user_messages: list[str] = field(default_factory=list)
426
+ system_notices: list[str] = field(default_factory=list)
427
+ stop_reason: str = ""
428
+ permission_decision: str = ""
429
+ permission_decision_reason: str = ""
430
+ ask_requested: bool = False
431
+ ask_reason: str = ""
432
+
433
+ def freeze(self) -> HookDispatchResult:
434
+ return HookDispatchResult(
435
+ blocked=self.blocked,
436
+ reason=self.reason,
437
+ modified_input=self.modified_input,
438
+ modified_prompt=self.modified_prompt,
439
+ additional_system_messages=tuple(self.additional_system_messages),
440
+ additional_user_messages=tuple(self.additional_user_messages),
441
+ system_notices=tuple(self.system_notices),
442
+ stop_reason=self.stop_reason,
443
+ permission_decision=self.permission_decision,
444
+ permission_decision_reason=self.permission_decision_reason,
445
+ ask_requested=self.ask_requested,
446
+ ask_reason=self.ask_reason,
447
+ )
@@ -0,0 +1,202 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ import os
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+
9
+ from ..atomic_io import atomic_write_json
10
+ from ..branding import canonical_user_config_dir, env_get
11
+
12
+ _TRUST_SCHEMA_VERSION = 1
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class ProjectHooksTrustKey:
17
+ workspace_root: str
18
+ relative_config_path: str
19
+ file_hash: str
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class ProjectHooksTrustState:
24
+ trusted_configs: tuple[ProjectHooksTrustKey, ...] = ()
25
+
26
+ def contains(self, key: ProjectHooksTrustKey) -> bool:
27
+ return key in set(self.trusted_configs)
28
+
29
+
30
+ def _config_dir() -> Path:
31
+ override = env_get("ALYSIS_CONFIG_DIR")
32
+ if override:
33
+ return Path(override)
34
+ return canonical_user_config_dir()
35
+
36
+
37
+ def trust_state_path(*, user_config_dir: Path | None = None) -> Path:
38
+ base = user_config_dir.expanduser().resolve() if user_config_dir is not None else _config_dir()
39
+ return base / "project_hooks_trust.json"
40
+
41
+
42
+ def _hash_file(path: Path) -> str:
43
+ return hashlib.sha256(path.read_bytes()).hexdigest()
44
+
45
+
46
+ def project_hooks_trust_key(*, workspace_root: Path, config_path: Path) -> ProjectHooksTrustKey:
47
+ resolved_workspace = workspace_root.expanduser().resolve()
48
+ resolved_path = config_path.expanduser().resolve()
49
+ if not resolved_path.is_file():
50
+ raise ValueError(f"Project hooks config not found: {resolved_path}")
51
+ try:
52
+ relative_config_path = os.fspath(resolved_path.relative_to(resolved_workspace))
53
+ except ValueError as exc:
54
+ raise ValueError(
55
+ f"Project hooks config must live under the workspace root: {resolved_path}"
56
+ ) from exc
57
+ return ProjectHooksTrustKey(
58
+ workspace_root=os.fspath(resolved_workspace),
59
+ relative_config_path=relative_config_path,
60
+ file_hash=_hash_file(resolved_path),
61
+ )
62
+
63
+
64
+ def load_trust_state(*, user_config_dir: Path | None = None) -> ProjectHooksTrustState:
65
+ path = trust_state_path(user_config_dir=user_config_dir)
66
+ if not path.exists():
67
+ return ProjectHooksTrustState()
68
+ try:
69
+ raw = json.loads(path.read_text(encoding="utf-8"))
70
+ except (OSError, json.JSONDecodeError, UnicodeDecodeError) as exc:
71
+ raise RuntimeError(f"Failed to read project hooks trust state: {path}") from exc
72
+ if not isinstance(raw, dict):
73
+ raise RuntimeError("Invalid project hooks trust state format.")
74
+ version = raw.get("schema_version", _TRUST_SCHEMA_VERSION)
75
+ if version != _TRUST_SCHEMA_VERSION:
76
+ raise RuntimeError(f"Unsupported project hooks trust schema version: {version}")
77
+ trusted_raw = raw.get("trusted_configs", [])
78
+ if not isinstance(trusted_raw, list):
79
+ raise RuntimeError("Invalid project hooks trust state: trusted_configs must be an array.")
80
+ trusted: list[ProjectHooksTrustKey] = []
81
+ for item in trusted_raw:
82
+ if not isinstance(item, dict):
83
+ continue
84
+ workspace_root = str(item.get("workspace_root") or "").strip()
85
+ relative_config_path = str(item.get("relative_config_path") or "").strip()
86
+ file_hash = str(item.get("file_hash") or "").strip()
87
+ if workspace_root and relative_config_path and file_hash:
88
+ trusted.append(
89
+ ProjectHooksTrustKey(
90
+ workspace_root=workspace_root,
91
+ relative_config_path=relative_config_path,
92
+ file_hash=file_hash,
93
+ )
94
+ )
95
+ trusted_sorted = tuple(
96
+ sorted(
97
+ set(trusted),
98
+ key=lambda key: (key.workspace_root, key.relative_config_path, key.file_hash),
99
+ )
100
+ )
101
+ return ProjectHooksTrustState(trusted_configs=trusted_sorted)
102
+
103
+
104
+ def save_trust_state(
105
+ state: ProjectHooksTrustState,
106
+ *,
107
+ user_config_dir: Path | None = None,
108
+ ) -> None:
109
+ path = trust_state_path(user_config_dir=user_config_dir)
110
+ path.parent.mkdir(parents=True, exist_ok=True)
111
+ payload = {
112
+ "schema_version": _TRUST_SCHEMA_VERSION,
113
+ "trusted_configs": [
114
+ {
115
+ "workspace_root": key.workspace_root,
116
+ "relative_config_path": key.relative_config_path,
117
+ "file_hash": key.file_hash,
118
+ }
119
+ for key in state.trusted_configs
120
+ ],
121
+ }
122
+ atomic_write_json(path, payload, ensure_ascii=True)
123
+
124
+
125
+ def trust_project_hooks_config(
126
+ *,
127
+ workspace_root: Path,
128
+ config_path: Path,
129
+ user_config_dir: Path | None = None,
130
+ ) -> ProjectHooksTrustState:
131
+ key = project_hooks_trust_key(workspace_root=workspace_root, config_path=config_path)
132
+ current = load_trust_state(user_config_dir=user_config_dir)
133
+ trusted = set(current.trusted_configs)
134
+ trusted.add(key)
135
+ updated = ProjectHooksTrustState(
136
+ trusted_configs=tuple(
137
+ sorted(
138
+ trusted,
139
+ key=lambda item: (item.workspace_root, item.relative_config_path, item.file_hash),
140
+ )
141
+ )
142
+ )
143
+ save_trust_state(updated, user_config_dir=user_config_dir)
144
+ return updated
145
+
146
+
147
+ def untrust_project_hooks_config(
148
+ *,
149
+ workspace_root: Path,
150
+ config_path: Path,
151
+ user_config_dir: Path | None = None,
152
+ ) -> ProjectHooksTrustState:
153
+ resolved_workspace = workspace_root.expanduser().resolve()
154
+ resolved_path = config_path.expanduser().resolve()
155
+ try:
156
+ relative_config_path = os.fspath(resolved_path.relative_to(resolved_workspace))
157
+ except ValueError as exc:
158
+ raise ValueError(
159
+ f"Project hooks config must live under the workspace root: {resolved_path}"
160
+ ) from exc
161
+ current = load_trust_state(user_config_dir=user_config_dir)
162
+ remaining = tuple(
163
+ key
164
+ for key in current.trusted_configs
165
+ if not (
166
+ key.workspace_root == os.fspath(resolved_workspace)
167
+ and key.relative_config_path == relative_config_path
168
+ )
169
+ )
170
+ updated = ProjectHooksTrustState(trusted_configs=remaining)
171
+ save_trust_state(updated, user_config_dir=user_config_dir)
172
+ return updated
173
+
174
+
175
+ def is_project_hooks_config_trusted(
176
+ *,
177
+ workspace_root: Path,
178
+ config_path: Path,
179
+ state: ProjectHooksTrustState | None = None,
180
+ user_config_dir: Path | None = None,
181
+ ) -> bool:
182
+ resolved_path = config_path.expanduser().resolve()
183
+ if not resolved_path.is_file():
184
+ return False
185
+ active_state = state if state is not None else load_trust_state(user_config_dir=user_config_dir)
186
+ return project_hooks_trust_key(
187
+ workspace_root=workspace_root,
188
+ config_path=config_path,
189
+ ) in set(active_state.trusted_configs)
190
+
191
+
192
+ __all__ = [
193
+ "ProjectHooksTrustKey",
194
+ "ProjectHooksTrustState",
195
+ "is_project_hooks_config_trusted",
196
+ "load_trust_state",
197
+ "project_hooks_trust_key",
198
+ "save_trust_state",
199
+ "trust_project_hooks_config",
200
+ "trust_state_path",
201
+ "untrust_project_hooks_config",
202
+ ]