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,1016 @@
1
+ from __future__ import annotations
2
+
3
+ import fnmatch
4
+ import hashlib
5
+ import json
6
+ import ntpath
7
+ import os
8
+ import posixpath
9
+ import re
10
+ import threading
11
+ import uuid
12
+ from collections.abc import Mapping, Sequence
13
+ from contextlib import contextmanager
14
+ from dataclasses import dataclass, field
15
+ from enum import Enum
16
+ from io import BufferedRandom
17
+ from pathlib import Path
18
+ from typing import Any, Literal
19
+
20
+ from .approval_scope import SUPPORTED_APPROVAL_SCOPES, ApprovalSessionScope
21
+ from .atomic_io import atomic_write_json
22
+ from .branding import canonical_user_config_dir, env_get
23
+
24
+ PERMISSION_POLICY_SCHEMA_VERSION = 1
25
+ _MAX_RULES = 1_000
26
+ _MAX_SELECTOR_LENGTH = 4_096
27
+ _MAX_SOURCE_LENGTH = 128
28
+ _RULE_ID_RE = re.compile(r"^pr_[a-f0-9]{32}$")
29
+ _SESSION_GRANT_ID_RE = re.compile(r"^sg_[a-f0-9]{32}$")
30
+ _WINDOWS_ABSOLUTE_RE = re.compile(r"^(?:[a-zA-Z]:[\\/]|[\\/]{2})")
31
+ _CONTROL_CHARACTER_RE = re.compile(r"[\x00-\x1f\x7f]")
32
+ _SHA256_RE = re.compile(r"^[a-f0-9]{64}$")
33
+ _POLICY_STORE_LOCK = threading.RLock()
34
+
35
+
36
+ def default_permission_policy_path() -> Path:
37
+ """Return the user-scoped policy path, honoring the standard config override."""
38
+
39
+ override = env_get("ALYSIS_CONFIG_DIR")
40
+ root = Path(override).expanduser() if override else canonical_user_config_dir()
41
+ return root / "permission_policy.json"
42
+
43
+
44
+ if os.name == "nt": # pragma: no cover - exercised on Windows
45
+ import msvcrt
46
+ else: # pragma: no cover - exercised on POSIX
47
+ import fcntl
48
+
49
+
50
+ class PolicyEffect(str, Enum):
51
+ ALLOW = "allow"
52
+ ASK = "ask"
53
+ DENY = "deny"
54
+
55
+
56
+ class PermissionPolicyError(RuntimeError):
57
+ """Base error that deliberately does not include policy selector contents."""
58
+
59
+
60
+ class PermissionPolicyValidationError(PermissionPolicyError):
61
+ pass
62
+
63
+
64
+ class PermissionPolicyCorruptError(PermissionPolicyError):
65
+ def __init__(self) -> None:
66
+ super().__init__("Permission policy is invalid or unreadable; access is denied.")
67
+
68
+
69
+ @dataclass(frozen=True, slots=True)
70
+ class PermissionRule:
71
+ id: str
72
+ effect: PolicyEffect
73
+ order: int
74
+ source: str
75
+ tool_pattern: str | None = None
76
+ path_pattern: str | None = None
77
+ command_pattern: str | None = field(default=None, repr=False)
78
+
79
+ def to_public_dict(self, *, reveal_command_pattern: bool = False) -> dict[str, Any]:
80
+ result: dict[str, Any] = {
81
+ "id": self.id,
82
+ "effect": self.effect.value,
83
+ "order": self.order,
84
+ "source": self.source,
85
+ "tool_pattern": self.tool_pattern,
86
+ "path_pattern": self.path_pattern,
87
+ "has_command_pattern": self.command_pattern is not None,
88
+ }
89
+ if reveal_command_pattern and self.command_pattern is not None:
90
+ result["command_pattern"] = self.command_pattern
91
+ return result
92
+
93
+ def _to_storage_dict(self) -> dict[str, Any]:
94
+ return {
95
+ "id": self.id,
96
+ "effect": self.effect.value,
97
+ "order": self.order,
98
+ "source": self.source,
99
+ "tool_pattern": self.tool_pattern,
100
+ "path_pattern": self.path_pattern,
101
+ "command_pattern": self.command_pattern,
102
+ }
103
+
104
+
105
+ @dataclass(frozen=True, slots=True)
106
+ class PermissionRequest:
107
+ tool_name: str
108
+ paths: tuple[str, ...] = ()
109
+ command: str | None = field(default=None, repr=False)
110
+ workspace_root: str | None = None
111
+ platform: Literal["auto", "windows", "posix"] = "auto"
112
+ sensitive: bool = False
113
+ external_directory: bool = False
114
+
115
+ @classmethod
116
+ def create(
117
+ cls,
118
+ tool_name: str,
119
+ *,
120
+ path: str | None = None,
121
+ paths: Sequence[str] = (),
122
+ command: str | None = None,
123
+ workspace_root: str | os.PathLike[str] | None = None,
124
+ platform: Literal["auto", "windows", "posix"] = "auto",
125
+ sensitive: bool = False,
126
+ external_directory: bool = False,
127
+ ) -> PermissionRequest:
128
+ combined_paths = ((path,) if path is not None else ()) + tuple(paths)
129
+ return cls(
130
+ tool_name=tool_name,
131
+ paths=combined_paths,
132
+ command=command,
133
+ workspace_root=os.fspath(workspace_root) if workspace_root is not None else None,
134
+ platform=platform,
135
+ sensitive=sensitive,
136
+ external_directory=external_directory,
137
+ )
138
+
139
+
140
+ @dataclass(frozen=True, slots=True)
141
+ class PermissionEvaluation:
142
+ decision: PolicyEffect
143
+ reason: str
144
+ matched_rule_id: str | None
145
+ matched_rule_source: str | None
146
+ specificity: int
147
+
148
+ @property
149
+ def allowed(self) -> bool:
150
+ return self.decision is PolicyEffect.ALLOW
151
+
152
+
153
+ @dataclass(frozen=True, slots=True)
154
+ class _NormalizedRequest:
155
+ tool_name: str
156
+ paths: tuple[str, ...]
157
+ command: str | None = field(repr=False)
158
+ platform: Literal["windows", "posix"]
159
+ sensitive: bool
160
+ external_directory: bool
161
+
162
+
163
+ class PermissionPolicy:
164
+ """Ordered, explainable permission rules with non-bypassable safety overrides."""
165
+
166
+ def __init__(
167
+ self,
168
+ rules: Sequence[PermissionRule] = (),
169
+ *,
170
+ sensitive_override: PolicyEffect | str = PolicyEffect.ASK,
171
+ external_directory_override: PolicyEffect | str = PolicyEffect.ASK,
172
+ ) -> None:
173
+ self._lock = threading.RLock()
174
+ self._rules = list(rules)
175
+ self.sensitive_override = _validate_safety_override(sensitive_override)
176
+ self.external_directory_override = _validate_safety_override(external_directory_override)
177
+ _validate_rule_collection(self._rules)
178
+
179
+ def list_rules(self) -> tuple[PermissionRule, ...]:
180
+ with self._lock:
181
+ return tuple(sorted(self._rules, key=lambda rule: rule.order))
182
+
183
+ def grant(
184
+ self,
185
+ effect: PolicyEffect | str,
186
+ *,
187
+ tool_pattern: str | None = None,
188
+ path_pattern: str | None = None,
189
+ command_pattern: str | None = None,
190
+ source: str = "user",
191
+ ) -> PermissionRule:
192
+ normalized_effect = _parse_effect(effect)
193
+ normalized_tool = _normalize_optional_tool_pattern(tool_pattern)
194
+ normalized_path = _normalize_optional_path_pattern(path_pattern)
195
+ normalized_command = _normalize_optional_command_pattern(command_pattern)
196
+ normalized_source = _validate_source(source)
197
+ if normalized_tool is None and normalized_path is None and normalized_command is None:
198
+ raise PermissionPolicyValidationError("A permission rule requires a selector.")
199
+ with self._lock:
200
+ if len(self._rules) >= _MAX_RULES:
201
+ raise PermissionPolicyValidationError("Permission policy contains too many rules.")
202
+ next_order = max((rule.order for rule in self._rules), default=-1) + 1
203
+ rule = PermissionRule(
204
+ id=f"pr_{uuid.uuid4().hex}",
205
+ effect=normalized_effect,
206
+ order=next_order,
207
+ source=normalized_source,
208
+ tool_pattern=normalized_tool,
209
+ path_pattern=normalized_path,
210
+ command_pattern=normalized_command,
211
+ )
212
+ self._rules.append(rule)
213
+ return rule
214
+
215
+ def revoke(self, rule_id: str) -> bool:
216
+ _validate_rule_id(rule_id)
217
+ with self._lock:
218
+ for index, rule in enumerate(self._rules):
219
+ if rule.id == rule_id:
220
+ del self._rules[index]
221
+ return True
222
+ return False
223
+
224
+ def set_safety_overrides(
225
+ self,
226
+ *,
227
+ sensitive: PolicyEffect | str | None = None,
228
+ external_directory: PolicyEffect | str | None = None,
229
+ ) -> None:
230
+ with self._lock:
231
+ if sensitive is not None:
232
+ self.sensitive_override = _validate_safety_override(sensitive)
233
+ if external_directory is not None:
234
+ self.external_directory_override = _validate_safety_override(external_directory)
235
+
236
+ def evaluate(self, request: PermissionRequest) -> PermissionEvaluation:
237
+ normalized = _normalize_request(request)
238
+ safety = self._evaluate_safety_overrides(normalized)
239
+ if safety is not None:
240
+ return safety
241
+
242
+ matches: list[tuple[int, int, int, PermissionRule]] = []
243
+ for rule in self.list_rules():
244
+ if not _rule_matches(rule, normalized):
245
+ continue
246
+ specificity = _rule_specificity(rule)
247
+ # At equal specificity, fail closed: deny, then ask, then allow.
248
+ effect_precedence = {
249
+ PolicyEffect.ALLOW: 0,
250
+ PolicyEffect.ASK: 1,
251
+ PolicyEffect.DENY: 2,
252
+ }[rule.effect]
253
+ # Earlier rules win only after specificity and safety precedence tie.
254
+ matches.append((specificity, effect_precedence, -rule.order, rule))
255
+ if not matches:
256
+ return PermissionEvaluation(
257
+ decision=PolicyEffect.ASK,
258
+ reason="no_matching_rule",
259
+ matched_rule_id=None,
260
+ matched_rule_source=None,
261
+ specificity=0,
262
+ )
263
+ specificity, _, _, winner = max(matches, key=lambda item: item[:3])
264
+ return PermissionEvaluation(
265
+ decision=winner.effect,
266
+ reason="matched_rule",
267
+ matched_rule_id=winner.id,
268
+ matched_rule_source=winner.source,
269
+ specificity=specificity,
270
+ )
271
+
272
+ def _evaluate_safety_overrides(
273
+ self, request: _NormalizedRequest
274
+ ) -> PermissionEvaluation | None:
275
+ candidates: list[tuple[PolicyEffect, str, str]] = []
276
+ if request.sensitive:
277
+ candidates.append(
278
+ (
279
+ self.sensitive_override,
280
+ "sensitive_resource_requires_approval",
281
+ "override:sensitive",
282
+ )
283
+ )
284
+ if request.external_directory:
285
+ candidates.append(
286
+ (
287
+ self.external_directory_override,
288
+ "external_directory_requires_approval",
289
+ "override:external_directory",
290
+ )
291
+ )
292
+ if not candidates:
293
+ return None
294
+ effect, reason, rule_id = max(
295
+ candidates,
296
+ key=lambda item: (item[0] is PolicyEffect.DENY, item[2] == "override:sensitive"),
297
+ )
298
+ return PermissionEvaluation(
299
+ decision=effect,
300
+ reason=reason,
301
+ matched_rule_id=rule_id,
302
+ matched_rule_source="builtin_safety",
303
+ specificity=2_147_483_647,
304
+ )
305
+
306
+ def _to_storage_payload(self) -> dict[str, Any]:
307
+ return {
308
+ "schema_version": PERMISSION_POLICY_SCHEMA_VERSION,
309
+ "safety_overrides": {
310
+ "sensitive": self.sensitive_override.value,
311
+ "external_directory": self.external_directory_override.value,
312
+ },
313
+ "rules": [rule._to_storage_dict() for rule in self.list_rules()],
314
+ }
315
+
316
+
317
+ class PermissionPolicyStore:
318
+ """Atomic JSON-backed policy store. Corruption always evaluates to deny."""
319
+
320
+ def __init__(self, path: Path) -> None:
321
+ self.path = Path(path)
322
+
323
+ def load(self) -> PermissionPolicy:
324
+ with _POLICY_STORE_LOCK:
325
+ return _load_policy_from_path(self.path)
326
+
327
+ def save(self, policy: PermissionPolicy) -> None:
328
+ with _locked_policy_state(self.path):
329
+ _save_policy_to_path(self.path, policy)
330
+
331
+ def list_rules(self) -> tuple[PermissionRule, ...]:
332
+ return self.load().list_rules()
333
+
334
+ def grant(
335
+ self,
336
+ effect: PolicyEffect | str,
337
+ *,
338
+ tool_pattern: str | None = None,
339
+ path_pattern: str | None = None,
340
+ command_pattern: str | None = None,
341
+ source: str = "user",
342
+ ) -> PermissionRule:
343
+ with _locked_policy_state(self.path):
344
+ policy = _load_policy_from_path(self.path)
345
+ rule = policy.grant(
346
+ effect,
347
+ tool_pattern=tool_pattern,
348
+ path_pattern=path_pattern,
349
+ command_pattern=command_pattern,
350
+ source=source,
351
+ )
352
+ _save_policy_to_path(self.path, policy)
353
+ return rule
354
+
355
+ def revoke(self, rule_id: str) -> bool:
356
+ with _locked_policy_state(self.path):
357
+ policy = _load_policy_from_path(self.path)
358
+ removed = policy.revoke(rule_id)
359
+ if removed:
360
+ _save_policy_to_path(self.path, policy)
361
+ return removed
362
+
363
+ def set_safety_overrides(
364
+ self,
365
+ *,
366
+ sensitive: PolicyEffect | str | None = None,
367
+ external_directory: PolicyEffect | str | None = None,
368
+ ) -> None:
369
+ with _locked_policy_state(self.path):
370
+ policy = _load_policy_from_path(self.path)
371
+ policy.set_safety_overrides(
372
+ sensitive=sensitive,
373
+ external_directory=external_directory,
374
+ )
375
+ _save_policy_to_path(self.path, policy)
376
+
377
+ def evaluate(self, request: PermissionRequest) -> PermissionEvaluation:
378
+ try:
379
+ return self.load().evaluate(request)
380
+ except PermissionPolicyCorruptError:
381
+ return PermissionEvaluation(
382
+ decision=PolicyEffect.DENY,
383
+ reason="permission_policy_corrupt",
384
+ matched_rule_id="override:corrupt_policy",
385
+ matched_rule_source="builtin_safety",
386
+ specificity=2_147_483_647,
387
+ )
388
+
389
+
390
+ @dataclass(frozen=True, slots=True)
391
+ class SessionPermissionGrant:
392
+ id: str
393
+ kind: str
394
+ scope_type: str
395
+ source: str
396
+ _scope_fingerprint: str = field(repr=False)
397
+
398
+ def to_public_dict(self) -> dict[str, str]:
399
+ return {
400
+ "id": self.id,
401
+ "kind": self.kind,
402
+ "scope_type": self.scope_type,
403
+ "source": self.source,
404
+ }
405
+
406
+
407
+ class InMemorySessionGrantAdapter:
408
+ """Inspectable opaque IDs around existing exact approval-session scopes."""
409
+
410
+ def __init__(self) -> None:
411
+ self._lock = threading.RLock()
412
+ self._grants: list[SessionPermissionGrant] = []
413
+
414
+ def grant(
415
+ self,
416
+ *,
417
+ kind: str,
418
+ scope: Mapping[str, Any],
419
+ source: str = "session",
420
+ ) -> SessionPermissionGrant:
421
+ normalized_kind = _validate_kind(kind)
422
+ scope_type, fingerprint = _validated_scope_fingerprint(scope)
423
+ declared_kind = str(scope.get("kind") or "").strip().casefold()
424
+ if declared_kind and declared_kind != normalized_kind:
425
+ raise PermissionPolicyValidationError("Approval session scope kind does not match.")
426
+ normalized_source = _validate_source(source)
427
+ with self._lock:
428
+ for grant in self._grants:
429
+ if grant.kind == normalized_kind and grant._scope_fingerprint == fingerprint:
430
+ return grant
431
+ grant = SessionPermissionGrant(
432
+ id=f"sg_{uuid.uuid4().hex}",
433
+ kind=normalized_kind,
434
+ scope_type=scope_type,
435
+ source=normalized_source,
436
+ _scope_fingerprint=fingerprint,
437
+ )
438
+ self._grants.append(grant)
439
+ return grant
440
+
441
+ def grant_approval_scope(
442
+ self,
443
+ approval_scope: ApprovalSessionScope,
444
+ *,
445
+ kind: str | None = None,
446
+ source: str = "session",
447
+ ) -> SessionPermissionGrant:
448
+ if not approval_scope.supported or approval_scope.scope is None:
449
+ raise PermissionPolicyValidationError("Approval session scope is not safe to grant.")
450
+ resolved_kind = kind or str(
451
+ approval_scope.scope.get("kind") or approval_scope.scope.get("safe_kind") or ""
452
+ )
453
+ return self.grant(kind=resolved_kind, scope=approval_scope.scope, source=source)
454
+
455
+ def list_grants(self) -> tuple[SessionPermissionGrant, ...]:
456
+ with self._lock:
457
+ return tuple(self._grants)
458
+
459
+ def revoke(self, grant_id: str) -> bool:
460
+ if _SESSION_GRANT_ID_RE.fullmatch(str(grant_id)) is None:
461
+ raise PermissionPolicyValidationError("Session grant id is invalid.")
462
+ with self._lock:
463
+ for index, grant in enumerate(self._grants):
464
+ if grant.id == grant_id:
465
+ del self._grants[index]
466
+ return True
467
+ return False
468
+
469
+ def evaluate(
470
+ self,
471
+ *,
472
+ kind: str,
473
+ scope: Mapping[str, Any],
474
+ sensitive: bool = False,
475
+ external_directory: bool = False,
476
+ ) -> PermissionEvaluation:
477
+ if sensitive:
478
+ return _session_safety_evaluation(
479
+ "sensitive_resource_requires_approval", "override:sensitive"
480
+ )
481
+ if external_directory:
482
+ return _session_safety_evaluation(
483
+ "external_directory_requires_approval", "override:external_directory"
484
+ )
485
+ normalized_kind = _validate_kind(kind)
486
+ _, fingerprint = _validated_scope_fingerprint(scope)
487
+ with self._lock:
488
+ for grant in self._grants:
489
+ if grant.kind == normalized_kind and grant._scope_fingerprint == fingerprint:
490
+ return PermissionEvaluation(
491
+ decision=PolicyEffect.ALLOW,
492
+ reason="session_exact_grant",
493
+ matched_rule_id=grant.id,
494
+ matched_rule_source=grant.source,
495
+ specificity=2_000_000_000,
496
+ )
497
+ return PermissionEvaluation(
498
+ decision=PolicyEffect.ASK,
499
+ reason="session_grant_not_found",
500
+ matched_rule_id=None,
501
+ matched_rule_source=None,
502
+ specificity=0,
503
+ )
504
+
505
+
506
+ def normalize_path_pattern(pattern: str) -> str:
507
+ return _normalize_path_pattern(pattern)
508
+
509
+
510
+ def normalize_command_pattern(
511
+ pattern: str, *, platform: Literal["windows", "posix"] = "posix"
512
+ ) -> str:
513
+ normalized = _normalize_command_text(pattern)
514
+ return normalized.casefold() if platform == "windows" else normalized
515
+
516
+
517
+ def normalize_workspace_path(
518
+ path: str,
519
+ *,
520
+ workspace_root: str | None = None,
521
+ platform: Literal["auto", "windows", "posix"] = "auto",
522
+ ) -> tuple[str | None, bool]:
523
+ selected_platform = _select_platform(platform, path, workspace_root)
524
+ return _normalize_workspace_path(path, workspace_root, selected_platform)
525
+
526
+
527
+ def _load_policy_from_path(path: Path) -> PermissionPolicy:
528
+ if not path.exists():
529
+ return PermissionPolicy()
530
+ try:
531
+ raw = json.loads(path.read_text(encoding="utf-8"))
532
+ return _policy_from_payload(raw)
533
+ except PermissionPolicyCorruptError:
534
+ raise
535
+ except Exception: # noqa: BLE001 - persistent policy must fail closed
536
+ raise PermissionPolicyCorruptError() from None
537
+
538
+
539
+ def _save_policy_to_path(path: Path, policy: PermissionPolicy) -> None:
540
+ if not isinstance(policy, PermissionPolicy):
541
+ raise PermissionPolicyValidationError("Permission policy is invalid.")
542
+ atomic_write_json(path, policy._to_storage_payload(), ensure_ascii=True)
543
+
544
+
545
+ @contextmanager
546
+ def _locked_policy_state(path: Path):
547
+ path.parent.mkdir(parents=True, exist_ok=True)
548
+ lock_path = path.with_name(f"{path.name}.lock")
549
+ with _POLICY_STORE_LOCK:
550
+ with lock_path.open("a+b") as handle:
551
+ _acquire_file_lock(handle)
552
+ try:
553
+ yield
554
+ finally:
555
+ _release_file_lock(handle)
556
+
557
+
558
+ def _acquire_file_lock(handle: BufferedRandom) -> None:
559
+ if os.name == "nt": # pragma: no cover - exercised on Windows
560
+ handle.seek(0, os.SEEK_END)
561
+ if handle.tell() == 0:
562
+ handle.write(b"\0")
563
+ handle.flush()
564
+ handle.seek(0)
565
+ msvcrt.locking(handle.fileno(), msvcrt.LK_LOCK, 1)
566
+ return
567
+ fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
568
+
569
+
570
+ def _release_file_lock(handle: BufferedRandom) -> None:
571
+ if os.name == "nt": # pragma: no cover - exercised on Windows
572
+ handle.seek(0)
573
+ msvcrt.locking(handle.fileno(), msvcrt.LK_UNLCK, 1)
574
+ return
575
+ fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
576
+
577
+
578
+ def _policy_from_payload(raw: Any) -> PermissionPolicy:
579
+ try:
580
+ if not isinstance(raw, dict) or set(raw) != {
581
+ "schema_version",
582
+ "safety_overrides",
583
+ "rules",
584
+ }:
585
+ raise ValueError
586
+ if (
587
+ isinstance(raw["schema_version"], bool)
588
+ or raw["schema_version"] != PERMISSION_POLICY_SCHEMA_VERSION
589
+ ):
590
+ raise ValueError
591
+ overrides = raw["safety_overrides"]
592
+ if not isinstance(overrides, dict) or set(overrides) != {
593
+ "sensitive",
594
+ "external_directory",
595
+ }:
596
+ raise ValueError
597
+ rules_raw = raw["rules"]
598
+ if not isinstance(rules_raw, list) or len(rules_raw) > _MAX_RULES:
599
+ raise ValueError
600
+ rules = tuple(_rule_from_payload(item) for item in rules_raw)
601
+ return PermissionPolicy(
602
+ rules,
603
+ sensitive_override=overrides["sensitive"],
604
+ external_directory_override=overrides["external_directory"],
605
+ )
606
+ except (PermissionPolicyValidationError, TypeError, ValueError):
607
+ raise PermissionPolicyCorruptError() from None
608
+
609
+
610
+ def _rule_from_payload(raw: Any) -> PermissionRule:
611
+ if not isinstance(raw, dict) or set(raw) != {
612
+ "id",
613
+ "effect",
614
+ "order",
615
+ "source",
616
+ "tool_pattern",
617
+ "path_pattern",
618
+ "command_pattern",
619
+ }:
620
+ raise ValueError
621
+ _validate_rule_id(raw["id"])
622
+ if isinstance(raw["order"], bool) or not isinstance(raw["order"], int) or raw["order"] < 0:
623
+ raise ValueError
624
+ rule = PermissionRule(
625
+ id=raw["id"],
626
+ effect=_parse_effect(raw["effect"]),
627
+ order=raw["order"],
628
+ source=_validate_source(raw["source"]),
629
+ tool_pattern=_normalize_optional_tool_pattern(raw["tool_pattern"]),
630
+ path_pattern=_normalize_optional_path_pattern(raw["path_pattern"]),
631
+ command_pattern=_normalize_optional_command_pattern(raw["command_pattern"]),
632
+ )
633
+ if rule.tool_pattern is None and rule.path_pattern is None and rule.command_pattern is None:
634
+ raise ValueError
635
+ return rule
636
+
637
+
638
+ def _validate_rule_collection(rules: Sequence[PermissionRule]) -> None:
639
+ if len(rules) > _MAX_RULES:
640
+ raise PermissionPolicyValidationError("Permission policy contains too many rules.")
641
+ if any(not isinstance(rule, PermissionRule) for rule in rules):
642
+ raise PermissionPolicyValidationError("Permission policy rule is invalid.")
643
+ ids = [rule.id for rule in rules]
644
+ orders = [rule.order for rule in rules]
645
+ if len(ids) != len(set(ids)) or len(orders) != len(set(orders)):
646
+ raise PermissionPolicyValidationError("Permission policy rule identity is invalid.")
647
+ for rule in rules:
648
+ _validate_rule_id(rule.id)
649
+ if isinstance(rule.order, bool) or not isinstance(rule.order, int) or rule.order < 0:
650
+ raise PermissionPolicyValidationError("Permission policy rule order is invalid.")
651
+ _parse_effect(rule.effect)
652
+ _validate_source(rule.source)
653
+ tool_pattern = _normalize_optional_tool_pattern(rule.tool_pattern)
654
+ path_pattern = _normalize_optional_path_pattern(rule.path_pattern)
655
+ command_pattern = _normalize_optional_command_pattern(rule.command_pattern)
656
+ if tool_pattern is None and path_pattern is None and command_pattern is None:
657
+ raise PermissionPolicyValidationError("A permission rule requires a selector.")
658
+
659
+
660
+ def _validate_rule_id(rule_id: Any) -> None:
661
+ if not isinstance(rule_id, str) or _RULE_ID_RE.fullmatch(rule_id) is None:
662
+ raise PermissionPolicyValidationError("Permission rule id is invalid.")
663
+
664
+
665
+ def _parse_effect(effect: PolicyEffect | str) -> PolicyEffect:
666
+ try:
667
+ return effect if isinstance(effect, PolicyEffect) else PolicyEffect(effect)
668
+ except (TypeError, ValueError):
669
+ raise PermissionPolicyValidationError("Permission rule effect is invalid.") from None
670
+
671
+
672
+ def _validate_safety_override(effect: PolicyEffect | str) -> PolicyEffect:
673
+ parsed = _parse_effect(effect)
674
+ if parsed is PolicyEffect.ALLOW:
675
+ raise PermissionPolicyValidationError("Safety overrides cannot allow access.")
676
+ return parsed
677
+
678
+
679
+ def _validate_source(source: Any) -> str:
680
+ if not isinstance(source, str):
681
+ raise PermissionPolicyValidationError("Permission rule source is invalid.")
682
+ normalized = source.strip()
683
+ if (
684
+ not normalized
685
+ or len(normalized) > _MAX_SOURCE_LENGTH
686
+ or _CONTROL_CHARACTER_RE.search(normalized)
687
+ ):
688
+ raise PermissionPolicyValidationError("Permission rule source is invalid.")
689
+ return normalized
690
+
691
+
692
+ def _validate_kind(kind: Any) -> str:
693
+ if not isinstance(kind, str):
694
+ raise PermissionPolicyValidationError("Permission scope kind is invalid.")
695
+ normalized = kind.strip().casefold()
696
+ if (
697
+ not normalized
698
+ or len(normalized) > _MAX_SOURCE_LENGTH
699
+ or _CONTROL_CHARACTER_RE.search(normalized)
700
+ ):
701
+ raise PermissionPolicyValidationError("Permission scope kind is invalid.")
702
+ return normalized
703
+
704
+
705
+ def _normalize_optional_tool_pattern(value: Any) -> str | None:
706
+ if value is None:
707
+ return None
708
+ if not isinstance(value, str):
709
+ raise PermissionPolicyValidationError("Tool selector is invalid.")
710
+ normalized = value.strip().casefold()
711
+ _validate_selector(normalized, "Tool selector is invalid.")
712
+ if "/" in normalized or "\\" in normalized:
713
+ raise PermissionPolicyValidationError("Tool selector is invalid.")
714
+ return normalized
715
+
716
+
717
+ def _normalize_optional_path_pattern(value: Any) -> str | None:
718
+ if value is None:
719
+ return None
720
+ if not isinstance(value, str):
721
+ raise PermissionPolicyValidationError("Path selector is invalid.")
722
+ return _normalize_path_pattern(value)
723
+
724
+
725
+ def _normalize_path_pattern(value: str) -> str:
726
+ normalized = value.strip().replace("\\", "/")
727
+ while "//" in normalized:
728
+ normalized = normalized.replace("//", "/")
729
+ while normalized.startswith("./"):
730
+ normalized = normalized[2:]
731
+ _validate_selector(normalized, "Path selector is invalid.")
732
+ if normalized.startswith("/") or _WINDOWS_ABSOLUTE_RE.match(normalized):
733
+ raise PermissionPolicyValidationError("Path selector must be workspace-relative.")
734
+ if any(part == ".." for part in normalized.split("/")):
735
+ raise PermissionPolicyValidationError("Path selector must be workspace-relative.")
736
+ return normalized
737
+
738
+
739
+ def _normalize_optional_command_pattern(value: Any) -> str | None:
740
+ if value is None:
741
+ return None
742
+ if not isinstance(value, str):
743
+ raise PermissionPolicyValidationError("Command selector is invalid.")
744
+ normalized = _normalize_command_text(value)
745
+ _validate_selector(normalized, "Command selector is invalid.")
746
+ return normalized
747
+
748
+
749
+ def _validate_selector(value: str, message: str) -> None:
750
+ if not value or len(value) > _MAX_SELECTOR_LENGTH or "\x00" in value:
751
+ raise PermissionPolicyValidationError(message)
752
+
753
+
754
+ def _normalize_command_text(value: str) -> str:
755
+ # Collapse separators without changing whitespace inside quoted shell arguments.
756
+ normalized: list[str] = []
757
+ quote: str | None = None
758
+ escaped = False
759
+ separator_pending = False
760
+ for char in value.replace("\r\n", "\n").replace("\r", "\n"):
761
+ if quote is not None:
762
+ normalized.append(char)
763
+ if escaped:
764
+ escaped = False
765
+ elif char == "\\" and quote == '"':
766
+ escaped = True
767
+ elif char == quote:
768
+ quote = None
769
+ continue
770
+ if char.isspace():
771
+ separator_pending = True
772
+ continue
773
+ if separator_pending and normalized:
774
+ normalized.append(" ")
775
+ separator_pending = False
776
+ normalized.append(char)
777
+ if char in {"'", '"'}:
778
+ quote = char
779
+ return "".join(normalized)
780
+
781
+
782
+ def _normalize_request(request: PermissionRequest) -> _NormalizedRequest:
783
+ if not isinstance(request, PermissionRequest):
784
+ raise PermissionPolicyValidationError("Permission request is invalid.")
785
+ tool_name = _validate_kind(request.tool_name)
786
+ selected_platform = _select_platform(
787
+ request.platform,
788
+ *(request.paths[:1]),
789
+ request.workspace_root,
790
+ )
791
+ normalized_paths: list[str] = []
792
+ detected_external = False
793
+ for path in request.paths:
794
+ normalized, external = _normalize_workspace_path(
795
+ path,
796
+ request.workspace_root,
797
+ selected_platform,
798
+ )
799
+ detected_external = detected_external or external
800
+ if normalized is not None:
801
+ normalized_paths.append(normalized)
802
+ command = _normalize_command_text(request.command) if request.command is not None else None
803
+ if command is not None and selected_platform == "windows":
804
+ command = command.casefold()
805
+ return _NormalizedRequest(
806
+ tool_name=tool_name,
807
+ paths=tuple(normalized_paths),
808
+ command=command,
809
+ platform=selected_platform,
810
+ sensitive=bool(request.sensitive),
811
+ external_directory=bool(request.external_directory or detected_external),
812
+ )
813
+
814
+
815
+ def _select_platform(
816
+ platform: Literal["auto", "windows", "posix"], *values: str | None
817
+ ) -> Literal["windows", "posix"]:
818
+ if platform in {"windows", "posix"}:
819
+ return platform
820
+ if platform != "auto":
821
+ raise PermissionPolicyValidationError("Permission request platform is invalid.")
822
+ if any(value and _WINDOWS_ABSOLUTE_RE.match(value) for value in values):
823
+ return "windows"
824
+ return "windows" if os.name == "nt" else "posix"
825
+
826
+
827
+ def _normalize_workspace_path(
828
+ value: str,
829
+ workspace_root: str | None,
830
+ platform: Literal["windows", "posix"],
831
+ ) -> tuple[str | None, bool]:
832
+ if not isinstance(value, str) or not value.strip() or "\x00" in value:
833
+ raise PermissionPolicyValidationError("Permission request path is invalid.")
834
+ if platform == "windows":
835
+ return _normalize_windows_workspace_path(value, workspace_root)
836
+ return _normalize_posix_workspace_path(value, workspace_root)
837
+
838
+
839
+ def _normalize_windows_workspace_path(
840
+ value: str, workspace_root: str | None
841
+ ) -> tuple[str | None, bool]:
842
+ raw = value.strip().replace("/", "\\")
843
+ is_absolute = bool(_WINDOWS_ABSOLUTE_RE.match(raw))
844
+ if is_absolute:
845
+ if not workspace_root or not _WINDOWS_ABSOLUTE_RE.match(workspace_root):
846
+ return None, True
847
+ try:
848
+ relative = ntpath.relpath(ntpath.normpath(raw), ntpath.normpath(workspace_root))
849
+ except ValueError:
850
+ return None, True
851
+ if relative == ".." or relative.startswith(f"..{ntpath.sep}") or ntpath.isabs(relative):
852
+ return None, True
853
+ raw = relative
854
+ normalized = ntpath.normpath(raw).replace("\\", "/")
855
+ if normalized in {"", "."}:
856
+ return ".", False
857
+ if normalized == ".." or normalized.startswith("../") or _WINDOWS_ABSOLUTE_RE.match(normalized):
858
+ return None, True
859
+ return normalized.casefold(), False
860
+
861
+
862
+ def _normalize_posix_workspace_path(
863
+ value: str, workspace_root: str | None
864
+ ) -> tuple[str | None, bool]:
865
+ raw = value.strip().replace("\\", "/")
866
+ if posixpath.isabs(raw):
867
+ if not workspace_root or not posixpath.isabs(workspace_root.replace("\\", "/")):
868
+ return None, True
869
+ root = posixpath.normpath(workspace_root.replace("\\", "/"))
870
+ normalized_absolute = posixpath.normpath(raw)
871
+ try:
872
+ if posixpath.commonpath((root, normalized_absolute)) != root:
873
+ return None, True
874
+ except ValueError:
875
+ return None, True
876
+ raw = posixpath.relpath(normalized_absolute, root)
877
+ normalized = posixpath.normpath(raw)
878
+ if normalized in {"", "."}:
879
+ return ".", False
880
+ if normalized == ".." or normalized.startswith("../") or posixpath.isabs(normalized):
881
+ return None, True
882
+ return normalized, False
883
+
884
+
885
+ def _rule_matches(rule: PermissionRule, request: _NormalizedRequest) -> bool:
886
+ if rule.tool_pattern is not None and not fnmatch.fnmatchcase(
887
+ request.tool_name, rule.tool_pattern
888
+ ):
889
+ return False
890
+ if rule.command_pattern is not None:
891
+ if request.command is None:
892
+ return False
893
+ pattern = normalize_command_pattern(rule.command_pattern, platform=request.platform)
894
+ if not fnmatch.fnmatchcase(request.command, pattern):
895
+ return False
896
+ if rule.path_pattern is not None:
897
+ if not request.paths:
898
+ return False
899
+ pattern = (
900
+ rule.path_pattern.casefold() if request.platform == "windows" else rule.path_pattern
901
+ )
902
+ matches = [_path_glob_matches(path, pattern) for path in request.paths]
903
+ # A deny/ask guard protects any matching target; an allow must cover every target.
904
+ if rule.effect is PolicyEffect.ALLOW:
905
+ return all(matches)
906
+ return any(matches)
907
+ return True
908
+
909
+
910
+ def _path_glob_matches(path: str, pattern: str) -> bool:
911
+ regex_parts: list[str] = ["^"]
912
+ index = 0
913
+ while index < len(pattern):
914
+ char = pattern[index]
915
+ if char == "*" and index + 1 < len(pattern) and pattern[index + 1] == "*":
916
+ if index + 2 < len(pattern) and pattern[index + 2] == "/":
917
+ regex_parts.append("(?:.*/)?")
918
+ index += 3
919
+ continue
920
+ if index > 0 and pattern[index - 1] == "/" and index + 2 == len(pattern):
921
+ regex_parts[-1] = "(?:/.*)?"
922
+ index += 2
923
+ continue
924
+ regex_parts.append(".*")
925
+ index += 2
926
+ continue
927
+ if char == "*":
928
+ regex_parts.append("[^/]*")
929
+ elif char == "?":
930
+ regex_parts.append("[^/]")
931
+ else:
932
+ regex_parts.append(re.escape(char))
933
+ index += 1
934
+ regex_parts.append("$")
935
+ return re.fullmatch("".join(regex_parts), path) is not None
936
+
937
+
938
+ def _rule_specificity(rule: PermissionRule) -> int:
939
+ patterns = tuple(
940
+ pattern
941
+ for pattern in (rule.tool_pattern, rule.path_pattern, rule.command_pattern)
942
+ if pattern is not None
943
+ )
944
+ selector_count = len(patterns)
945
+ exact_count = sum(not any(char in pattern for char in "*?") for pattern in patterns)
946
+ literal_count = sum(sum(char not in "*?" for char in pattern) for pattern in patterns)
947
+ wildcard_count = sum(sum(char in "*?" for char in pattern) for pattern in patterns)
948
+ return (
949
+ selector_count * 1_000_000
950
+ + exact_count * 100_000
951
+ + min(literal_count, 9_999) * 10
952
+ - min(wildcard_count, 9)
953
+ )
954
+
955
+
956
+ def _validated_scope_fingerprint(scope: Mapping[str, Any]) -> tuple[str, str]:
957
+ if not isinstance(scope, Mapping):
958
+ raise PermissionPolicyValidationError("Approval session scope is invalid.")
959
+ try:
960
+ copied = dict(scope)
961
+ scope_type = str(copied.get("type") or "")
962
+ if scope_type not in SUPPORTED_APPROVAL_SCOPES:
963
+ raise ValueError
964
+ if scope_type == "exact_command_hash":
965
+ if _SHA256_RE.fullmatch(str(copied.get("command_hash") or "")) is None:
966
+ raise ValueError
967
+ elif scope_type == "exact_file_set":
968
+ if _SHA256_RE.fullmatch(str(copied.get("files_hash") or "")) is None:
969
+ raise ValueError
970
+ files = copied.get("files")
971
+ if not isinstance(files, list) or not files:
972
+ raise ValueError
973
+ elif scope_type == "exact_verify_command_set":
974
+ if _SHA256_RE.fullmatch(str(copied.get("commands_hash") or "")) is None:
975
+ raise ValueError
976
+ else:
977
+ if copied.get("backend_owned") is not True or not str(copied.get("safe_kind") or ""):
978
+ raise ValueError
979
+ encoded = json.dumps(
980
+ copied,
981
+ ensure_ascii=True,
982
+ sort_keys=True,
983
+ separators=(",", ":"),
984
+ ).encode("utf-8")
985
+ except (TypeError, ValueError):
986
+ raise PermissionPolicyValidationError("Approval session scope is invalid.") from None
987
+ return scope_type, hashlib.sha256(encoded).hexdigest()
988
+
989
+
990
+ def _session_safety_evaluation(reason: str, rule_id: str) -> PermissionEvaluation:
991
+ return PermissionEvaluation(
992
+ decision=PolicyEffect.ASK,
993
+ reason=reason,
994
+ matched_rule_id=rule_id,
995
+ matched_rule_source="builtin_safety",
996
+ specificity=2_147_483_647,
997
+ )
998
+
999
+
1000
+ __all__ = [
1001
+ "InMemorySessionGrantAdapter",
1002
+ "PERMISSION_POLICY_SCHEMA_VERSION",
1003
+ "PermissionEvaluation",
1004
+ "PermissionPolicy",
1005
+ "PermissionPolicyCorruptError",
1006
+ "PermissionPolicyError",
1007
+ "PermissionPolicyStore",
1008
+ "PermissionPolicyValidationError",
1009
+ "PermissionRequest",
1010
+ "PermissionRule",
1011
+ "PolicyEffect",
1012
+ "SessionPermissionGrant",
1013
+ "normalize_command_pattern",
1014
+ "normalize_path_pattern",
1015
+ "normalize_workspace_path",
1016
+ ]