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,629 @@
1
+ from __future__ import annotations
2
+
3
+ import copy
4
+ import json
5
+ import re
6
+ from collections.abc import Collection
7
+ from dataclasses import dataclass, fields, is_dataclass, replace
8
+ from typing import Any
9
+
10
+ from ..tools.fs import classify_sensitive_path
11
+
12
+ _SENSITIVE_POLICY_KEY = "_alysis_output_policy"
13
+ _REDACTED_ARGUMENT = "[redacted: sensitive file content]"
14
+ _REDACTED_MESSAGE = "Sensitive tool output redacted."
15
+ _REDACTED_RESPONSE_TAINT = "[redacted: approved sensitive content]"
16
+ _TERMINAL_SENSITIVE_READ_ERROR = (
17
+ "Sensitive path is protected and will not be readable after this failure. "
18
+ "No content was returned. This failure is terminal; do not retry."
19
+ )
20
+ _CONTENT_ARGUMENT_KEYS = frozenset(
21
+ {
22
+ "body",
23
+ "bytes",
24
+ "content",
25
+ "contents",
26
+ "data",
27
+ "diff",
28
+ "edit",
29
+ "edits",
30
+ "expected",
31
+ "expected_old",
32
+ "insert",
33
+ "lines",
34
+ "new",
35
+ "new_content",
36
+ "old",
37
+ "old_content",
38
+ "patch",
39
+ "replacement",
40
+ "text",
41
+ "value",
42
+ }
43
+ )
44
+ _DIRECT_PATH_FIELDS = (
45
+ "path",
46
+ "source_path",
47
+ "destination_path",
48
+ "file",
49
+ "file_path",
50
+ )
51
+ _PATCH_PATH_RE = re.compile(r"^(?:\+\+\+|---)\s+(?:[ab]/)?([^\t\r\n]+)", re.MULTILINE)
52
+ _TAINT_VALUE_KEYS = _CONTENT_ARGUMENT_KEYS | frozenset({"insert_content", "replacement", "target"})
53
+
54
+
55
+ @dataclass(frozen=True, slots=True)
56
+ class SensitiveToolBoundary:
57
+ """Persistence/display policy derived without opening sensitive files."""
58
+
59
+ sensitive: bool = False
60
+ paths: tuple[str, ...] = ()
61
+ categories: tuple[str, ...] = ()
62
+
63
+
64
+ @dataclass(frozen=True, slots=True)
65
+ class _PersistenceSafeToolCall:
66
+ id: str
67
+ name: str
68
+ arguments: Any
69
+ provider_metadata: Any = None
70
+
71
+
72
+ @dataclass(frozen=True, slots=True)
73
+ class _PersistenceSafeResponse:
74
+ content: str
75
+ tool_calls: list[Any]
76
+ raw: dict[str, Any]
77
+ response_model: Any = None
78
+ usage: Any = None
79
+ provider_metadata: Any = None
80
+ reasoning: Any = ()
81
+ assistant_phase: Any = None
82
+
83
+
84
+ def _normalized_unique_strings(values: list[Any]) -> tuple[str, ...]:
85
+ normalized: list[str] = []
86
+ seen: set[str] = set()
87
+ for value in values:
88
+ item = str(value or "").strip().replace("\\", "/")
89
+ if not item or item == "/dev/null":
90
+ continue
91
+ key = item.casefold()
92
+ if key in seen:
93
+ continue
94
+ seen.add(key)
95
+ normalized.append(item)
96
+ return tuple(normalized)
97
+
98
+
99
+ def _candidate_paths(tool_name: str, arguments: Any) -> tuple[str, ...]:
100
+ if not isinstance(arguments, dict):
101
+ return ()
102
+ normalized_name = str(tool_name or "").strip().casefold()
103
+ if not (normalized_name.startswith("fs_") or normalized_name == "git_apply_patch"):
104
+ return ()
105
+ candidates = [arguments.get(field) for field in _DIRECT_PATH_FIELDS]
106
+ if normalized_name == "git_apply_patch":
107
+ patch = arguments.get("patch")
108
+ if patch is None:
109
+ patch = arguments.get("diff")
110
+ if patch is not None:
111
+ candidates.extend(_PATCH_PATH_RE.findall(str(patch)))
112
+ return _normalized_unique_strings(candidates)
113
+
114
+
115
+ def _policy_categories(result: Any) -> tuple[str, ...]:
116
+ if not isinstance(result, dict):
117
+ return ()
118
+ policy = result.get(_SENSITIVE_POLICY_KEY)
119
+ if not isinstance(policy, dict) or policy.get("sensitive") is not True:
120
+ return ()
121
+ if str(policy.get("persist") or "").casefold() != "redact":
122
+ return ()
123
+ if str(policy.get("display") or "").casefold() != "redact":
124
+ return ()
125
+ raw_categories = policy.get("categories")
126
+ if not isinstance(raw_categories, list | tuple):
127
+ return ()
128
+ return _normalized_unique_strings(list(raw_categories))
129
+
130
+
131
+ def sensitive_tool_boundary(
132
+ tool_name: str,
133
+ arguments: Any,
134
+ *,
135
+ result: Any = None,
136
+ ) -> SensitiveToolBoundary:
137
+ paths = _candidate_paths(tool_name, arguments)
138
+ categories: list[str] = []
139
+ sensitive_paths: list[str] = []
140
+ for path in paths:
141
+ classification = classify_sensitive_path(path)
142
+ if not classification.sensitive:
143
+ continue
144
+ sensitive_paths.append(path)
145
+ categories.append(str(classification.category or "sensitive_file"))
146
+
147
+ policy_categories = _policy_categories(result)
148
+ policy = result.get(_SENSITIVE_POLICY_KEY) if isinstance(result, dict) else None
149
+ policy_sensitive = isinstance(policy, dict) and policy.get("sensitive") is True
150
+ if not sensitive_paths and not policy_sensitive:
151
+ return SensitiveToolBoundary()
152
+ return SensitiveToolBoundary(
153
+ sensitive=True,
154
+ paths=_normalized_unique_strings(sensitive_paths or list(paths)),
155
+ categories=_normalized_unique_strings(categories + list(policy_categories)),
156
+ )
157
+
158
+
159
+ def _redact_content_fields(value: Any, *, parent_key: str = "") -> Any:
160
+ if parent_key.casefold() in _CONTENT_ARGUMENT_KEYS:
161
+ return _REDACTED_ARGUMENT
162
+ if isinstance(value, dict):
163
+ return {
164
+ str(key): _redact_content_fields(item, parent_key=str(key))
165
+ for key, item in value.items()
166
+ }
167
+ if isinstance(value, list):
168
+ return [_redact_content_fields(item) for item in value]
169
+ if isinstance(value, tuple):
170
+ return tuple(_redact_content_fields(item) for item in value)
171
+ return copy.deepcopy(value)
172
+
173
+
174
+ def redact_sensitive_tool_arguments(
175
+ tool_name: str,
176
+ arguments: Any,
177
+ *,
178
+ boundary: SensitiveToolBoundary | None = None,
179
+ ) -> Any:
180
+ effective = boundary or sensitive_tool_boundary(tool_name, arguments)
181
+ if not effective.sensitive:
182
+ return arguments
183
+ return _redact_content_fields(arguments)
184
+
185
+
186
+ def sensitive_result_stub(
187
+ tool_name: str,
188
+ boundary: SensitiveToolBoundary,
189
+ ) -> dict[str, Any]:
190
+ """Return the only representation allowed beyond the immediate model call."""
191
+
192
+ return {
193
+ "_alysis_redacted": True,
194
+ "sensitive": True,
195
+ "persist": "redact",
196
+ "display": "redact",
197
+ "tool": str(tool_name or ""),
198
+ "paths": list(boundary.paths),
199
+ "categories": list(boundary.categories),
200
+ "message": _REDACTED_MESSAGE,
201
+ }
202
+
203
+
204
+ def redact_sensitive_tool_result(
205
+ tool_name: str,
206
+ arguments: Any,
207
+ result: Any,
208
+ *,
209
+ boundary: SensitiveToolBoundary | None = None,
210
+ ) -> Any:
211
+ effective = boundary or sensitive_tool_boundary(tool_name, arguments, result=result)
212
+ if not effective.sensitive:
213
+ return result
214
+ stub = sensitive_result_stub(tool_name, effective)
215
+ if isinstance(result, dict) and "error" in result:
216
+ normalized_tool = str(tool_name or "").strip().casefold()
217
+ if normalized_tool in {"fs_read", "fs_read_lines"}:
218
+ if result.get("error_code") == "fs_path_not_found":
219
+ path = effective.paths[0] if effective.paths else "requested path"
220
+ stub["error"] = (
221
+ f"Path does not exist: {path}. This result is terminal; do not retry this path."
222
+ )
223
+ stub["error_code"] = "fs_path_not_found"
224
+ else:
225
+ stub["error"] = _TERMINAL_SENSITIVE_READ_ERROR
226
+ stub["error_code"] = "sensitive_read_terminal"
227
+ stub["terminal"] = True
228
+ stub["retryable"] = False
229
+ else:
230
+ stub["error"] = "Sensitive tool operation failed; details redacted."
231
+ return stub
232
+
233
+
234
+ def _add_taint_variants(values: set[str], raw_value: Any) -> None:
235
+ if isinstance(raw_value, bytes):
236
+ text = raw_value.decode("utf-8", errors="replace")
237
+ elif isinstance(raw_value, str):
238
+ text = raw_value
239
+ else:
240
+ return
241
+ if not text or not text.strip():
242
+ return
243
+ values.add(text)
244
+ stripped = text.strip()
245
+ if stripped:
246
+ values.add(stripped)
247
+ escaped = json.dumps(text, ensure_ascii=False)[1:-1]
248
+ if escaped and escaped != text:
249
+ values.add(escaped)
250
+
251
+ def _collect_json_strings(value: Any) -> None:
252
+ if isinstance(value, str):
253
+ if value.strip():
254
+ values.add(value)
255
+ return
256
+ if isinstance(value, dict):
257
+ for item in value.values():
258
+ _collect_json_strings(item)
259
+ return
260
+ if isinstance(value, list):
261
+ for item in value:
262
+ _collect_json_strings(item)
263
+
264
+ def _collect_json_text(candidate: str) -> None:
265
+ try:
266
+ decoded = json.loads(candidate)
267
+ except (TypeError, ValueError, json.JSONDecodeError):
268
+ return
269
+ _collect_json_strings(decoded)
270
+
271
+ for line in text.splitlines():
272
+ line_value = line.strip()
273
+ if line_value:
274
+ values.add(line_value)
275
+ for separator in ("=", ":"):
276
+ if separator not in line_value:
277
+ continue
278
+ key, candidate = line_value.split(separator, 1)
279
+ if not key.strip() or not candidate.strip():
280
+ continue
281
+ candidate = candidate.strip().strip("\"'")
282
+ if candidate:
283
+ values.add(candidate)
284
+ _collect_json_text(candidate)
285
+ break
286
+ _collect_json_text(text)
287
+
288
+
289
+ def _collect_taint_values(value: Any, values: set[str], *, parent_key: str = "") -> None:
290
+ if isinstance(value, dict):
291
+ for key, item in value.items():
292
+ normalized_key = str(key).casefold()
293
+ if normalized_key in _TAINT_VALUE_KEYS and isinstance(item, str | bytes):
294
+ _add_taint_variants(values, item)
295
+ else:
296
+ _collect_taint_values(item, values, parent_key=normalized_key)
297
+ return
298
+ if isinstance(value, list | tuple):
299
+ for item in value:
300
+ _collect_taint_values(item, values, parent_key=parent_key)
301
+ return
302
+ if parent_key in _TAINT_VALUE_KEYS:
303
+ _add_taint_variants(values, value)
304
+
305
+
306
+ def collect_sensitive_response_taints(
307
+ tool_name: str,
308
+ arguments: Any,
309
+ result: Any,
310
+ *,
311
+ boundary: SensitiveToolBoundary | None = None,
312
+ ) -> tuple[str, ...]:
313
+ """Collect exact approved content values that a provider must not echo durably.
314
+
315
+ Paths, categories, operation names, and unrelated metadata are deliberately
316
+ excluded. Only content-bearing leaves from a sensitive tool exchange become
317
+ taints, which keeps redaction narrow and value-specific.
318
+ """
319
+
320
+ effective = boundary or sensitive_tool_boundary(tool_name, arguments, result=result)
321
+ if not effective.sensitive:
322
+ return ()
323
+ values: set[str] = set()
324
+ _collect_taint_values(arguments, values)
325
+ if not (isinstance(result, dict) and "error" in result):
326
+ if isinstance(result, str | bytes):
327
+ _add_taint_variants(values, result)
328
+ else:
329
+ _collect_taint_values(result, values)
330
+ return tuple(sorted(values, key=lambda item: (-len(item), item)))
331
+
332
+
333
+ def _redact_tainted_text(text: str, taints: tuple[str, ...]) -> str:
334
+ redacted = text
335
+ for taint in taints:
336
+ if taint and taint in redacted:
337
+ redacted = redacted.replace(taint, _REDACTED_RESPONSE_TAINT)
338
+ return redacted
339
+
340
+
341
+ def _redact_tainted_value(value: Any, taints: tuple[str, ...]) -> Any:
342
+ if isinstance(value, str):
343
+ redacted = _redact_tainted_text(value, taints)
344
+ return value if redacted == value else redacted
345
+ if isinstance(value, bytes):
346
+ decoded = value.decode("utf-8", errors="replace")
347
+ redacted = _redact_tainted_text(decoded, taints)
348
+ return value if redacted == decoded else redacted.encode("utf-8")
349
+ if isinstance(value, dict):
350
+ copied: dict[Any, Any] | None = None
351
+ for key, item in value.items():
352
+ redacted_item = _redact_tainted_value(item, taints)
353
+ if redacted_item is not item and copied is None:
354
+ copied = dict(value)
355
+ if copied is not None:
356
+ copied[key] = redacted_item
357
+ return value if copied is None else copied
358
+ if isinstance(value, list):
359
+ redacted_items = [_redact_tainted_value(item, taints) for item in value]
360
+ if all(
361
+ redacted is original for redacted, original in zip(redacted_items, value, strict=True)
362
+ ):
363
+ return value
364
+ return redacted_items
365
+ if isinstance(value, tuple):
366
+ redacted_items = tuple(_redact_tainted_value(item, taints) for item in value)
367
+ if all(
368
+ redacted is original for redacted, original in zip(redacted_items, value, strict=True)
369
+ ):
370
+ return value
371
+ return redacted_items
372
+ if is_dataclass(value) and not isinstance(value, type):
373
+ updates: dict[str, Any] = {}
374
+ for dataclass_field in fields(value):
375
+ original = getattr(value, dataclass_field.name)
376
+ redacted = _redact_tainted_value(original, taints)
377
+ if redacted is not original:
378
+ updates[dataclass_field.name] = redacted
379
+ if not updates:
380
+ return value
381
+ try:
382
+ return replace(value, **updates)
383
+ except (TypeError, ValueError):
384
+ return value
385
+ return value
386
+
387
+
388
+ def redact_sensitive_response_taints(response: Any, taints: Collection[str]) -> Any:
389
+ """Redact exact approved values from every provider-response channel."""
390
+
391
+ normalized = tuple(
392
+ sorted(
393
+ {str(value) for value in taints if isinstance(value, str) and value},
394
+ key=lambda item: (-len(item), item),
395
+ )
396
+ )
397
+ if not normalized:
398
+ return response
399
+ redacted_response = _redact_tainted_value(response, normalized)
400
+ if redacted_response is not response:
401
+ return redacted_response
402
+
403
+ content = _redact_tainted_value(str(getattr(response, "content", "") or ""), normalized)
404
+ raw = _redact_tainted_value(getattr(response, "raw", {}), normalized)
405
+ response_tool_calls = list(getattr(response, "tool_calls", []) or [])
406
+ tool_calls: list[Any] = []
407
+ calls_changed = False
408
+ for tool_call in response_tool_calls:
409
+ arguments = _redact_tainted_value(getattr(tool_call, "arguments", None), normalized)
410
+ provider_metadata = _redact_tainted_value(
411
+ getattr(tool_call, "provider_metadata", None), normalized
412
+ )
413
+ if arguments is getattr(tool_call, "arguments", None) and provider_metadata is getattr(
414
+ tool_call, "provider_metadata", None
415
+ ):
416
+ tool_calls.append(tool_call)
417
+ continue
418
+ calls_changed = True
419
+ tool_calls.append(
420
+ _PersistenceSafeToolCall(
421
+ id=str(getattr(tool_call, "id", "") or ""),
422
+ name=str(getattr(tool_call, "name", "") or ""),
423
+ arguments=arguments,
424
+ provider_metadata=provider_metadata,
425
+ )
426
+ )
427
+ provider_metadata = _redact_tainted_value(
428
+ getattr(response, "provider_metadata", None), normalized
429
+ )
430
+ reasoning = _redact_tainted_value(getattr(response, "reasoning", ()), normalized)
431
+ usage = _redact_tainted_value(getattr(response, "usage", None), normalized)
432
+ changed = (
433
+ content != str(getattr(response, "content", "") or "")
434
+ or raw is not getattr(response, "raw", {})
435
+ or calls_changed
436
+ or provider_metadata is not getattr(response, "provider_metadata", None)
437
+ or reasoning is not getattr(response, "reasoning", ())
438
+ or usage is not getattr(response, "usage", None)
439
+ )
440
+ if not changed:
441
+ return response
442
+ return _PersistenceSafeResponse(
443
+ content=content,
444
+ tool_calls=tool_calls,
445
+ raw=raw,
446
+ response_model=copy.deepcopy(getattr(response, "response_model", None)),
447
+ usage=usage,
448
+ provider_metadata=provider_metadata,
449
+ reasoning=reasoning,
450
+ assistant_phase=copy.deepcopy(getattr(response, "assistant_phase", None)),
451
+ )
452
+
453
+
454
+ def redact_assistant_tool_call_message(message: dict[str, Any]) -> dict[str, Any]:
455
+ """Redact sensitive call bodies while preserving provider call IDs and shape."""
456
+
457
+ tool_calls = message.get("tool_calls")
458
+ if not isinstance(tool_calls, list):
459
+ return message
460
+ copied: dict[str, Any] | None = None
461
+ copied_calls: list[Any] = []
462
+ for call in tool_calls:
463
+ if not isinstance(call, dict):
464
+ copied_calls.append(copy.deepcopy(call))
465
+ continue
466
+ function = call.get("function")
467
+ if not isinstance(function, dict):
468
+ copied_calls.append(copy.deepcopy(call))
469
+ continue
470
+ tool_name = str(function.get("name") or "")
471
+ serialized = function.get("arguments")
472
+ try:
473
+ arguments = json.loads(serialized) if isinstance(serialized, str) else serialized
474
+ except (TypeError, ValueError, json.JSONDecodeError):
475
+ arguments = {}
476
+ boundary = sensitive_tool_boundary(tool_name, arguments)
477
+ if not boundary.sensitive:
478
+ copied_calls.append(copy.deepcopy(call))
479
+ continue
480
+ if copied is None:
481
+ copied = copy.deepcopy(message)
482
+ copied_call = copy.deepcopy(call)
483
+ copied_function = dict(copied_call["function"])
484
+ redacted = redact_sensitive_tool_arguments(
485
+ tool_name,
486
+ arguments,
487
+ boundary=boundary,
488
+ )
489
+ copied_function["arguments"] = json.dumps(redacted, ensure_ascii=False)
490
+ copied_call["function"] = copied_function
491
+ copied_calls.append(copied_call)
492
+ if copied is None:
493
+ return message
494
+ copied["content"] = ""
495
+ copied["tool_calls"] = copied_calls
496
+ return copied
497
+
498
+
499
+ def redact_sensitive_response_for_persistence(
500
+ response: Any, *, taints: Collection[str] = ()
501
+ ) -> Any:
502
+ """Return a usage/persistence-safe response while preserving accounting fields."""
503
+
504
+ response = redact_sensitive_response_taints(response, taints)
505
+
506
+ tool_calls = getattr(response, "tool_calls", None)
507
+ if not isinstance(tool_calls, list):
508
+ return response
509
+ copied_calls: list[Any] = []
510
+ changed = False
511
+ for tool_call in tool_calls:
512
+ tool_name = str(getattr(tool_call, "name", "") or "")
513
+ arguments = getattr(tool_call, "arguments", None)
514
+ boundary = sensitive_tool_boundary(tool_name, arguments)
515
+ if not boundary.sensitive:
516
+ copied_calls.append(tool_call)
517
+ continue
518
+ changed = True
519
+ redacted_arguments = redact_sensitive_tool_arguments(
520
+ tool_name,
521
+ arguments,
522
+ boundary=boundary,
523
+ )
524
+ try:
525
+ copied_calls.append(replace(tool_call, arguments=redacted_arguments))
526
+ except (TypeError, ValueError):
527
+ copied_calls.append(
528
+ _PersistenceSafeToolCall(
529
+ id=str(getattr(tool_call, "id", "") or ""),
530
+ name=tool_name,
531
+ arguments=redacted_arguments,
532
+ provider_metadata=copy.deepcopy(getattr(tool_call, "provider_metadata", None)),
533
+ )
534
+ )
535
+ if not changed:
536
+ return response
537
+ try:
538
+ # Provider-native raw bodies commonly repeat function arguments. They
539
+ # are not needed for usage accounting and cannot safely be retained.
540
+ return replace(response, content="", tool_calls=copied_calls, raw={})
541
+ except (TypeError, ValueError):
542
+ return _PersistenceSafeResponse(
543
+ content="",
544
+ tool_calls=copied_calls,
545
+ raw={},
546
+ response_model=copy.deepcopy(getattr(response, "response_model", None)),
547
+ usage=copy.deepcopy(getattr(response, "usage", None)),
548
+ provider_metadata=copy.deepcopy(getattr(response, "provider_metadata", None)),
549
+ reasoning=copy.deepcopy(getattr(response, "reasoning", ())),
550
+ assistant_phase=copy.deepcopy(getattr(response, "assistant_phase", None)),
551
+ )
552
+
553
+
554
+ def redact_consumed_sensitive_tool_messages(
555
+ messages: list[dict[str, Any]],
556
+ result_stubs: dict[str, str],
557
+ ) -> None:
558
+ """Scrub one-call-only sensitive messages after the provider consumed them."""
559
+
560
+ if not result_stubs:
561
+ return
562
+ call_ids = set(result_stubs)
563
+ for index, message in enumerate(messages):
564
+ if not isinstance(message, dict):
565
+ continue
566
+ if str(message.get("role") or "") == "tool":
567
+ call_id = str(message.get("tool_call_id") or "")
568
+ if call_id in result_stubs:
569
+ message["content"] = result_stubs[call_id]
570
+ continue
571
+ if str(message.get("role") or "") != "assistant":
572
+ continue
573
+ tool_calls = message.get("tool_calls")
574
+ if not isinstance(tool_calls, list):
575
+ continue
576
+ if any(
577
+ isinstance(call, dict) and str(call.get("id") or "") in call_ids for call in tool_calls
578
+ ):
579
+ messages[index] = redact_assistant_tool_call_message(message)
580
+
581
+
582
+ def inject_ephemeral_sensitive_tool_messages(
583
+ messages: list[dict[str, Any]],
584
+ *,
585
+ result_content: dict[str, str],
586
+ arguments_content: dict[str, str],
587
+ ) -> list[dict[str, Any]]:
588
+ """Expose sensitive tool material to one provider request, without mutating history."""
589
+
590
+ call_ids = set(result_content) | set(arguments_content)
591
+ if not call_ids:
592
+ return messages
593
+ injected = copy.deepcopy(messages)
594
+ for message in injected:
595
+ if str(message.get("role") or "") == "tool":
596
+ call_id = str(message.get("tool_call_id") or "")
597
+ if call_id in result_content:
598
+ message["content"] = result_content[call_id]
599
+ continue
600
+ if str(message.get("role") or "") != "assistant":
601
+ continue
602
+ tool_calls = message.get("tool_calls")
603
+ if not isinstance(tool_calls, list):
604
+ continue
605
+ for call in tool_calls:
606
+ if not isinstance(call, dict):
607
+ continue
608
+ call_id = str(call.get("id") or "")
609
+ if call_id not in arguments_content:
610
+ continue
611
+ function = call.get("function")
612
+ if isinstance(function, dict):
613
+ function["arguments"] = arguments_content[call_id]
614
+ return injected
615
+
616
+
617
+ __all__ = [
618
+ "SensitiveToolBoundary",
619
+ "collect_sensitive_response_taints",
620
+ "inject_ephemeral_sensitive_tool_messages",
621
+ "redact_assistant_tool_call_message",
622
+ "redact_consumed_sensitive_tool_messages",
623
+ "redact_sensitive_tool_arguments",
624
+ "redact_sensitive_tool_result",
625
+ "redact_sensitive_response_for_persistence",
626
+ "redact_sensitive_response_taints",
627
+ "sensitive_result_stub",
628
+ "sensitive_tool_boundary",
629
+ ]