iCode-TUI 0.29.1__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 (1138) hide show
  1. chrys/__init__.py +16 -0
  2. chrys/app/__init__.py +3 -0
  3. chrys/app/acp/__init__.py +3 -0
  4. chrys/app/acp/bridge.py +664 -0
  5. chrys/app/acp/content.py +132 -0
  6. chrys/app/acp/doc/frontend-api.md +425 -0
  7. chrys/app/acp/history.py +585 -0
  8. chrys/app/acp/server.py +1796 -0
  9. chrys/app/acp/session_manager.py +1815 -0
  10. chrys/app/acp/tool_status.py +45 -0
  11. chrys/app/cli/__init__.py +3 -0
  12. chrys/app/cli/acp.py +178 -0
  13. chrys/app/cli/app.py +165 -0
  14. chrys/app/cli/approvals.py +101 -0
  15. chrys/app/cli/headless.py +223 -0
  16. chrys/app/cli/launch_cwd.py +42 -0
  17. chrys/app/cli/profiles.py +298 -0
  18. chrys/app/cli/progress.py +857 -0
  19. chrys/app/cli/run.py +425 -0
  20. chrys/app/cli/serve.py +1593 -0
  21. chrys/app/cli/tool_summary.py +225 -0
  22. chrys/app/cli/trajectory.py +201 -0
  23. chrys/app/cli/workflow.py +601 -0
  24. chrys/app/features/__init__.py +3 -0
  25. chrys/app/features/buddy/__init__.py +3 -0
  26. chrys/app/features/buddy/actions.py +68 -0
  27. chrys/app/features/buddy/animation.py +76 -0
  28. chrys/app/features/buddy/commands.py +196 -0
  29. chrys/app/features/buddy/docs/design.md +187 -0
  30. chrys/app/features/buddy/hatchery.py +80 -0
  31. chrys/app/features/buddy/lifecycle.py +41 -0
  32. chrys/app/features/buddy/model.py +265 -0
  33. chrys/app/features/buddy/pixel_renderer.py +113 -0
  34. chrys/app/features/buddy/pixel_sprites.py +279 -0
  35. chrys/app/features/buddy/portrait.py +76 -0
  36. chrys/app/features/buddy/progression.py +58 -0
  37. chrys/app/features/buddy/replies.py +191 -0
  38. chrys/app/features/buddy/sprites/alien.toml +70 -0
  39. chrys/app/features/buddy/sprites/axolotl.toml +71 -0
  40. chrys/app/features/buddy/sprites/bat.toml +69 -0
  41. chrys/app/features/buddy/sprites/bee.toml +69 -0
  42. chrys/app/features/buddy/sprites/cactus.toml +73 -0
  43. chrys/app/features/buddy/sprites/capybara.toml +70 -0
  44. chrys/app/features/buddy/sprites/cat.toml +69 -0
  45. chrys/app/features/buddy/sprites/crab.toml +70 -0
  46. chrys/app/features/buddy/sprites/dragon.toml +71 -0
  47. chrys/app/features/buddy/sprites/duck.toml +69 -0
  48. chrys/app/features/buddy/sprites/fox.toml +69 -0
  49. chrys/app/features/buddy/sprites/frog.toml +70 -0
  50. chrys/app/features/buddy/sprites/ghost.toml +70 -0
  51. chrys/app/features/buddy/sprites/goose.toml +69 -0
  52. chrys/app/features/buddy/sprites/jelly.toml +70 -0
  53. chrys/app/features/buddy/sprites/llama.toml +70 -0
  54. chrys/app/features/buddy/sprites/mushroom.toml +69 -0
  55. chrys/app/features/buddy/sprites/octopus.toml +70 -0
  56. chrys/app/features/buddy/sprites/owl.toml +70 -0
  57. chrys/app/features/buddy/sprites/panda.toml +71 -0
  58. chrys/app/features/buddy/sprites/penguin.toml +69 -0
  59. chrys/app/features/buddy/sprites/rabbit.toml +70 -0
  60. chrys/app/features/buddy/sprites/robot.toml +72 -0
  61. chrys/app/features/buddy/sprites/shark.toml +70 -0
  62. chrys/app/features/buddy/sprites/snail.toml +69 -0
  63. chrys/app/features/buddy/sprites/snake.toml +69 -0
  64. chrys/app/features/buddy/sprites/tabby.toml +69 -0
  65. chrys/app/features/buddy/sprites/turtle.toml +69 -0
  66. chrys/app/features/buddy/store.py +143 -0
  67. chrys/app/features/session_title/__init__.py +7 -0
  68. chrys/app/features/session_title/updater.py +582 -0
  69. chrys/app/installer.py +651 -0
  70. chrys/app/parsing.py +17 -0
  71. chrys/app/tui/__init__.py +3 -0
  72. chrys/app/tui/app.py +1287 -0
  73. chrys/app/tui/behaviors/__init__.py +3 -0
  74. chrys/app/tui/behaviors/click_outside_dismiss.py +90 -0
  75. chrys/app/tui/behaviors/insert_clipboard.py +73 -0
  76. chrys/app/tui/behaviors/right_click_copy.py +146 -0
  77. chrys/app/tui/binding_display.py +139 -0
  78. chrys/app/tui/buddy_messages.py +17 -0
  79. chrys/app/tui/buddy_reply.py +86 -0
  80. chrys/app/tui/chrys.tcss +549 -0
  81. chrys/app/tui/clipboard.py +109 -0
  82. chrys/app/tui/copy_messages.py +10 -0
  83. chrys/app/tui/i18n.py +230 -0
  84. chrys/app/tui/language.py +58 -0
  85. chrys/app/tui/notifications/__init__.py +12 -0
  86. chrys/app/tui/notifications/drivers.py +180 -0
  87. chrys/app/tui/notifications/service.py +126 -0
  88. chrys/app/tui/notifications/settings.py +141 -0
  89. chrys/app/tui/screens/__init__.py +3 -0
  90. chrys/app/tui/screens/agents/__init__.py +8 -0
  91. chrys/app/tui/screens/agents/agent_draft_store.py +937 -0
  92. chrys/app/tui/screens/agents/config.py +2142 -0
  93. chrys/app/tui/screens/agents/config.tcss +169 -0
  94. chrys/app/tui/screens/agents/panels/__init__.py +3 -0
  95. chrys/app/tui/screens/agents/panels/acp.py +1176 -0
  96. chrys/app/tui/screens/agents/panels/basic.py +497 -0
  97. chrys/app/tui/screens/agents/panels/compaction.py +231 -0
  98. chrys/app/tui/screens/agents/panels/config_card.py +218 -0
  99. chrys/app/tui/screens/agents/panels/instructions.py +91 -0
  100. chrys/app/tui/screens/agents/panels/markdown_report.py +76 -0
  101. chrys/app/tui/screens/agents/panels/mcp.py +2262 -0
  102. chrys/app/tui/screens/agents/panels/memory.py +684 -0
  103. chrys/app/tui/screens/agents/panels/path_entry.py +227 -0
  104. chrys/app/tui/screens/agents/panels/skills.py +806 -0
  105. chrys/app/tui/screens/agents/panels/subagents.py +652 -0
  106. chrys/app/tui/screens/agents/panels/tools.py +205 -0
  107. chrys/app/tui/screens/agents/picker.py +103 -0
  108. chrys/app/tui/screens/agents/skill_paths.py +38 -0
  109. chrys/app/tui/screens/agents/validation_messages.py +452 -0
  110. chrys/app/tui/screens/dialogs/__init__.py +35 -0
  111. chrys/app/tui/screens/dialogs/agent_load.py +834 -0
  112. chrys/app/tui/screens/dialogs/agent_load.tcss +78 -0
  113. chrys/app/tui/screens/dialogs/app_mode.py +55 -0
  114. chrys/app/tui/screens/dialogs/approval/__init__.py +15 -0
  115. chrys/app/tui/screens/dialogs/approval/approval.tcss +311 -0
  116. chrys/app/tui/screens/dialogs/approval/bodies/__init__.py +3 -0
  117. chrys/app/tui/screens/dialogs/approval/bodies/file_edit.py +210 -0
  118. chrys/app/tui/screens/dialogs/approval/bodies/sub_agent.py +82 -0
  119. chrys/app/tui/screens/dialogs/approval/body.py +110 -0
  120. chrys/app/tui/screens/dialogs/approval/dialog.py +565 -0
  121. chrys/app/tui/screens/dialogs/approval/mode.py +105 -0
  122. chrys/app/tui/screens/dialogs/ask_user.py +148 -0
  123. chrys/app/tui/screens/dialogs/ask_user.tcss +62 -0
  124. chrys/app/tui/screens/dialogs/base.py +84 -0
  125. chrys/app/tui/screens/dialogs/confirm.py +184 -0
  126. chrys/app/tui/screens/dialogs/confirm.tcss +46 -0
  127. chrys/app/tui/screens/dialogs/connection_test.py +183 -0
  128. chrys/app/tui/screens/dialogs/connection_test.tcss +92 -0
  129. chrys/app/tui/screens/dialogs/diagram.py +102 -0
  130. chrys/app/tui/screens/dialogs/diagram.tcss +24 -0
  131. chrys/app/tui/screens/dialogs/editor.py +378 -0
  132. chrys/app/tui/screens/dialogs/editor.tcss +132 -0
  133. chrys/app/tui/screens/dialogs/file_picker.py +582 -0
  134. chrys/app/tui/screens/dialogs/file_picker.tcss +78 -0
  135. chrys/app/tui/screens/dialogs/fork_session.py +219 -0
  136. chrys/app/tui/screens/dialogs/fork_session.tcss +75 -0
  137. chrys/app/tui/screens/dialogs/image_compression.py +102 -0
  138. chrys/app/tui/screens/dialogs/image_compression.tcss +28 -0
  139. chrys/app/tui/screens/dialogs/man_page.py +172 -0
  140. chrys/app/tui/screens/dialogs/man_page.tcss +39 -0
  141. chrys/app/tui/screens/dialogs/new_folder.py +277 -0
  142. chrys/app/tui/screens/dialogs/new_folder.tcss +51 -0
  143. chrys/app/tui/screens/dialogs/notice.tcss +14 -0
  144. chrys/app/tui/screens/dialogs/prompt_cache.py +144 -0
  145. chrys/app/tui/screens/dialogs/prompt_cache.tcss +38 -0
  146. chrys/app/tui/screens/dialogs/runtime_details.py +371 -0
  147. chrys/app/tui/screens/dialogs/runtime_details.tcss +49 -0
  148. chrys/app/tui/screens/dialogs/session_title.py +110 -0
  149. chrys/app/tui/screens/dialogs/session_title.tcss +43 -0
  150. chrys/app/tui/screens/dialogs/tool_view.py +207 -0
  151. chrys/app/tui/screens/dialogs/tool_view.tcss +43 -0
  152. chrys/app/tui/screens/dialogs/vision_unsupported.py +122 -0
  153. chrys/app/tui/screens/dialogs/vision_unsupported.tcss +35 -0
  154. chrys/app/tui/screens/dialogs/workflow_confirm.py +91 -0
  155. chrys/app/tui/screens/dialogs/workflow_confirm.tcss +17 -0
  156. chrys/app/tui/screens/dialogs/workflow_input.py +258 -0
  157. chrys/app/tui/screens/dialogs/workflow_input.tcss +73 -0
  158. chrys/app/tui/screens/dialogs/workflow_node.py +462 -0
  159. chrys/app/tui/screens/dialogs/workflow_node.tcss +41 -0
  160. chrys/app/tui/screens/dialogs/workflow_picker.py +46 -0
  161. chrys/app/tui/screens/dialogs/workflow_result.py +136 -0
  162. chrys/app/tui/screens/dialogs/workflow_result.tcss +14 -0
  163. chrys/app/tui/screens/diff/__init__.py +22 -0
  164. chrys/app/tui/screens/diff/rollback_modal.py +1381 -0
  165. chrys/app/tui/screens/diff/rollback_modal.tcss +248 -0
  166. chrys/app/tui/screens/diff/screen.py +1532 -0
  167. chrys/app/tui/screens/diff/screen.tcss +104 -0
  168. chrys/app/tui/screens/guides/__init__.py +7 -0
  169. chrys/app/tui/screens/guides/_docs/en/guides/configuration/agents.md +178 -0
  170. chrys/app/tui/screens/guides/_docs/en/guides/configuration/approval.md +117 -0
  171. chrys/app/tui/screens/guides/_docs/en/guides/configuration/chat-options.md +276 -0
  172. chrys/app/tui/screens/guides/_docs/en/guides/configuration/compaction.md +42 -0
  173. chrys/app/tui/screens/guides/_docs/en/guides/configuration/memory.md +48 -0
  174. chrys/app/tui/screens/guides/_docs/en/guides/configuration/models.md +158 -0
  175. chrys/app/tui/screens/guides/_docs/en/guides/configuration/settings.md +210 -0
  176. chrys/app/tui/screens/guides/_docs/en/guides/configuration/themes.md +61 -0
  177. chrys/app/tui/screens/guides/_docs/en/guides/configuration/tools.md +60 -0
  178. chrys/app/tui/screens/guides/_docs/en/guides/configuration/web-tools.md +286 -0
  179. chrys/app/tui/screens/guides/_docs/en/guides/daily-use/sessions.md +77 -0
  180. chrys/app/tui/screens/guides/_docs/en/guides/daily-use/workspaces.md +51 -0
  181. chrys/app/tui/screens/guides/_docs/en/guides/extensions/external-acp-agents.md +126 -0
  182. chrys/app/tui/screens/guides/_docs/en/guides/extensions/hooks.md +167 -0
  183. chrys/app/tui/screens/guides/_docs/en/guides/extensions/mcp.md +95 -0
  184. chrys/app/tui/screens/guides/_docs/en/guides/extensions/skills.md +175 -0
  185. chrys/app/tui/screens/guides/_docs/en/guides/running/icode-acp.md +61 -0
  186. chrys/app/tui/screens/guides/_docs/en/guides/running/icode-run.md +190 -0
  187. chrys/app/tui/screens/guides/_docs/en/guides/running/icode-serve.md +239 -0
  188. chrys/app/tui/screens/guides/_docs/en/guides/running/workflows.md +664 -0
  189. chrys/app/tui/screens/guides/_docs/en/reference/agent-profile.md +548 -0
  190. chrys/app/tui/screens/guides/_docs/en/reference/hooks.md +602 -0
  191. chrys/app/tui/screens/guides/_docs/en/reference/opentelemetry.md +177 -0
  192. chrys/app/tui/screens/guides/_docs/en/reference/settings.md +245 -0
  193. chrys/app/tui/screens/guides/_docs/en/reference/themes.md +149 -0
  194. chrys/app/tui/screens/guides/_docs/en/reference/tool-kinds-and-names.md +58 -0
  195. chrys/app/tui/screens/guides/_docs/en/reference/tui-slash-commands.md +76 -0
  196. chrys/app/tui/screens/guides/_docs/en/reference/workflows.md +1006 -0
  197. chrys/app/tui/screens/guides/_docs/en/start/getting-started.md +170 -0
  198. chrys/app/tui/screens/guides/_docs/en/start/what-is-icode.md +34 -0
  199. chrys/app/tui/screens/guides/_docs/index.yaml +100 -0
  200. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/agents.md +178 -0
  201. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/approval.md +117 -0
  202. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/chat-options.md +276 -0
  203. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/compaction.md +42 -0
  204. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/memory.md +48 -0
  205. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/models.md +158 -0
  206. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/settings.md +210 -0
  207. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/themes.md +61 -0
  208. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/tools.md +60 -0
  209. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/web-tools.md +286 -0
  210. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/daily-use/sessions.md +77 -0
  211. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/daily-use/workspaces.md +51 -0
  212. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/external-acp-agents.md +126 -0
  213. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/hooks.md +167 -0
  214. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/mcp.md +95 -0
  215. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/skills.md +175 -0
  216. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/icode-acp.md +61 -0
  217. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/icode-run.md +190 -0
  218. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/icode-serve.md +239 -0
  219. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/workflows.md +664 -0
  220. chrys/app/tui/screens/guides/_docs/zh-Hans/reference/agent-profile.md +548 -0
  221. chrys/app/tui/screens/guides/_docs/zh-Hans/reference/hooks.md +602 -0
  222. chrys/app/tui/screens/guides/_docs/zh-Hans/reference/opentelemetry.md +177 -0
  223. chrys/app/tui/screens/guides/_docs/zh-Hans/reference/settings.md +245 -0
  224. chrys/app/tui/screens/guides/_docs/zh-Hans/reference/themes.md +149 -0
  225. chrys/app/tui/screens/guides/_docs/zh-Hans/reference/tool-kinds-and-names.md +58 -0
  226. chrys/app/tui/screens/guides/_docs/zh-Hans/reference/tui-slash-commands.md +76 -0
  227. chrys/app/tui/screens/guides/_docs/zh-Hans/reference/workflows.md +1005 -0
  228. chrys/app/tui/screens/guides/_docs/zh-Hans/start/getting-started.md +170 -0
  229. chrys/app/tui/screens/guides/_docs/zh-Hans/start/what-is-icode.md +34 -0
  230. chrys/app/tui/screens/guides/index.py +215 -0
  231. chrys/app/tui/screens/guides/reader.py +173 -0
  232. chrys/app/tui/screens/guides/screen.py +442 -0
  233. chrys/app/tui/screens/guides/screen.tcss +52 -0
  234. chrys/app/tui/screens/logs/__init__.py +7 -0
  235. chrys/app/tui/screens/logs/screen.py +504 -0
  236. chrys/app/tui/screens/logs/screen.tcss +45 -0
  237. chrys/app/tui/screens/main/__init__.py +7 -0
  238. chrys/app/tui/screens/main/action_availability.py +30 -0
  239. chrys/app/tui/screens/main/buddy_command.py +143 -0
  240. chrys/app/tui/screens/main/commands.py +1073 -0
  241. chrys/app/tui/screens/main/config_actions.py +464 -0
  242. chrys/app/tui/screens/main/copy_actions.py +117 -0
  243. chrys/app/tui/screens/main/dialog_controllers.py +826 -0
  244. chrys/app/tui/screens/main/dialog_gateway.py +166 -0
  245. chrys/app/tui/screens/main/diff_controller.py +478 -0
  246. chrys/app/tui/screens/main/engine_read_model.py +184 -0
  247. chrys/app/tui/screens/main/event_handlers.py +2014 -0
  248. chrys/app/tui/screens/main/input_flow.py +241 -0
  249. chrys/app/tui/screens/main/live_diff.py +244 -0
  250. chrys/app/tui/screens/main/main_screen_presenter.py +295 -0
  251. chrys/app/tui/screens/main/model_indicator.py +217 -0
  252. chrys/app/tui/screens/main/navigation.py +343 -0
  253. chrys/app/tui/screens/main/ports.py +571 -0
  254. chrys/app/tui/screens/main/recent_dirs.py +123 -0
  255. chrys/app/tui/screens/main/rollback_controller.py +586 -0
  256. chrys/app/tui/screens/main/runtime_info.py +71 -0
  257. chrys/app/tui/screens/main/screen.py +2265 -0
  258. chrys/app/tui/screens/main/screen.tcss +21 -0
  259. chrys/app/tui/screens/main/session_handlers.py +1127 -0
  260. chrys/app/tui/screens/main/session_title.py +239 -0
  261. chrys/app/tui/screens/main/settings_coordinator.py +365 -0
  262. chrys/app/tui/screens/main/settings_persistence.py +184 -0
  263. chrys/app/tui/screens/main/shell_mode.py +112 -0
  264. chrys/app/tui/screens/main/state.py +289 -0
  265. chrys/app/tui/screens/main/submit_state.py +11 -0
  266. chrys/app/tui/screens/main/subscriptions.py +177 -0
  267. chrys/app/tui/screens/main/suggestions.py +982 -0
  268. chrys/app/tui/screens/main/tool_action_bridge.py +75 -0
  269. chrys/app/tui/screens/main/view_adapter.py +1567 -0
  270. chrys/app/tui/screens/main/workflow_browser.py +747 -0
  271. chrys/app/tui/screens/main/workflow_content.py +248 -0
  272. chrys/app/tui/screens/main/workflow_controller.py +240 -0
  273. chrys/app/tui/screens/main/workflow_feedback.py +137 -0
  274. chrys/app/tui/screens/main/workflow_flow.py +49 -0
  275. chrys/app/tui/screens/main/workflow_run_control.py +456 -0
  276. chrys/app/tui/screens/main/workflow_session_view.py +316 -0
  277. chrys/app/tui/screens/main/workspace_actions.py +272 -0
  278. chrys/app/tui/screens/main/workspace_branch.py +194 -0
  279. chrys/app/tui/screens/menus/__init__.py +8 -0
  280. chrys/app/tui/screens/menus/languages.py +114 -0
  281. chrys/app/tui/screens/models/__init__.py +7 -0
  282. chrys/app/tui/screens/models/picker.py +147 -0
  283. chrys/app/tui/screens/models/screen.py +2043 -0
  284. chrys/app/tui/screens/models/screen.tcss +255 -0
  285. chrys/app/tui/screens/sessions/__init__.py +8 -0
  286. chrys/app/tui/screens/sessions/presenter.py +586 -0
  287. chrys/app/tui/screens/sessions/screen.py +779 -0
  288. chrys/app/tui/screens/sessions/screen.tcss +127 -0
  289. chrys/app/tui/screens/settings/__init__.py +29 -0
  290. chrys/app/tui/screens/settings/danger.py +26 -0
  291. chrys/app/tui/screens/settings/dialog.py +281 -0
  292. chrys/app/tui/screens/settings/layout.py +519 -0
  293. chrys/app/tui/screens/settings/migrate_dialog.py +253 -0
  294. chrys/app/tui/screens/settings/panes/__init__.py +3 -0
  295. chrys/app/tui/screens/settings/panes/notifications.py +248 -0
  296. chrys/app/tui/screens/settings/panes/sessions.py +148 -0
  297. chrys/app/tui/screens/settings/ports.py +55 -0
  298. chrys/app/tui/screens/settings/provenance.py +102 -0
  299. chrys/app/tui/screens/settings/rows.py +546 -0
  300. chrys/app/tui/screens/settings/settings.tcss +406 -0
  301. chrys/app/tui/screens/themes/__init__.py +8 -0
  302. chrys/app/tui/screens/themes/dialogs.py +330 -0
  303. chrys/app/tui/screens/themes/editor.py +632 -0
  304. chrys/app/tui/screens/themes/editor.tcss +268 -0
  305. chrys/app/tui/screens/themes/messages.py +95 -0
  306. chrys/app/tui/screens/themes/palette.py +694 -0
  307. chrys/app/tui/screens/themes/panel.py +113 -0
  308. chrys/app/tui/screens/themes/picker.py +203 -0
  309. chrys/app/tui/screens/themes/save.py +191 -0
  310. chrys/app/tui/support/__init__.py +3 -0
  311. chrys/app/tui/support/file_manager.py +64 -0
  312. chrys/app/tui/support/gc_freeze.py +1114 -0
  313. chrys/app/tui/support/path_shortcuts.py +50 -0
  314. chrys/app/tui/support/prompt_history.py +298 -0
  315. chrys/app/tui/support/workspace_mru.py +476 -0
  316. chrys/app/tui/terminal/__init__.py +6 -0
  317. chrys/app/tui/terminal/_pty_backend.py +114 -0
  318. chrys/app/tui/terminal/emulator/__init__.py +24 -0
  319. chrys/app/tui/terminal/emulator/buffer.py +456 -0
  320. chrys/app/tui/terminal/emulator/charsets.py +113 -0
  321. chrys/app/tui/terminal/emulator/core.py +963 -0
  322. chrys/app/tui/terminal/emulator/input.py +342 -0
  323. chrys/app/tui/terminal/emulator/parser.py +227 -0
  324. chrys/app/tui/terminal/emulator/pen.py +158 -0
  325. chrys/app/tui/terminal/launcher.py +455 -0
  326. chrys/app/tui/terminal/leaked_reports.py +44 -0
  327. chrys/app/tui/terminal/panel.py +323 -0
  328. chrys/app/tui/terminal/pty_process.py +405 -0
  329. chrys/app/tui/terminal/rendering.py +151 -0
  330. chrys/app/tui/terminal/shell.py +273 -0
  331. chrys/app/tui/terminal/shell_integration.py +199 -0
  332. chrys/app/tui/terminal/title.py +224 -0
  333. chrys/app/tui/terminal/widget.py +654 -0
  334. chrys/app/tui/theme.py +422 -0
  335. chrys/app/tui/theme_loader.py +201 -0
  336. chrys/app/tui/theme_stylesheet.py +49 -0
  337. chrys/app/tui/themes/__init__.py +3 -0
  338. chrys/app/tui/themes/document.py +120 -0
  339. chrys/app/tui/themes/preview.py +170 -0
  340. chrys/app/tui/themes/store.py +176 -0
  341. chrys/app/tui/toasts.py +69 -0
  342. chrys/app/tui/util/__init__.py +3 -0
  343. chrys/app/tui/util/context_pressure.py +146 -0
  344. chrys/app/tui/util/debug_log.py +292 -0
  345. chrys/app/tui/util/diff_entries.py +64 -0
  346. chrys/app/tui/util/formatting.py +53 -0
  347. chrys/app/tui/util/git_branch.py +326 -0
  348. chrys/app/tui/util/invocation_progress.py +57 -0
  349. chrys/app/tui/util/logo.py +32 -0
  350. chrys/app/tui/util/message_gate.py +35 -0
  351. chrys/app/tui/util/mutation_sources.py +14 -0
  352. chrys/app/tui/util/picker_text.py +27 -0
  353. chrys/app/tui/util/removal.py +47 -0
  354. chrys/app/tui/util/rich_style.py +36 -0
  355. chrys/app/tui/util/source_text.py +12 -0
  356. chrys/app/tui/util/static_update.py +88 -0
  357. chrys/app/tui/util/todo_format.py +51 -0
  358. chrys/app/tui/util/visibility.py +214 -0
  359. chrys/app/tui/widgets/__init__.py +60 -0
  360. chrys/app/tui/widgets/ask_user_controls.py +652 -0
  361. chrys/app/tui/widgets/ask_user_markdown.py +17 -0
  362. chrys/app/tui/widgets/ask_user_prompt.py +606 -0
  363. chrys/app/tui/widgets/buttons.py +52 -0
  364. chrys/app/tui/widgets/chat/__init__.py +3 -0
  365. chrys/app/tui/widgets/chat/agent_transcript_surface.py +818 -0
  366. chrys/app/tui/widgets/chat/chrome.py +296 -0
  367. chrys/app/tui/widgets/chat/compaction_card.py +353 -0
  368. chrys/app/tui/widgets/chat/context_fold.py +77 -0
  369. chrys/app/tui/widgets/chat/file_snapshot.py +91 -0
  370. chrys/app/tui/widgets/chat/image_preview.py +214 -0
  371. chrys/app/tui/widgets/chat/live_renderer.py +383 -0
  372. chrys/app/tui/widgets/chat/messages.py +1039 -0
  373. chrys/app/tui/widgets/chat/panel.py +1769 -0
  374. chrys/app/tui/widgets/chat/ports.py +245 -0
  375. chrys/app/tui/widgets/chat/renderers/__init__.py +7 -0
  376. chrys/app/tui/widgets/chat/renderers/ask_user.py +747 -0
  377. chrys/app/tui/widgets/chat/renderers/execute.py +431 -0
  378. chrys/app/tui/widgets/chat/renderers/file_edit.py +925 -0
  379. chrys/app/tui/widgets/chat/renderers/hosted_code.py +83 -0
  380. chrys/app/tui/widgets/chat/renderers/hosted_generic.py +330 -0
  381. chrys/app/tui/widgets/chat/renderers/hosted_image.py +107 -0
  382. chrys/app/tui/widgets/chat/renderers/hosted_mcp.py +43 -0
  383. chrys/app/tui/widgets/chat/renderers/hosted_search.py +167 -0
  384. chrys/app/tui/widgets/chat/renderers/hosted_shell.py +132 -0
  385. chrys/app/tui/widgets/chat/renderers/read_file.py +399 -0
  386. chrys/app/tui/widgets/chat/renderers/search.py +348 -0
  387. chrys/app/tui/widgets/chat/renderers/skill.py +564 -0
  388. chrys/app/tui/widgets/chat/renderers/sleep.py +287 -0
  389. chrys/app/tui/widgets/chat/renderers/sub_agent.py +1385 -0
  390. chrys/app/tui/widgets/chat/renderers/todo.py +114 -0
  391. chrys/app/tui/widgets/chat/renderers/web.py +69 -0
  392. chrys/app/tui/widgets/chat/replay.py +1605 -0
  393. chrys/app/tui/widgets/chat/replay_mount.py +322 -0
  394. chrys/app/tui/widgets/chat/scroll_controller.py +579 -0
  395. chrys/app/tui/widgets/chat/selection_controller.py +435 -0
  396. chrys/app/tui/widgets/chat/session_json.py +508 -0
  397. chrys/app/tui/widgets/chat/toc_model.py +156 -0
  398. chrys/app/tui/widgets/chat/tool_call.py +2478 -0
  399. chrys/app/tui/widgets/chat/tool_registry.py +753 -0
  400. chrys/app/tui/widgets/chat/tool_renderers.py +247 -0
  401. chrys/app/tui/widgets/chat/tool_view_builders.py +260 -0
  402. chrys/app/tui/widgets/chat/transcript_reader.py +57 -0
  403. chrys/app/tui/widgets/checkbox.py +24 -0
  404. chrys/app/tui/widgets/chrome/__init__.py +3 -0
  405. chrys/app/tui/widgets/chrome/app_header.py +283 -0
  406. chrys/app/tui/widgets/chrome/commands.py +95 -0
  407. chrys/app/tui/widgets/chrome/file_index.py +68 -0
  408. chrys/app/tui/widgets/chrome/file_scanner.py +670 -0
  409. chrys/app/tui/widgets/chrome/footer.py +284 -0
  410. chrys/app/tui/widgets/chrome/image_paste.py +351 -0
  411. chrys/app/tui/widgets/chrome/input_bar.py +1299 -0
  412. chrys/app/tui/widgets/chrome/status_bar.py +911 -0
  413. chrys/app/tui/widgets/chrome/suggestion_list.py +652 -0
  414. chrys/app/tui/widgets/click_affordance.py +24 -0
  415. chrys/app/tui/widgets/color_picker/__init__.py +254 -0
  416. chrys/app/tui/widgets/color_picker/controls.py +171 -0
  417. chrys/app/tui/widgets/color_picker/model.py +89 -0
  418. chrys/app/tui/widgets/dialog_buttons.py +58 -0
  419. chrys/app/tui/widgets/dialog_scroll.py +24 -0
  420. chrys/app/tui/widgets/diff_view/__init__.py +20 -0
  421. chrys/app/tui/widgets/diff_view/cells.py +59 -0
  422. chrys/app/tui/widgets/diff_view/code.py +280 -0
  423. chrys/app/tui/widgets/diff_view/compute.py +286 -0
  424. chrys/app/tui/widgets/diff_view/gutter.py +161 -0
  425. chrys/app/tui/widgets/diff_view/inline.py +77 -0
  426. chrys/app/tui/widgets/diff_view/palette.py +87 -0
  427. chrys/app/tui/widgets/diff_view/protocols.py +24 -0
  428. chrys/app/tui/widgets/diff_view/rows.py +138 -0
  429. chrys/app/tui/widgets/diff_view/unified.py +267 -0
  430. chrys/app/tui/widgets/diff_view/widget.py +209 -0
  431. chrys/app/tui/widgets/editor/__init__.py +79 -0
  432. chrys/app/tui/widgets/editor/coordinates.py +30 -0
  433. chrys/app/tui/widgets/editor/highlighter.py +139 -0
  434. chrys/app/tui/widgets/editor/keymaps.py +357 -0
  435. chrys/app/tui/widgets/editor/types.py +216 -0
  436. chrys/app/tui/widgets/editor/vim.py +1195 -0
  437. chrys/app/tui/widgets/editor/widget.py +401 -0
  438. chrys/app/tui/widgets/file_preview.py +58 -0
  439. chrys/app/tui/widgets/hatch.py +138 -0
  440. chrys/app/tui/widgets/input.py +66 -0
  441. chrys/app/tui/widgets/loading.py +125 -0
  442. chrys/app/tui/widgets/markdown/__init__.py +25 -0
  443. chrys/app/tui/widgets/markdown/_utils.py +21 -0
  444. chrys/app/tui/widgets/markdown/anchors.py +71 -0
  445. chrys/app/tui/widgets/markdown/blocks.py +115 -0
  446. chrys/app/tui/widgets/markdown/copyable.py +163 -0
  447. chrys/app/tui/widgets/markdown/diagram/__init__.py +35 -0
  448. chrys/app/tui/widgets/markdown/diagram/canvas.py +409 -0
  449. chrys/app/tui/widgets/markdown/diagram/charts.py +52 -0
  450. chrys/app/tui/widgets/markdown/diagram/diagnostics.py +101 -0
  451. chrys/app/tui/widgets/markdown/diagram/geometry.py +46 -0
  452. chrys/app/tui/widgets/markdown/diagram/layout.py +494 -0
  453. chrys/app/tui/widgets/markdown/diagram/messages.py +18 -0
  454. chrys/app/tui/widgets/markdown/diagram/model.py +275 -0
  455. chrys/app/tui/widgets/markdown/diagram/parser.py +173 -0
  456. chrys/app/tui/widgets/markdown/diagram/parsers/__init__.py +3 -0
  457. chrys/app/tui/widgets/markdown/diagram/parsers/charts.py +349 -0
  458. chrys/app/tui/widgets/markdown/diagram/parsers/common.py +403 -0
  459. chrys/app/tui/widgets/markdown/diagram/parsers/graphs.py +1072 -0
  460. chrys/app/tui/widgets/markdown/diagram/parsers/planning.py +317 -0
  461. chrys/app/tui/widgets/markdown/diagram/parsers/schedule.py +298 -0
  462. chrys/app/tui/widgets/markdown/diagram/parsers/schemas.py +241 -0
  463. chrys/app/tui/widgets/markdown/diagram/parsers/structure.py +221 -0
  464. chrys/app/tui/widgets/markdown/diagram/presentation.py +32 -0
  465. chrys/app/tui/widgets/markdown/diagram/renderers/__init__.py +3 -0
  466. chrys/app/tui/widgets/markdown/diagram/renderers/charts.py +275 -0
  467. chrys/app/tui/widgets/markdown/diagram/renderers/common.py +73 -0
  468. chrys/app/tui/widgets/markdown/diagram/renderers/planning.py +157 -0
  469. chrys/app/tui/widgets/markdown/diagram/renderers/schedule.py +202 -0
  470. chrys/app/tui/widgets/markdown/diagram/renderers/structure.py +77 -0
  471. chrys/app/tui/widgets/markdown/diagram/renderers/xy.py +385 -0
  472. chrys/app/tui/widgets/markdown/diagram/router.py +651 -0
  473. chrys/app/tui/widgets/markdown/diagram/specs/__init__.py +3 -0
  474. chrys/app/tui/widgets/markdown/diagram/specs/charts.py +94 -0
  475. chrys/app/tui/widgets/markdown/diagram/specs/planning.py +68 -0
  476. chrys/app/tui/widgets/markdown/diagram/specs/schedule.py +65 -0
  477. chrys/app/tui/widgets/markdown/diagram/specs/structure.py +23 -0
  478. chrys/app/tui/widgets/markdown/diagram/viewer.py +92 -0
  479. chrys/app/tui/widgets/markdown/links.py +47 -0
  480. chrys/app/tui/widgets/markdown/math/__init__.py +36 -0
  481. chrys/app/tui/widgets/markdown/math/layout.py +739 -0
  482. chrys/app/tui/widgets/markdown/math/markdown.py +291 -0
  483. chrys/app/tui/widgets/markdown/math/parser.py +325 -0
  484. chrys/app/tui/widgets/markdown/math/symbols.py +325 -0
  485. chrys/app/tui/widgets/markdown/navigator.py +80 -0
  486. chrys/app/tui/widgets/markdown/parser.py +1057 -0
  487. chrys/app/tui/widgets/markdown/stream.py +73 -0
  488. chrys/app/tui/widgets/markdown/toc.py +98 -0
  489. chrys/app/tui/widgets/markdown/viewer.py +146 -0
  490. chrys/app/tui/widgets/markdown/widget.py +2046 -0
  491. chrys/app/tui/widgets/marquee.py +101 -0
  492. chrys/app/tui/widgets/option_menu.py +82 -0
  493. chrys/app/tui/widgets/page_navigator.py +118 -0
  494. chrys/app/tui/widgets/select.py +196 -0
  495. chrys/app/tui/widgets/selection.py +59 -0
  496. chrys/app/tui/widgets/sidebar/__init__.py +3 -0
  497. chrys/app/tui/widgets/sidebar/buddy.py +367 -0
  498. chrys/app/tui/widgets/sidebar/context.py +482 -0
  499. chrys/app/tui/widgets/sidebar/debug.py +195 -0
  500. chrys/app/tui/widgets/sidebar/empty_state.py +11 -0
  501. chrys/app/tui/widgets/sidebar/panel.py +198 -0
  502. chrys/app/tui/widgets/sidebar/tasks.py +198 -0
  503. chrys/app/tui/widgets/sidebar/toc.py +166 -0
  504. chrys/app/tui/widgets/sidebar/toc_types.py +30 -0
  505. chrys/app/tui/widgets/syntax_theme.py +85 -0
  506. chrys/app/tui/widgets/text_area.py +258 -0
  507. chrys/app/tui/widgets/trajectory/__init__.py +8 -0
  508. chrys/app/tui/widgets/trajectory/chartkit.py +264 -0
  509. chrys/app/tui/widgets/trajectory/insights.py +1266 -0
  510. chrys/app/tui/widgets/trajectory/overview.py +1101 -0
  511. chrys/app/tui/widgets/trajectory/panel.py +983 -0
  512. chrys/app/tui/widgets/trajectory/presentation.py +311 -0
  513. chrys/app/tui/widgets/trajectory/session_info.py +124 -0
  514. chrys/app/tui/widgets/trajectory/text_view.py +124 -0
  515. chrys/app/tui/widgets/trajectory/timeline.py +380 -0
  516. chrys/app/tui/widgets/welcome.py +126 -0
  517. chrys/app/tui/widgets/workflow/__init__.py +5 -0
  518. chrys/app/tui/widgets/workflow/edges.py +89 -0
  519. chrys/app/tui/widgets/workflow/graph.py +869 -0
  520. chrys/app/tui/widgets/workflow/info.py +310 -0
  521. chrys/app/tui/widgets/workflow/node_view.py +27 -0
  522. chrys/app/tui/widgets/workflow/output.py +225 -0
  523. chrys/app/tui/widgets/workflow/panel.py +658 -0
  524. chrys/app/tui/widgets/workflow/projector.py +324 -0
  525. chrys/app/tui/widgets/workflow/records.py +69 -0
  526. chrys/app/tui/widgets/workflow/scrollbar.py +82 -0
  527. chrys/app/tui/widgets/workflow/selection.py +151 -0
  528. chrys/app/tui/widgets/workflow/source.py +65 -0
  529. chrys/app/tui/widgets/workflow/text.py +257 -0
  530. chrys/app/tui/widgets/workflow/values.py +317 -0
  531. chrys/foundation/__init__.py +0 -0
  532. chrys/foundation/branding.py +15 -0
  533. chrys/foundation/config/__init__.py +7 -0
  534. chrys/foundation/config/coercion.py +374 -0
  535. chrys/foundation/config/context.py +34 -0
  536. chrys/foundation/config/env_file.py +141 -0
  537. chrys/foundation/config/env_layers.py +370 -0
  538. chrys/foundation/config/migrations.py +521 -0
  539. chrys/foundation/config/notification_events.py +22 -0
  540. chrys/foundation/config/process_settings.py +332 -0
  541. chrys/foundation/config/runtime_pointer.py +133 -0
  542. chrys/foundation/config/settings.py +1952 -0
  543. chrys/foundation/config/settings_store.py +996 -0
  544. chrys/foundation/config/spec.py +396 -0
  545. chrys/foundation/config/user_settings.py +140 -0
  546. chrys/foundation/config/warnings.py +262 -0
  547. chrys/foundation/config/web_values.py +135 -0
  548. chrys/foundation/config/yaml_store.py +487 -0
  549. chrys/foundation/errors/__init__.py +68 -0
  550. chrys/foundation/errors/_legacy.py +117 -0
  551. chrys/foundation/errors/_text.py +13 -0
  552. chrys/foundation/errors/_walk.py +85 -0
  553. chrys/foundation/errors/classify.py +278 -0
  554. chrys/foundation/errors/display.py +274 -0
  555. chrys/foundation/errors/formatting.py +283 -0
  556. chrys/foundation/errors/kinds.py +67 -0
  557. chrys/foundation/errors/network.py +497 -0
  558. chrys/foundation/errors/provider.py +445 -0
  559. chrys/foundation/errors/route.py +85 -0
  560. chrys/foundation/events/__init__.py +8 -0
  561. chrys/foundation/events/bus.py +143 -0
  562. chrys/foundation/events/types.py +1629 -0
  563. chrys/foundation/events/workflow.py +38 -0
  564. chrys/foundation/hosted_tools.py +142 -0
  565. chrys/foundation/i18n/__init__.py +32 -0
  566. chrys/foundation/i18n/_catalogs/zh-Hans/LC_MESSAGES/chrys.mo +0 -0
  567. chrys/foundation/i18n/catalogs.py +42 -0
  568. chrys/foundation/i18n/formatting.py +202 -0
  569. chrys/foundation/i18n/locale.py +92 -0
  570. chrys/foundation/i18n/localizer.py +187 -0
  571. chrys/foundation/i18n/messages.py +268 -0
  572. chrys/foundation/io/__init__.py +3 -0
  573. chrys/foundation/io/result_content.py +237 -0
  574. chrys/foundation/models/__init__.py +3 -0
  575. chrys/foundation/models/approval_reuse.py +23 -0
  576. chrys/foundation/models/ask_user.py +468 -0
  577. chrys/foundation/models/execution.py +18 -0
  578. chrys/foundation/models/history_markers.py +115 -0
  579. chrys/foundation/models/invocations.py +63 -0
  580. chrys/foundation/models/mutation_scope.py +28 -0
  581. chrys/foundation/models/session_env.py +87 -0
  582. chrys/foundation/models/session_surface.py +29 -0
  583. chrys/foundation/models/todos.py +74 -0
  584. chrys/foundation/models/turns.py +125 -0
  585. chrys/foundation/models/workflow_session.py +89 -0
  586. chrys/foundation/models/workspace.py +59 -0
  587. chrys/foundation/net/__init__.py +3 -0
  588. chrys/foundation/net/address_class.py +57 -0
  589. chrys/foundation/net/route_probe.py +94 -0
  590. chrys/foundation/net/url.py +107 -0
  591. chrys/foundation/observability/__init__.py +3 -0
  592. chrys/foundation/observability/exporters.py +174 -0
  593. chrys/foundation/observability/gate.py +31 -0
  594. chrys/foundation/observability/setup.py +497 -0
  595. chrys/foundation/observability/sink.py +228 -0
  596. chrys/foundation/patches/__init__.py +19 -0
  597. chrys/foundation/patches/mdurl_cache.py +59 -0
  598. chrys/foundation/patches/patcher.py +287 -0
  599. chrys/foundation/patches/staged_members.py +205 -0
  600. chrys/foundation/patches/textual_block_border.py +105 -0
  601. chrys/foundation/patches/textual_button_ansi.py +65 -0
  602. chrys/foundation/patches/textual_button_ansi_old.txt +68 -0
  603. chrys/foundation/patches/textual_callback_cache.py +248 -0
  604. chrys/foundation/patches/textual_callback_dispatch.py +109 -0
  605. chrys/foundation/patches/textual_compositor_cjk.py +566 -0
  606. chrys/foundation/patches/textual_dispatch_cache.py +150 -0
  607. chrys/foundation/patches/textual_ime_cursor_anchor.py +92 -0
  608. chrys/foundation/patches/textual_kitty_keyboard.py +268 -0
  609. chrys/foundation/patches/textual_lru_acyclic.py +241 -0
  610. chrys/foundation/patches/textual_message_pump.py +136 -0
  611. chrys/foundation/patches/textual_node_diet.py +875 -0
  612. chrys/foundation/patches/textual_one_shot_timer.py +48 -0
  613. chrys/foundation/patches/textual_option_list.py +90 -0
  614. chrys/foundation/patches/textual_precompose.py +89 -0
  615. chrys/foundation/patches/textual_pruned_tabs.py +69 -0
  616. chrys/foundation/patches/textual_reflow_reuse.py +1753 -0
  617. chrys/foundation/patches/textual_removed_node_caches.py +86 -0
  618. chrys/foundation/patches/textual_removed_screen_callbacks.py +55 -0
  619. chrys/foundation/patches/textual_selection_extract.py +48 -0
  620. chrys/foundation/patches/textual_strip_cycles.py +119 -0
  621. chrys/foundation/patches/textual_tab_selection.py +1611 -0
  622. chrys/foundation/patches/textual_timer_skip.py +70 -0
  623. chrys/foundation/patches/textual_utf8_decoder.py +106 -0
  624. chrys/foundation/patches/textual_win_sleep.py +167 -0
  625. chrys/foundation/patches/watchdog_fsevents.py +70 -0
  626. chrys/foundation/patches/watchdog_windows.py +52 -0
  627. chrys/foundation/platform/__init__.py +556 -0
  628. chrys/foundation/platform/c_api.py +32 -0
  629. chrys/foundation/platform/child_reap.py +107 -0
  630. chrys/foundation/platform/command_line.py +95 -0
  631. chrys/foundation/platform/files.py +1470 -0
  632. chrys/foundation/platform/output_capture.py +113 -0
  633. chrys/foundation/platform/paths.py +193 -0
  634. chrys/foundation/platform/process.py +1468 -0
  635. chrys/foundation/platform/pty_output.py +19 -0
  636. chrys/foundation/platform/runtime_paths.py +281 -0
  637. chrys/foundation/platform/win_console.py +129 -0
  638. chrys/foundation/reasoning_origin.py +67 -0
  639. chrys/foundation/recovery.py +16 -0
  640. chrys/foundation/retry.py +278 -0
  641. chrys/foundation/text/__init__.py +3 -0
  642. chrys/foundation/text/encoding.py +603 -0
  643. chrys/foundation/text/images.py +304 -0
  644. chrys/foundation/text/lines.py +24 -0
  645. chrys/foundation/text/mentions.py +90 -0
  646. chrys/foundation/text/model_json.py +25 -0
  647. chrys/foundation/text/reading_order.py +20 -0
  648. chrys/foundation/text/tokenizer.py +25 -0
  649. chrys/foundation/text/tool_output.py +287 -0
  650. chrys/foundation/text/yaml_io.py +62 -0
  651. chrys/foundation/tool_call_context.py +202 -0
  652. chrys/foundation/tool_execution_stamp.py +184 -0
  653. chrys/foundation/tool_invocation_order.py +48 -0
  654. chrys/foundation/tool_kinds.py +102 -0
  655. chrys/foundation/tool_result_metadata.py +287 -0
  656. chrys/foundation/trajectory/__init__.py +10 -0
  657. chrys/foundation/trajectory/context.py +481 -0
  658. chrys/foundation/trajectory/envelope.py +571 -0
  659. chrys/foundation/trajectory/event_types.py +294 -0
  660. chrys/foundation/trajectory/finalizers.py +46 -0
  661. chrys/foundation/trajectory/fingerprint.py +62 -0
  662. chrys/foundation/trajectory/ids.py +155 -0
  663. chrys/foundation/trajectory/keys.py +99 -0
  664. chrys/foundation/trajectory/lease.py +173 -0
  665. chrys/foundation/trajectory/metadata.py +72 -0
  666. chrys/foundation/trajectory/reader.py +271 -0
  667. chrys/foundation/trajectory/recovery.py +225 -0
  668. chrys/foundation/trajectory/revisions.py +152 -0
  669. chrys/foundation/trajectory/segments.py +225 -0
  670. chrys/foundation/trajectory/tools.py +117 -0
  671. chrys/foundation/trajectory/usage.py +151 -0
  672. chrys/foundation/trajectory/writer.py +972 -0
  673. chrys/foundation/trajectory_timing.py +97 -0
  674. chrys/foundation/util/__init__.py +3 -0
  675. chrys/foundation/util/async_tasks.py +29 -0
  676. chrys/foundation/util/chrys_headers.py +23 -0
  677. chrys/foundation/util/env_templates.py +104 -0
  678. chrys/foundation/util/filenames.py +96 -0
  679. chrys/foundation/util/header_charset.py +138 -0
  680. chrys/foundation/util/httpx_helpers.py +12 -0
  681. chrys/foundation/util/lock.py +236 -0
  682. chrys/foundation/util/once_close.py +46 -0
  683. chrys/foundation/util/session_ids.py +29 -0
  684. chrys/foundation/util/shell_tokens.py +88 -0
  685. chrys/foundation/util/sub_agent_context.py +38 -0
  686. chrys/foundation/util/time.py +35 -0
  687. chrys/foundation/util/unicode_scalars.py +36 -0
  688. chrys/foundation/vendor/__init__.py +43 -0
  689. chrys/foundation/vendor/ripgrep/rg-aarch64-apple-darwin +0 -0
  690. chrys/foundation/vendor/ripgrep/rg-aarch64-pc-windows-msvc.exe +0 -0
  691. chrys/foundation/vendor/ripgrep/rg-aarch64-unknown-linux-gnu +0 -0
  692. chrys/foundation/vendor/ripgrep/rg-x86_64-apple-darwin +0 -0
  693. chrys/foundation/vendor/ripgrep/rg-x86_64-pc-windows-msvc.exe +0 -0
  694. chrys/foundation/vendor/ripgrep/rg-x86_64-unknown-linux-musl +0 -0
  695. chrys/kernel/__init__.py +256 -0
  696. chrys/kernel/_content.py +1866 -0
  697. chrys/kernel/_loop_recorder.py +631 -0
  698. chrys/kernel/_null_overlay.py +1026 -0
  699. chrys/kernel/_result_ceiling.py +216 -0
  700. chrys/kernel/_serialization.py +431 -0
  701. chrys/kernel/_tool_arg_errors.py +1629 -0
  702. chrys/kernel/_tool_execution.py +1172 -0
  703. chrys/kernel/_tool_expansion.py +26 -0
  704. chrys/kernel/_types.py +2598 -0
  705. chrys/kernel/agent.py +994 -0
  706. chrys/kernel/client.py +1084 -0
  707. chrys/kernel/compaction.py +1315 -0
  708. chrys/kernel/exceptions.py +211 -0
  709. chrys/kernel/exchanges.py +683 -0
  710. chrys/kernel/identity.py +95 -0
  711. chrys/kernel/images.py +36 -0
  712. chrys/kernel/instrumentation.py +1618 -0
  713. chrys/kernel/loop.py +2308 -0
  714. chrys/kernel/middleware.py +616 -0
  715. chrys/kernel/sessions.py +604 -0
  716. chrys/kernel/tools.py +1061 -0
  717. chrys/kernel/types.py +126 -0
  718. chrys/orchestration/__init__.py +3 -0
  719. chrys/orchestration/engine/__init__.py +3 -0
  720. chrys/orchestration/engine/assembly.py +236 -0
  721. chrys/orchestration/engine/build/__init__.py +3 -0
  722. chrys/orchestration/engine/build/builder.py +1061 -0
  723. chrys/orchestration/engine/build/construction.py +408 -0
  724. chrys/orchestration/engine/build/loaded.py +139 -0
  725. chrys/orchestration/engine/engine.py +789 -0
  726. chrys/orchestration/engine/execution.py +771 -0
  727. chrys/orchestration/engine/loader.py +865 -0
  728. chrys/orchestration/engine/rollback.py +918 -0
  729. chrys/orchestration/engine/run/__init__.py +3 -0
  730. chrys/orchestration/engine/run/active_injection.py +419 -0
  731. chrys/orchestration/engine/run/attachments.py +519 -0
  732. chrys/orchestration/engine/run/bindings.py +1095 -0
  733. chrys/orchestration/engine/run/coordinator.py +1062 -0
  734. chrys/orchestration/engine/run/finalizer.py +417 -0
  735. chrys/orchestration/engine/run/input_refs.py +60 -0
  736. chrys/orchestration/engine/run/prompt_content.py +457 -0
  737. chrys/orchestration/engine/run/resume.py +299 -0
  738. chrys/orchestration/engine/run/retry.py +948 -0
  739. chrys/orchestration/engine/run/runner.py +625 -0
  740. chrys/orchestration/engine/run/runtime_skills.py +146 -0
  741. chrys/orchestration/engine/run/sub_agent_coordination.py +126 -0
  742. chrys/orchestration/engine/run/turn_hooks.py +258 -0
  743. chrys/orchestration/engine/run/turn_state.py +149 -0
  744. chrys/orchestration/engine/run/working_dir.py +50 -0
  745. chrys/orchestration/engine/session_lifecycle.py +2013 -0
  746. chrys/orchestration/engine/state/__init__.py +3 -0
  747. chrys/orchestration/engine/state/active_session.py +183 -0
  748. chrys/orchestration/engine/state/controls.py +639 -0
  749. chrys/orchestration/engine/state/current_agent.py +26 -0
  750. chrys/orchestration/engine/state/lifecycle_permits.py +395 -0
  751. chrys/orchestration/engine/state/machine.py +193 -0
  752. chrys/orchestration/engine/state/session_writer.py +434 -0
  753. chrys/orchestration/engine/trajectory.py +714 -0
  754. chrys/orchestration/engine/usage.py +118 -0
  755. chrys/orchestration/invoker/__init__.py +3 -0
  756. chrys/orchestration/invoker/acp.py +727 -0
  757. chrys/orchestration/invoker/acp_protocol.py +1115 -0
  758. chrys/orchestration/invoker/acp_spec.py +79 -0
  759. chrys/orchestration/invoker/attempts.py +994 -0
  760. chrys/orchestration/invoker/child_compaction.py +131 -0
  761. chrys/orchestration/invoker/child_history.py +99 -0
  762. chrys/orchestration/invoker/contracts.py +253 -0
  763. chrys/orchestration/invoker/evidence.py +94 -0
  764. chrys/orchestration/invoker/kernel.py +343 -0
  765. chrys/orchestration/invoker/origin.py +79 -0
  766. chrys/orchestration/invoker/resources.py +343 -0
  767. chrys/orchestration/invoker/runtime.py +299 -0
  768. chrys/orchestration/invoker/usage.py +80 -0
  769. chrys/orchestration/session_hooks.py +117 -0
  770. chrys/orchestration/session_host.py +1023 -0
  771. chrys/orchestration/session_resources.py +95 -0
  772. chrys/orchestration/session_usage.py +219 -0
  773. chrys/orchestration/startup.py +255 -0
  774. chrys/orchestration/sub_agents/__init__.py +3 -0
  775. chrys/orchestration/sub_agents/acp_policy.py +269 -0
  776. chrys/orchestration/sub_agents/kernel_policy.py +848 -0
  777. chrys/orchestration/sub_agents/registration.py +44 -0
  778. chrys/orchestration/sub_agents/shell.py +337 -0
  779. chrys/orchestration/sub_agents/tools.py +2228 -0
  780. chrys/orchestration/workflows/__init__.py +3 -0
  781. chrys/orchestration/workflows/agent_archive.py +156 -0
  782. chrys/orchestration/workflows/agent_node.py +894 -0
  783. chrys/orchestration/workflows/agent_node_build.py +687 -0
  784. chrys/orchestration/workflows/catalog.py +332 -0
  785. chrys/orchestration/workflows/coordinator.py +771 -0
  786. chrys/orchestration/workflows/hooks.py +176 -0
  787. chrys/orchestration/workflows/preview.py +322 -0
  788. chrys/orchestration/workflows/rollback.py +151 -0
  789. chrys/orchestration/workflows/runner.py +986 -0
  790. chrys/orchestration/workflows/session.py +390 -0
  791. chrys/orchestration/workflows/settings.py +97 -0
  792. chrys/orchestration/workflows/validation.py +674 -0
  793. chrys/orchestration/workflows/worker_client.py +1002 -0
  794. chrys/service/__init__.py +3 -0
  795. chrys/service/acp_client/__init__.py +65 -0
  796. chrys/service/acp_client/_transport.py +922 -0
  797. chrys/service/acp_client/client.py +1311 -0
  798. chrys/service/acp_client/errors.py +224 -0
  799. chrys/service/acp_client/protocol.py +220 -0
  800. chrys/service/acp_client/spawn.py +239 -0
  801. chrys/service/acp_client/spec.py +100 -0
  802. chrys/service/agent_middleware/__init__.py +60 -0
  803. chrys/service/agent_middleware/_metadata_keys.py +52 -0
  804. chrys/service/agent_middleware/control/__init__.py +3 -0
  805. chrys/service/agent_middleware/control/approval.py +769 -0
  806. chrys/service/agent_middleware/control/approval_hooks.py +62 -0
  807. chrys/service/agent_middleware/control/ask_user.py +207 -0
  808. chrys/service/agent_middleware/control/interrupt.py +50 -0
  809. chrys/service/agent_middleware/control/sleep.py +118 -0
  810. chrys/service/agent_middleware/control/todo.py +117 -0
  811. chrys/service/agent_middleware/events/__init__.py +3 -0
  812. chrys/service/agent_middleware/events/hook_dispatch.py +250 -0
  813. chrys/service/agent_middleware/events/hosted_tools.py +1762 -0
  814. chrys/service/agent_middleware/events/intermediate_text.py +128 -0
  815. chrys/service/agent_middleware/events/rejection_metadata.py +50 -0
  816. chrys/service/agent_middleware/events/result_persistence.py +98 -0
  817. chrys/service/agent_middleware/events/sub_agent_events.py +951 -0
  818. chrys/service/agent_middleware/events/tool_error_text.py +22 -0
  819. chrys/service/agent_middleware/events/tool_events.py +779 -0
  820. chrys/service/agent_middleware/injection.py +510 -0
  821. chrys/service/agent_middleware/reminders/__init__.py +91 -0
  822. chrys/service/agent_middleware/reminders/archive_pointer.py +154 -0
  823. chrys/service/agent_middleware/reminders/base.py +25 -0
  824. chrys/service/agent_middleware/reminders/context_usage.py +74 -0
  825. chrys/service/agent_middleware/reminders/file_change.py +63 -0
  826. chrys/service/agent_middleware/reminders/mcp.py +39 -0
  827. chrys/service/agent_middleware/reminders/profile_switch.py +106 -0
  828. chrys/service/agent_middleware/reminders/runtime_env.py +190 -0
  829. chrys/service/agent_middleware/reminders/skills.py +44 -0
  830. chrys/service/agent_middleware/reminders/sub_agents.py +35 -0
  831. chrys/service/agent_middleware/reminders/todo.py +35 -0
  832. chrys/service/agent_middleware/reminders/turn_line.py +48 -0
  833. chrys/service/agent_middleware/response_validation.py +1380 -0
  834. chrys/service/agent_middleware/system_reminder.py +1107 -0
  835. chrys/service/agent_middleware/validators.py +396 -0
  836. chrys/service/analytics/__init__.py +124 -0
  837. chrys/service/analytics/_actions.py +896 -0
  838. chrys/service/analytics/_context_evidence.py +357 -0
  839. chrys/service/analytics/_critical_path.py +179 -0
  840. chrys/service/analytics/_facts.py +919 -0
  841. chrys/service/analytics/_insights.py +769 -0
  842. chrys/service/analytics/_metric_ops.py +83 -0
  843. chrys/service/analytics/_session_projection.py +460 -0
  844. chrys/service/analytics/_timeline.py +434 -0
  845. chrys/service/analytics/_turn_graph.py +1237 -0
  846. chrys/service/analytics/_turns.py +1377 -0
  847. chrys/service/analytics/_workflows.py +171 -0
  848. chrys/service/analytics/aggregation.py +1474 -0
  849. chrys/service/analytics/classification.py +128 -0
  850. chrys/service/analytics/export.py +803 -0
  851. chrys/service/analytics/findings.py +282 -0
  852. chrys/service/analytics/math.py +79 -0
  853. chrys/service/analytics/model.py +886 -0
  854. chrys/service/analytics/reader.py +321 -0
  855. chrys/service/approval/__init__.py +3 -0
  856. chrys/service/approval/arbitration.py +102 -0
  857. chrys/service/approval/command_identity.py +109 -0
  858. chrys/service/approval/correlation.py +57 -0
  859. chrys/service/approval/grant_store.py +108 -0
  860. chrys/service/approval/judge.py +589 -0
  861. chrys/service/approval/level.py +15 -0
  862. chrys/service/approval/policy.py +209 -0
  863. chrys/service/approval/reuse.py +223 -0
  864. chrys/service/approval/reuse_binding.py +133 -0
  865. chrys/service/approval/safety_classifier.py +536 -0
  866. chrys/service/approval/turn_context.py +49 -0
  867. chrys/service/context/__init__.py +3 -0
  868. chrys/service/context/compaction/__init__.py +38 -0
  869. chrys/service/context/compaction/breaker.py +156 -0
  870. chrys/service/context/compaction/budgets.py +93 -0
  871. chrys/service/context/compaction/calibration.py +77 -0
  872. chrys/service/context/compaction/compression.py +690 -0
  873. chrys/service/context/compaction/current_turn_drop.py +458 -0
  874. chrys/service/context/compaction/events.py +201 -0
  875. chrys/service/context/compaction/exclusions.py +261 -0
  876. chrys/service/context/compaction/groups.py +361 -0
  877. chrys/service/context/compaction/last_words.py +1996 -0
  878. chrys/service/context/compaction/last_words_state.py +752 -0
  879. chrys/service/context/compaction/scoped.py +472 -0
  880. chrys/service/context/compaction/spill.py +1947 -0
  881. chrys/service/context/compaction/strategy.py +1116 -0
  882. chrys/service/context/compaction/summaries.py +319 -0
  883. chrys/service/context/compaction/turns.py +375 -0
  884. chrys/service/context/manager.py +174 -0
  885. chrys/service/context/memory_loader.py +792 -0
  886. chrys/service/context/middleware/__init__.py +3 -0
  887. chrys/service/context/middleware/usage.py +332 -0
  888. chrys/service/context/providers/__init__.py +3 -0
  889. chrys/service/context/providers/context_management.py +432 -0
  890. chrys/service/context/providers/history.py +647 -0
  891. chrys/service/context/providers/memory.py +50 -0
  892. chrys/service/context/tool_content.py +22 -0
  893. chrys/service/hooks/__init__.py +37 -0
  894. chrys/service/hooks/detached_worker.py +118 -0
  895. chrys/service/hooks/docs/README.md +14 -0
  896. chrys/service/hooks/docs/authoring.md +684 -0
  897. chrys/service/hooks/docs/configuration.md +499 -0
  898. chrys/service/hooks/docs/design.md +359 -0
  899. chrys/service/hooks/docs/project-hooks.md +348 -0
  900. chrys/service/hooks/events.py +157 -0
  901. chrys/service/hooks/loader.py +665 -0
  902. chrys/service/hooks/manager.py +807 -0
  903. chrys/service/hooks/matcher.py +111 -0
  904. chrys/service/hooks/outbox.py +294 -0
  905. chrys/service/hooks/runner.py +572 -0
  906. chrys/service/hooks/schema.py +407 -0
  907. chrys/service/llm/__init__.py +7 -0
  908. chrys/service/llm/_structured_outputs.py +192 -0
  909. chrys/service/llm/anthropic_messages/__init__.py +9 -0
  910. chrys/service/llm/anthropic_messages/client.py +254 -0
  911. chrys/service/llm/anthropic_messages/decode.py +331 -0
  912. chrys/service/llm/anthropic_messages/history.py +279 -0
  913. chrys/service/llm/anthropic_messages/request.py +323 -0
  914. chrys/service/llm/anthropic_messages/server_tools.py +319 -0
  915. chrys/service/llm/anthropic_messages/stream.py +297 -0
  916. chrys/service/llm/anthropic_messages/thinking_binding.py +156 -0
  917. chrys/service/llm/chat_completions/__init__.py +9 -0
  918. chrys/service/llm/chat_completions/client.py +369 -0
  919. chrys/service/llm/chat_completions/decode.py +338 -0
  920. chrys/service/llm/chat_completions/history.py +657 -0
  921. chrys/service/llm/chat_completions/reasoning.py +244 -0
  922. chrys/service/llm/chat_completions/request.py +192 -0
  923. chrys/service/llm/chat_completions/stream.py +546 -0
  924. chrys/service/llm/chat_completions/validation.py +308 -0
  925. chrys/service/llm/clients.py +613 -0
  926. chrys/service/llm/images.py +65 -0
  927. chrys/service/llm/mock.py +413 -0
  928. chrys/service/llm/observer.py +446 -0
  929. chrys/service/llm/one_shot.py +106 -0
  930. chrys/service/llm/openai_exceptions.py +93 -0
  931. chrys/service/llm/openai_responses/__init__.py +10 -0
  932. chrys/service/llm/openai_responses/client.py +323 -0
  933. chrys/service/llm/openai_responses/decode.py +432 -0
  934. chrys/service/llm/openai_responses/history.py +519 -0
  935. chrys/service/llm/openai_responses/hosted.py +625 -0
  936. chrys/service/llm/openai_responses/replay.py +629 -0
  937. chrys/service/llm/openai_responses/request.py +342 -0
  938. chrys/service/llm/openai_responses/stream.py +847 -0
  939. chrys/service/llm/openai_timestamps.py +57 -0
  940. chrys/service/llm/providers.py +107 -0
  941. chrys/service/llm/proxy_route.py +54 -0
  942. chrys/service/llm/raw_http_log.py +309 -0
  943. chrys/service/llm/route_facts.py +63 -0
  944. chrys/service/llm/route_sessions.py +49 -0
  945. chrys/service/llm/sdk_anthropic.py +18 -0
  946. chrys/service/llm/sdk_headers.py +47 -0
  947. chrys/service/llm/sdk_openai.py +18 -0
  948. chrys/service/llm/sdk_retry.py +59 -0
  949. chrys/service/llm/wire_client.py +246 -0
  950. chrys/service/mcp/__init__.py +3 -0
  951. chrys/service/mcp/_connection.py +235 -0
  952. chrys/service/mcp/_http_transport.py +350 -0
  953. chrys/service/mcp/_progressive.py +459 -0
  954. chrys/service/mcp/_stdio_transport.py +614 -0
  955. chrys/service/mcp/_tool_mixins.py +96 -0
  956. chrys/service/mcp/adapter.py +1031 -0
  957. chrys/service/mcp/cache.py +476 -0
  958. chrys/service/mcp/content.py +32 -0
  959. chrys/service/mcp/errors.py +169 -0
  960. chrys/service/mcp/owned.py +1330 -0
  961. chrys/service/mcp/result_limits.py +168 -0
  962. chrys/service/mcp/thinking_warning.py +95 -0
  963. chrys/service/mcp/validation.py +74 -0
  964. chrys/service/mutations/__init__.py +84 -0
  965. chrys/service/mutations/coordination.py +894 -0
  966. chrys/service/mutations/detector.py +458 -0
  967. chrys/service/mutations/git_calibrator.py +397 -0
  968. chrys/service/mutations/git_state.py +647 -0
  969. chrys/service/mutations/pipeline.py +779 -0
  970. chrys/service/mutations/scanner.py +466 -0
  971. chrys/service/mutations/snapshot_files.py +233 -0
  972. chrys/service/mutations/store.py +625 -0
  973. chrys/service/mutations/tool_names.py +15 -0
  974. chrys/service/mutations/trace.py +246 -0
  975. chrys/service/mutations/tracker.py +1289 -0
  976. chrys/service/mutations/types.py +559 -0
  977. chrys/service/mutations/workspace_changes.py +1583 -0
  978. chrys/service/profiles/__init__.py +2 -0
  979. chrys/service/profiles/agents/__init__.py +3 -0
  980. chrys/service/profiles/agents/builtins/Code.yaml +199 -0
  981. chrys/service/profiles/agents/builtins/Explore.yaml +62 -0
  982. chrys/service/profiles/agents/builtins/General.yaml +114 -0
  983. chrys/service/profiles/agents/builtins/QA.yaml +128 -0
  984. chrys/service/profiles/agents/loader.py +808 -0
  985. chrys/service/profiles/agents/registry.py +727 -0
  986. chrys/service/profiles/agents/schema.py +371 -0
  987. chrys/service/profiles/agents/serializer.py +196 -0
  988. chrys/service/profiles/coercion.py +22 -0
  989. chrys/service/profiles/models/__init__.py +3 -0
  990. chrys/service/profiles/models/env_bridge.py +157 -0
  991. chrys/service/profiles/models/loader.py +282 -0
  992. chrys/service/profiles/models/options.py +438 -0
  993. chrys/service/profiles/models/registry.py +104 -0
  994. chrys/service/profiles/models/resolver.py +286 -0
  995. chrys/service/profiles/models/schema.py +91 -0
  996. chrys/service/profiles/models/serializer.py +76 -0
  997. chrys/service/session/__init__.py +3 -0
  998. chrys/service/session/checkpoint.py +156 -0
  999. chrys/service/session/history.py +1616 -0
  1000. chrys/service/session/message_metadata.py +151 -0
  1001. chrys/service/session/persistence.py +435 -0
  1002. chrys/service/session/runtime_metadata.py +210 -0
  1003. chrys/service/session/sub_agent_logs.py +1069 -0
  1004. chrys/service/session/sub_agent_transcript.py +654 -0
  1005. chrys/service/skills/__init__.py +3 -0
  1006. chrys/service/skills/adapter.py +408 -0
  1007. chrys/service/skills/constants.py +8 -0
  1008. chrys/service/skills/loader.py +575 -0
  1009. chrys/service/skills/model.py +130 -0
  1010. chrys/service/skills/provider.py +841 -0
  1011. chrys/service/skills/runner.py +390 -0
  1012. chrys/service/state/__init__.py +3 -0
  1013. chrys/service/state/_fork.py +710 -0
  1014. chrys/service/state/_session_files.py +175 -0
  1015. chrys/service/state/_session_meta.py +552 -0
  1016. chrys/service/state/locks.py +154 -0
  1017. chrys/service/state/serializers.py +156 -0
  1018. chrys/service/state/session_catalog.py +361 -0
  1019. chrys/service/state/session_listing.py +84 -0
  1020. chrys/service/state/session_migration.py +446 -0
  1021. chrys/service/state/session_mru.py +315 -0
  1022. chrys/service/state/store.py +2132 -0
  1023. chrys/service/state/workflow.py +158 -0
  1024. chrys/service/text_blocks.py +47 -0
  1025. chrys/service/todos/__init__.py +3 -0
  1026. chrys/service/todos/reminder.py +31 -0
  1027. chrys/service/todos/tracker.py +58 -0
  1028. chrys/service/tools/__init__.py +3 -0
  1029. chrys/service/tools/approval_targets.py +92 -0
  1030. chrys/service/tools/builtins/__init__.py +3 -0
  1031. chrys/service/tools/builtins/ask_user.py +326 -0
  1032. chrys/service/tools/builtins/doc_converter/__init__.py +494 -0
  1033. chrys/service/tools/builtins/doc_converter/artifacts.py +392 -0
  1034. chrys/service/tools/builtins/doc_converter/parsers/__init__.py +3 -0
  1035. chrys/service/tools/builtins/doc_converter/parsers/_table.py +34 -0
  1036. chrys/service/tools/builtins/doc_converter/parsers/base.py +101 -0
  1037. chrys/service/tools/builtins/doc_converter/parsers/docx.py +88 -0
  1038. chrys/service/tools/builtins/doc_converter/parsers/pdf.py +144 -0
  1039. chrys/service/tools/builtins/doc_converter/parsers/pptx.py +103 -0
  1040. chrys/service/tools/builtins/doc_converter/parsers/xls.py +32 -0
  1041. chrys/service/tools/builtins/doc_converter/parsers/xlsx.py +51 -0
  1042. chrys/service/tools/builtins/doc_converter/registry.py +55 -0
  1043. chrys/service/tools/builtins/filesystem.py +1071 -0
  1044. chrys/service/tools/builtins/search.py +1265 -0
  1045. chrys/service/tools/builtins/shell.py +967 -0
  1046. chrys/service/tools/builtins/shell_command_filtering/__init__.py +5 -0
  1047. chrys/service/tools/builtins/shell_command_filtering/policy.py +60 -0
  1048. chrys/service/tools/builtins/shell_command_filtering/presets.py +152 -0
  1049. chrys/service/tools/builtins/shell_command_filtering/readonly/__init__.py +358 -0
  1050. chrys/service/tools/builtins/shell_command_filtering/readonly/commands.py +416 -0
  1051. chrys/service/tools/builtins/shell_command_filtering/readonly/git.py +408 -0
  1052. chrys/service/tools/builtins/shell_command_filtering/readonly/syntax.py +87 -0
  1053. chrys/service/tools/builtins/shell_command_filtering/tokenizer.py +214 -0
  1054. chrys/service/tools/builtins/shell_filter.py +24 -0
  1055. chrys/service/tools/builtins/sleep.py +91 -0
  1056. chrys/service/tools/builtins/todo.py +64 -0
  1057. chrys/service/tools/builtins/web/__init__.py +6 -0
  1058. chrys/service/tools/builtins/web/build.py +268 -0
  1059. chrys/service/tools/builtins/web/config.py +338 -0
  1060. chrys/service/tools/builtins/web/credentials.py +37 -0
  1061. chrys/service/tools/builtins/web/fetch/__init__.py +3 -0
  1062. chrys/service/tools/builtins/web/fetch/cache.py +68 -0
  1063. chrys/service/tools/builtins/web/fetch/convert.py +56 -0
  1064. chrys/service/tools/builtins/web/fetch/tool.py +223 -0
  1065. chrys/service/tools/builtins/web/hosted.py +61 -0
  1066. chrys/service/tools/builtins/web/http.py +330 -0
  1067. chrys/service/tools/builtins/web/progress.py +18 -0
  1068. chrys/service/tools/builtins/web/search/__init__.py +3 -0
  1069. chrys/service/tools/builtins/web/search/exa_mcp.py +146 -0
  1070. chrys/service/tools/builtins/web/search/html.py +152 -0
  1071. chrys/service/tools/builtins/web/search/json_api.py +227 -0
  1072. chrys/service/tools/builtins/web/search/providers.py +77 -0
  1073. chrys/service/tools/builtins/web/search/tool.py +218 -0
  1074. chrys/service/tools/builtins/web/search/types.py +28 -0
  1075. chrys/service/tools/builtins/web/tls_identity.py +66 -0
  1076. chrys/service/tools/kinds.py +115 -0
  1077. chrys/service/tools/names.py +47 -0
  1078. chrys/service/tools/registry.py +242 -0
  1079. chrys/service/tools/result_metadata.py +103 -0
  1080. chrys/service/tools/session_artifacts.py +196 -0
  1081. chrys/service/tools/spill.py +218 -0
  1082. chrys/service/tools/workspace_paths.py +81 -0
  1083. chrys/service/trajectory/__init__.py +11 -0
  1084. chrys/service/trajectory/approvals.py +201 -0
  1085. chrys/service/trajectory/compaction.py +420 -0
  1086. chrys/service/trajectory/fork.py +75 -0
  1087. chrys/service/trajectory/hooks.py +217 -0
  1088. chrys/service/trajectory/items.py +41 -0
  1089. chrys/service/trajectory/preparation.py +374 -0
  1090. chrys/service/trajectory/retries.py +95 -0
  1091. chrys/service/trajectory/revisions.py +129 -0
  1092. chrys/service/trajectory/segmented.py +163 -0
  1093. chrys/service/trajectory/session.py +762 -0
  1094. chrys/service/trajectory/state.py +83 -0
  1095. chrys/service/trajectory/sub_agents.py +169 -0
  1096. chrys/service/trajectory/tombstone.py +262 -0
  1097. chrys/service/trajectory/tools.py +323 -0
  1098. chrys/service/trajectory/validation.py +130 -0
  1099. chrys/service/trajectory/waits.py +161 -0
  1100. chrys/service/trajectory/workflow.py +148 -0
  1101. chrys/service/vision.py +128 -0
  1102. chrys/service/workflows/__init__.py +3 -0
  1103. chrys/service/workflows/admission.py +151 -0
  1104. chrys/service/workflows/artifacts.py +109 -0
  1105. chrys/service/workflows/asks.py +130 -0
  1106. chrys/service/workflows/builtins/__init__.py +12 -0
  1107. chrys/service/workflows/builtins/demo-workflow.manifest.json +635 -0
  1108. chrys/service/workflows/builtins/demo-workflow.py +756 -0
  1109. chrys/service/workflows/discovery.py +554 -0
  1110. chrys/service/workflows/environment.py +284 -0
  1111. chrys/service/workflows/graph.py +430 -0
  1112. chrys/service/workflows/history.py +82 -0
  1113. chrys/service/workflows/interpreter.py +107 -0
  1114. chrys/service/workflows/journal.py +334 -0
  1115. chrys/service/workflows/layout.py +56 -0
  1116. chrys/service/workflows/ledger.py +149 -0
  1117. chrys/service/workflows/model_selection.py +18 -0
  1118. chrys/service/workflows/orphans.py +209 -0
  1119. chrys/service/workflows/outcomes.py +46 -0
  1120. chrys/service/workflows/protocol.py +210 -0
  1121. chrys/service/workflows/records.py +142 -0
  1122. chrys/service/workflows/scheduler.py +770 -0
  1123. chrys/service/workflows/sdk/__init__.py +51 -0
  1124. chrys/service/workflows/sdk/_ask.py +167 -0
  1125. chrys/service/workflows/sdk/_builder.py +781 -0
  1126. chrys/service/workflows/sdk/_values.py +92 -0
  1127. chrys/service/workflows/sdk_artifact.py +74 -0
  1128. chrys/service/workflows/store.py +494 -0
  1129. chrys/service/workflows/transcript.py +102 -0
  1130. chrys/service/workflows/values.py +73 -0
  1131. chrys/service/workflows/worker_host.py +1820 -0
  1132. chrys/workflows.py +38 -0
  1133. icode_tui-0.29.1.dist-info/METADATA +104 -0
  1134. icode_tui-0.29.1.dist-info/RECORD +1138 -0
  1135. icode_tui-0.29.1.dist-info/WHEEL +4 -0
  1136. icode_tui-0.29.1.dist-info/entry_points.txt +2 -0
  1137. icode_tui-0.29.1.dist-info/licenses/LICENSE +202 -0
  1138. icode_tui-0.29.1.dist-info/licenses/NOTICE +1955 -0
@@ -0,0 +1,1069 @@
1
+ # Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
2
+
3
+ """Sub-agent audit log storage."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import asyncio
8
+ import hashlib
9
+ import hmac
10
+ import json
11
+ import logging
12
+ import os
13
+ import re
14
+ import secrets
15
+ from dataclasses import dataclass
16
+ from datetime import UTC, datetime
17
+ from pathlib import Path
18
+ from typing import Any
19
+ from urllib.parse import urlsplit
20
+
21
+ from chrys.foundation.platform import get_platform
22
+ from chrys.foundation.platform.files import (
23
+ SecureFileError,
24
+ atomic_write_owner_only_bytes,
25
+ atomic_write_owner_only_text,
26
+ read_owner_verified_bounded,
27
+ secure_open_owner_only,
28
+ secure_open_owner_only_binary,
29
+ secure_unlink_owner_verified,
30
+ surrogate_safe_text,
31
+ )
32
+ from chrys.foundation.trajectory.metadata import read_analytics_item_id
33
+ from chrys.foundation.util.filenames import bounded_safe_stem, safe_fs_name
34
+ from chrys.foundation.util.lock import FileLock
35
+ from chrys.service.session.message_metadata import TOOL_RESULT_METADATA_KEY
36
+ from chrys.service.state.serializers import serialize_state
37
+
38
+ logger = logging.getLogger(__name__)
39
+
40
+ SUB_AGENT_LOG_PREVIEW_MAX_CHARS = 2000
41
+ SUB_AGENT_TRANSCRIPT_OCCURRENCE_IDENTITY = "analytics_item_id_v1"
42
+ SUB_AGENT_LOG_TMP_PREFIX = "."
43
+ SUB_AGENT_LOG_TMP_SUFFIX = ".tmp"
44
+ SUB_AGENT_PENDING_SOURCE_PATH_KEY = "_chrys_source_path"
45
+ SUB_AGENT_RESTORE_CONSUMED_KEY = "_chrys_restore_consumed"
46
+ _INVOCATION_ID_RE = re.compile(r"^[0-9a-f]{12}$")
47
+ _ACP_HMAC_KEY_FILE = "acp-audit-hmac.key"
48
+ _ACP_HMAC_KEY_BYTES = 32
49
+ _ACP_RING_ITEMS = 500
50
+ _ACP_RING_BYTES = 256 * 1024
51
+ _ACP_ATTEMPT_ITEMS = 50
52
+ # Read ceilings for owner-VERIFIED artifacts. Owner verification proves the
53
+ # file is same-uid, but a same-uid ACP child is UNTRUSTED (it may plant a
54
+ # huge/sparse record), so restore/fork reads must be bounded to stop one
55
+ # small-on-disk file from forcing an unbounded allocation. Pending records
56
+ # are tiny metadata; audit envelopes may embed a kernel sub-agent's full
57
+ # serialized history (including images), so they get far more headroom.
58
+ _MAX_PENDING_RECORD_BYTES = 4 * 1024 * 1024
59
+ MAX_SUB_AGENT_AUDIT_BYTES = 128 * 1024 * 1024
60
+ # A same-uid ACP child is untrusted: individual pending records are bounded to
61
+ # _MAX_PENDING_RECORD_BYTES, but the child can plant hundreds of them, so drain
62
+ # must also cap the AGGREGATE it materializes in memory per restore. Past the
63
+ # cap the remainder is deferred, not dropped — finalize deletes the records
64
+ # consumed this restore, so the leftovers drain over subsequent restores.
65
+ MAX_PENDING_RECORDS = 256
66
+ MAX_PENDING_RECORDS_TOTAL_BYTES = 32 * 1024 * 1024
67
+ # Even before the per-record caps apply, the mere ENUMERATION of a session
68
+ # artifact directory is attacker-influenced: a same-uid child can plant millions
69
+ # of tiny files. `Path.glob`/`Path.iterdir` are EAGER on 3.14 (they list scandir
70
+ # before yielding), so `scan_bounded_json_files` drives `os.scandir` directly and
71
+ # breaks after MAX_SCANNED_* examined entries. A truncated scan defers the
72
+ # remainder; corrupt/consumed files leave the scan over restores so it converges.
73
+ MAX_SCANNED_PENDING_FILES = 4096
74
+ MAX_SCANNED_AUDIT_FILES = 4096
75
+ # Enumeration count is not enough: a bounded set of files can still be huge/
76
+ # sparse, so reading them all (`read_owner_verified_bounded`, up to the per-file
77
+ # ceilings) would itself be an unbounded I/O DoS (e.g. 4096 x 128 MiB audit
78
+ # reads). Bound the CUMULATIVE on-disk size a single scan will accept for
79
+ # reading, using scandir's free st_size to refuse a bomb without reading it.
80
+ MAX_PENDING_SCAN_TOTAL_BYTES = 64 * 1024 * 1024
81
+ MAX_AUDIT_SCAN_TOTAL_BYTES = 512 * 1024 * 1024
82
+ # Marks a paused record whose still-"running" audit could NOT be re-flipped to
83
+ # "orphaned" this restore (transient audit-write failure). finalize keeps such
84
+ # records so the next restore retries the repair; deleting them would strand
85
+ # the audit "running" and backlinked, hence permanently exempt from repair.
86
+ SUB_AGENT_REPAIR_FAILED_KEY = "_chrys_repair_failed"
87
+
88
+
89
+ @dataclass(slots=True)
90
+ class SubAgentLogStats:
91
+ """In-memory counters snapshotted into sub-agent audit logs."""
92
+
93
+ tool_call_count: int = 0
94
+ total_tokens: int = 0
95
+ total_usage_tokens: int = 0
96
+ usage_unreported_attempts: int = 0
97
+
98
+ def record_tool_call(self) -> None:
99
+ self.tool_call_count += 1
100
+
101
+ def record_usage(self, total_tokens: int, total_usage_tokens: int) -> None:
102
+ self.total_tokens = total_tokens
103
+ self.total_usage_tokens = total_usage_tokens
104
+
105
+
106
+ @dataclass(frozen=True, slots=True)
107
+ class SubAgentModelProvenance:
108
+ """Non-sensitive model provenance stored in sub-agent audit metadata."""
109
+
110
+ provider: str = ""
111
+ api_style: str = ""
112
+ model_id: str = ""
113
+ base_url_origin: str = ""
114
+
115
+
116
+ class SubAgentSessionLogWriter:
117
+ """Cancellation-safe per-invocation writer for one sub-agent log file."""
118
+
119
+ def __init__(
120
+ self,
121
+ *,
122
+ parent_session_dir: Path | None,
123
+ parent_session_id: str | None,
124
+ invocation_id: str,
125
+ tool_name: str,
126
+ agent_profile: str,
127
+ agent_display_name: str,
128
+ prompt: str,
129
+ parent_provider_call_id: str,
130
+ parent_event_call_id: str,
131
+ model: SubAgentModelProvenance | None = None,
132
+ primary_cwd: str = "",
133
+ working_dirs: list[str] | None = None,
134
+ stats: SubAgentLogStats | None = None,
135
+ runner: str = "kernel",
136
+ acp_command: str = "",
137
+ acp_args: list[str] | None = None,
138
+ ) -> None:
139
+ self._parent_session_dir = parent_session_dir
140
+ self._parent_session_id = parent_session_id or ""
141
+ self._invocation_id = invocation_id
142
+ self._tool_name = tool_name
143
+ self._agent_profile = agent_profile
144
+ self._agent_display_name = agent_display_name
145
+ self._prompt_preview = preview_text(prompt)
146
+ self._parent_provider_call_id = parent_provider_call_id
147
+ self._parent_event_call_id = parent_event_call_id
148
+ self._model = model or SubAgentModelProvenance()
149
+ self._primary_cwd = primary_cwd
150
+ self._working_dirs = list(working_dirs or ([primary_cwd] if primary_cwd else []))
151
+ self._stats = stats or SubAgentLogStats()
152
+ self._runner = runner
153
+ self._acp_command = acp_command
154
+ self._acp_args = list(acp_args or [])
155
+ self._created_at = _utc_now()
156
+ self._lock = asyncio.Lock()
157
+ self._active = False
158
+ self._log_file_name = make_sub_agent_log_file_name(tool_name, invocation_id)
159
+
160
+ @property
161
+ def log_file_name(self) -> str:
162
+ return self._log_file_name
163
+
164
+ @property
165
+ def path(self) -> Path | None:
166
+ if self._parent_session_dir is None:
167
+ return None
168
+ return sessions_dir(self._parent_session_dir) / self._log_file_name
169
+
170
+ @property
171
+ def active(self) -> bool:
172
+ return self._active
173
+
174
+ def set_acp_launch(self, command: str, args: list[str]) -> None:
175
+ """Update the resolved launch argv used by the next ACP envelope write."""
176
+ if self._runner != "acp":
177
+ raise RuntimeError("ACP launch provenance is available only for ACP audit writers")
178
+ self._acp_command = command
179
+ self._acp_args = list(args)
180
+
181
+ async def write(
182
+ self,
183
+ *,
184
+ status: str,
185
+ state: dict[str, Any] | None = None,
186
+ result: str = "",
187
+ failure_reason: str = "",
188
+ last_error: str = "",
189
+ ended: bool = False,
190
+ acp_state: dict[str, Any] | None = None,
191
+ ) -> bool:
192
+ """Best-effort write of the current invocation envelope."""
193
+ path = self.path
194
+ if path is None:
195
+ return False
196
+ async with self._lock:
197
+ write_task: asyncio.Task[None] | None = None
198
+ cancelled = False
199
+ try:
200
+ envelope = self._envelope(
201
+ status=status,
202
+ state=state,
203
+ result=result,
204
+ failure_reason=failure_reason,
205
+ last_error=last_error,
206
+ ended=ended,
207
+ acp_state=acp_state,
208
+ )
209
+ write_task = asyncio.create_task(asyncio.to_thread(_atomic_write_json, path, envelope))
210
+ try:
211
+ await asyncio.shield(write_task)
212
+ except asyncio.CancelledError:
213
+ cancelled = True
214
+ while not write_task.done():
215
+ try:
216
+ await asyncio.shield(write_task)
217
+ except asyncio.CancelledError:
218
+ cancelled = True
219
+ write_task.result()
220
+ except Exception as exc:
221
+ logger.warning("Failed to write sub-agent audit log %s: %s", path.name, exc)
222
+ if cancelled:
223
+ raise asyncio.CancelledError from exc
224
+ if self._runner == "acp":
225
+ raise
226
+ return False
227
+ self._active = True
228
+ if cancelled:
229
+ raise asyncio.CancelledError
230
+ return True
231
+
232
+ def _envelope(
233
+ self,
234
+ *,
235
+ status: str,
236
+ state: dict[str, Any] | None,
237
+ result: str,
238
+ failure_reason: str,
239
+ last_error: str,
240
+ ended: bool,
241
+ acp_state: dict[str, Any] | None,
242
+ ) -> dict[str, Any]:
243
+ serialized_state = serialize_state(state or {}) if self._runner == "kernel" else {}
244
+ now = _utc_now()
245
+ meta = {
246
+ "schema_version": 1,
247
+ "runner": self._runner,
248
+ "record_type": "sub_agent_session",
249
+ "parent_session_id": self._parent_session_id,
250
+ "parent_event_call_id": self._parent_event_call_id,
251
+ "parent_provider_call_id": self._parent_provider_call_id,
252
+ "invocation_id": self._invocation_id,
253
+ "tool_name": self._tool_name,
254
+ "agent_profile": self._agent_profile,
255
+ "agent_display_name": self._agent_display_name,
256
+ "status": status,
257
+ "failure_reason": failure_reason,
258
+ "last_error": last_error,
259
+ "created_at": self._created_at,
260
+ "updated_at": now,
261
+ "ended_at": now if ended else "",
262
+ "prompt_preview": self._prompt_preview,
263
+ "result_preview": preview_text(result),
264
+ "message_count": len(serialized_state.get("messages", [])),
265
+ "tool_call_count": self._stats.tool_call_count,
266
+ "total_tokens": self._stats.total_tokens,
267
+ "total_usage_tokens": self._stats.total_usage_tokens,
268
+ "usage_unreported_attempts": self._stats.usage_unreported_attempts,
269
+ "primary_cwd": self._primary_cwd,
270
+ "working_dirs": list(self._working_dirs),
271
+ "model_provider": self._model.provider,
272
+ "model_api_style": self._model.api_style,
273
+ "model_id": self._model.model_id,
274
+ "model_base_url_origin": self._model.base_url_origin,
275
+ }
276
+ if self._runner == "kernel" and serialized_history_has_occurrence_ids(serialized_state):
277
+ meta["transcript_occurrence_identity"] = SUB_AGENT_TRANSCRIPT_OCCURRENCE_IDENTITY
278
+ if self._runner == "acp":
279
+ normalized = normalize_acp_state(acp_state or {}, parent_session_dir=self._parent_session_dir)
280
+ normalized["usage_unreported_attempts"] = self._stats.usage_unreported_attempts
281
+ normalized["launch"] = acp_launch_provenance(self._acp_command, self._acp_args)
282
+ return {"meta": meta, "acp_state": normalized}
283
+ return {"meta": meta, "state": serialized_state}
284
+
285
+
286
+ def serialized_history_has_occurrence_ids(state: dict[str, Any]) -> bool:
287
+ """Return whether every persisted kernel message has stable occurrence identity."""
288
+ live_messages = state.get("messages")
289
+ compressed = state.get("compressed_msgs")
290
+ if not isinstance(live_messages, list) or not isinstance(compressed, list):
291
+ return False
292
+ messages: list[Any] = list(live_messages)
293
+ for block in compressed:
294
+ if not isinstance(block, dict):
295
+ return False
296
+ block_messages = block.get("messages")
297
+ if not isinstance(block_messages, list):
298
+ return False
299
+ messages.extend(block_messages)
300
+ return all(
301
+ isinstance(message, dict) and read_analytics_item_id(message.get("additional_properties")) is not None
302
+ for message in messages
303
+ )
304
+
305
+
306
+ def pending_dir(parent_session_dir: Path) -> Path:
307
+ return parent_session_dir / "sub_agents" / "pending"
308
+
309
+
310
+ def sessions_dir(parent_session_dir: Path) -> Path:
311
+ return parent_session_dir / "sub_agents" / "sessions"
312
+
313
+
314
+ def make_sub_agent_log_file_name(tool_name: str, invocation_id: str) -> str:
315
+ # The invocation id is appended as the filename suffix. Sanitize it as well
316
+ # as the tool-name stem: a chrys-generated 12-hex id passes through
317
+ # unchanged (``safe_fs_name`` is a length-preserving 1:1 substitution), but
318
+ # an attacker-plantable pause record could carry a traversal payload
319
+ # (``/../../victim``) that would otherwise escape the sessions directory
320
+ # when this name is path-joined. Stripping the separators keeps the result
321
+ # a single in-directory basename regardless of caller trust.
322
+ suffix = f"_{safe_fs_name(invocation_id)}.json"
323
+ stem = bounded_safe_stem(
324
+ tool_name,
325
+ suffix=suffix,
326
+ temp_prefix=SUB_AGENT_LOG_TMP_PREFIX,
327
+ temp_suffix=SUB_AGENT_LOG_TMP_SUFFIX,
328
+ )
329
+ return f"{stem}{suffix}"
330
+
331
+
332
+ def validate_invocation_id(invocation_id: str) -> bool:
333
+ return bool(_INVOCATION_ID_RE.fullmatch(invocation_id))
334
+
335
+
336
+ def preview_text(text: str, *, max_chars: int = SUB_AGENT_LOG_PREVIEW_MAX_CHARS) -> str:
337
+ """Return a bounded preview, appending ``...`` only within the cap."""
338
+ if len(text) <= max_chars:
339
+ return text
340
+ if max_chars <= 3:
341
+ return "." * max(0, max_chars)
342
+ return f"{text[: max_chars - 3]}..."
343
+
344
+
345
+ def sanitize_base_url_origin(raw_url: str) -> str:
346
+ """Return ``scheme://host[:port]`` for normal absolute URLs, else ``""``."""
347
+ if not raw_url:
348
+ return ""
349
+ try:
350
+ parsed = urlsplit(raw_url)
351
+ except ValueError:
352
+ return ""
353
+ if parsed.scheme not in {"http", "https"} or not parsed.hostname:
354
+ return ""
355
+ try:
356
+ port = parsed.port
357
+ except ValueError:
358
+ return ""
359
+ host = parsed.hostname
360
+ if port is None:
361
+ return f"{parsed.scheme}://{host}"
362
+ return f"{parsed.scheme}://{host}:{port}"
363
+
364
+
365
+ def redact_acp_argv(args: list[str]) -> list[str]:
366
+ """Conservatively retain option prefixes while redacting every value."""
367
+ redacted: list[str] = []
368
+ positional_only = False
369
+ for arg in args:
370
+ if positional_only:
371
+ # Everything after ``--`` is positional (doc §10: every attached
372
+ # AND positional value is replaced), even if it starts with ``-``.
373
+ redacted.append("<redacted>")
374
+ continue
375
+ if arg == "--":
376
+ # The end-of-options marker is a fixed token, not a secret; keep
377
+ # it and redact everything that follows.
378
+ positional_only = True
379
+ redacted.append(arg)
380
+ elif arg.startswith("--") and "=" in arg:
381
+ option, _value = arg.split("=", 1)
382
+ redacted.append(f"{option}=<redacted>")
383
+ elif arg.startswith("--") and len(arg) > 2:
384
+ redacted.append(arg)
385
+ elif arg.startswith("-") and len(arg) > 2:
386
+ redacted.append(f"{arg[:2]}<redacted>")
387
+ elif arg.startswith("-") and len(arg) == 2:
388
+ redacted.append(arg)
389
+ else:
390
+ redacted.append("<redacted>")
391
+ return redacted
392
+
393
+
394
+ def _audit_safe_text(value: str) -> str:
395
+ """Escape any unpaired surrogate so this string is safe to audit.
396
+
397
+ POSIX filesystem paths (and env-expanded launch argv derived from them) can
398
+ carry surrogateescaped bytes — legal str values that are NOT strict-UTF-8
399
+ encodable. Left raw they crash the audit twice: the provenance digest below
400
+ and the ``ensure_ascii=False`` file write, and even a surrogatepass-written
401
+ file would fail the audit's strict-UTF-8 JSON read-back in reconcile/fork.
402
+ ``backslashreplace`` turns a lone surrogate into its ``\\udcXX`` escape (a
403
+ no-op on valid text), keeping the stored value total and round-trippable
404
+ while the digest below still binds the exact original bytes.
405
+
406
+ This is per-field defense-in-depth for the provenance digest inputs; the
407
+ whole-envelope boundary sanitize in ``_atomic_write_json`` (and the sibling
408
+ ACP writers) is what guarantees EVERY field — meta paths included — is
409
+ surrogate-free before the strict file write.
410
+ """
411
+ return surrogate_safe_text(value)
412
+
413
+
414
+ def acp_launch_provenance(command: str, args: list[str]) -> dict[str, Any]:
415
+ """Return non-secret launch metadata plus a keyed full-argv digest."""
416
+ key = load_or_create_acp_audit_key()
417
+ # surrogatepass keeps the digest total over the EXACT argv bytes even when a
418
+ # value carries a surrogateescaped filesystem byte; the stored command/args
419
+ # are surrogate-escaped so the audit envelope stays strict-UTF-8 round-trippable.
420
+ payload = json.dumps(
421
+ [command, *args],
422
+ ensure_ascii=False,
423
+ separators=(",", ":"),
424
+ allow_nan=False,
425
+ ).encode("utf-8", "surrogatepass")
426
+ return {
427
+ "command": _audit_safe_text(command),
428
+ "args": [_audit_safe_text(arg) for arg in redact_acp_argv(args)],
429
+ "arg_count": len(args),
430
+ "argv_hmac_sha256": hmac.new(key, payload, hashlib.sha256).hexdigest(),
431
+ }
432
+
433
+
434
+ def load_or_create_acp_audit_key(*, config_dir: Path | None = None, timeout: float = 5.0) -> bytes:
435
+ """Securely validate or no-replace-create the installation ACP HMAC key."""
436
+ root = config_dir or get_platform().config_dir
437
+ root.mkdir(parents=True, exist_ok=True)
438
+ key_path = root / _ACP_HMAC_KEY_FILE
439
+ lock_path = root / f"{_ACP_HMAC_KEY_FILE}.lock"
440
+ with FileLock(lock_path, timeout=timeout):
441
+ if key_path.exists() or key_path.is_symlink():
442
+ return _read_acp_audit_key(key_path)
443
+
444
+ temp_path = root / f".{_ACP_HMAC_KEY_FILE}.{secrets.token_hex(8)}.tmp"
445
+ fd = secure_open_owner_only(temp_path, write=True, create=True)
446
+ try:
447
+ key = secrets.token_bytes(_ACP_HMAC_KEY_BYTES)
448
+ with os.fdopen(fd, "wb") as file:
449
+ fd = -1
450
+ file.write(key)
451
+ file.flush()
452
+ os.fsync(file.fileno())
453
+ try:
454
+ os.link(temp_path, key_path, follow_symlinks=False)
455
+ except FileExistsError:
456
+ # A creator not using this lock is still never allowed to be
457
+ # overwritten; validate what won the no-replace publish race.
458
+ return _read_acp_audit_key(key_path)
459
+ _fsync_parent(root)
460
+ return _read_acp_audit_key(key_path)
461
+ finally:
462
+ if fd >= 0:
463
+ os.close(fd)
464
+ secure_unlink_owner_verified(temp_path)
465
+
466
+
467
+ def _read_acp_audit_key(path: Path) -> bytes:
468
+ try:
469
+ with secure_open_owner_only_binary(path) as file:
470
+ key = file.read(_ACP_HMAC_KEY_BYTES + 1)
471
+ except OSError as exc:
472
+ raise SecureFileError("The ACP audit HMAC key failed secure verification.") from exc
473
+ if len(key) != _ACP_HMAC_KEY_BYTES:
474
+ raise SecureFileError("The ACP audit HMAC key has an invalid length.")
475
+ return key
476
+
477
+
478
+ def _fsync_parent(path: Path) -> None:
479
+ if get_platform().is_windows:
480
+ return
481
+ fd = os.open(path, os.O_RDONLY)
482
+ try:
483
+ os.fsync(fd)
484
+ finally:
485
+ os.close(fd)
486
+
487
+
488
+ def normalize_acp_state(state: dict[str, Any], *, parent_session_dir: Path | None) -> dict[str, Any]:
489
+ """Bound the ACP codec and normalize referenced files session-relatively."""
490
+ normalized = json.loads(json.dumps(state, ensure_ascii=False, default=str, allow_nan=False))
491
+ updates = normalized.get("translated_updates")
492
+ if isinstance(updates, list):
493
+ ring: list[Any] = []
494
+ # Count list delimiters and commas as well as entry payloads.
495
+ size = 2
496
+ for update in reversed(updates[-_ACP_RING_ITEMS:]):
497
+ item_size = len(
498
+ json.dumps(
499
+ update,
500
+ ensure_ascii=False,
501
+ separators=(",", ":"),
502
+ allow_nan=False,
503
+ ).encode()
504
+ )
505
+ added = item_size + (1 if ring else 0)
506
+ if size + added > _ACP_RING_BYTES:
507
+ break
508
+ ring.append(update)
509
+ size += added
510
+ normalized["translated_updates"] = list(reversed(ring))
511
+ attempts = normalized.get("attempts")
512
+ if isinstance(attempts, list) and len(attempts) > _ACP_ATTEMPT_ITEMS:
513
+ # Manual retries are unlimited, so the attempt history needs a ring
514
+ # like translated_updates. Newest-wins; per-entry fields are already
515
+ # client-capped (ACP session ids are validated at 4096 chars).
516
+ normalized["attempts"] = attempts[-_ACP_ATTEMPT_ITEMS:]
517
+ for key in ("diagnostic_path", "stderr_log_path"):
518
+ raw = normalized.get(key)
519
+ if not raw:
520
+ continue
521
+ if parent_session_dir is None:
522
+ normalized.pop(key, None)
523
+ continue
524
+ root = parent_session_dir.resolve()
525
+ path = Path(str(raw))
526
+ if not path.is_absolute():
527
+ path = root / path
528
+ try:
529
+ resolved = path.resolve()
530
+ normalized[key] = str(resolved.relative_to(root))
531
+ except (OSError, RuntimeError, ValueError) as exc:
532
+ raise ValueError(f"ACP state file reference escapes the session directory: {key}") from exc
533
+ return normalized
534
+
535
+
536
+ def validate_acp_state_file_references(state: dict[str, Any], *, parent_session_dir: Path) -> None:
537
+ """Reject absolute, parent-traversing, and symlink-escaping ACP references."""
538
+ root = parent_session_dir.resolve()
539
+ for key in ("diagnostic_path", "stderr_log_path"):
540
+ raw = state.get(key)
541
+ if not raw:
542
+ continue
543
+ path = Path(str(raw))
544
+ if path.is_absolute() or ".." in path.parts:
545
+ raise ValueError(f"Invalid ACP state file reference: {key}")
546
+ try:
547
+ (root / path).resolve().relative_to(root)
548
+ except (OSError, RuntimeError, ValueError) as exc:
549
+ raise ValueError(f"ACP state file reference escapes the session directory: {key}") from exc
550
+
551
+
552
+ _PAUSED_RECORD_REQUIRED_STR_FIELDS = ("invocation_id", "tool_name")
553
+ _PAUSED_RECORD_OPTIONAL_STR_FIELDS = (
554
+ "parent_call_id",
555
+ "parent_provider_call_id",
556
+ "sub_agent_log_file",
557
+ "created_at",
558
+ "paused_at",
559
+ "status",
560
+ "failure_reason",
561
+ "last_error",
562
+ )
563
+
564
+
565
+ def _paused_record_is_well_formed(data: dict[str, Any]) -> bool:
566
+ """Reject records whose field types would crash restore consumers.
567
+
568
+ Required identity fields must be non-empty strings; optional fields are
569
+ string-or-absent (``inject_error_results_for_sub_agents`` uses parent
570
+ call ids as dict keys, and the restore Warning joins tool names).
571
+ """
572
+ for field in _PAUSED_RECORD_REQUIRED_STR_FIELDS:
573
+ value = data.get(field)
574
+ if not isinstance(value, str) or not value:
575
+ return False
576
+ for field in _PAUSED_RECORD_OPTIONAL_STR_FIELDS:
577
+ value = data.get(field)
578
+ if value is not None and not isinstance(value, str):
579
+ return False
580
+ return True
581
+
582
+
583
+ def scan_bounded_json_files(
584
+ directory: Path,
585
+ *,
586
+ max_files: int,
587
+ max_total_bytes: int | None = None,
588
+ suffixes: tuple[str, ...] = (".json",),
589
+ ) -> tuple[list[Path], bool]:
590
+ """Lazily enumerate at most *max_files* matching entries via ``os.scandir``.
591
+
592
+ ``Path.glob``/``Path.iterdir`` are EAGER on CPython 3.14 — they run
593
+ ``list(scandir)`` internally before yielding the first result, so wrapping
594
+ them in ``islice`` does NOT bound enumeration and a same-uid flood of
595
+ planted files still OOMs restore/fork during listing. ``os.scandir`` yields
596
+ lazily; we break after *max_files* EXAMINED entries (not just matched ones,
597
+ so a flood of non-matching names cannot spin the loop unbounded) and, when
598
+ *max_total_bytes* is given, once the cumulative on-disk size of accepted
599
+ files would exceed it — using the free ``st_size`` from ``scandir`` so a
600
+ sparse/huge file is refused WITHOUT being read (bounding the bytes a later
601
+ ``read_owner_verified_bounded`` will materialize). Returns the bounded
602
+ subset sorted by path plus a truncated flag; the remainder (the
603
+ filesystem-order tail beyond the caps) is deferred to a later pass.
604
+ """
605
+ paths: list[Path] = []
606
+ truncated = False
607
+ total = 0
608
+ try:
609
+ with os.scandir(directory) as iterator:
610
+ examined = 0
611
+ for entry in iterator:
612
+ examined += 1
613
+ if examined > max_files or (max_total_bytes is not None and total >= max_total_bytes):
614
+ truncated = True
615
+ break
616
+ if not entry.name.endswith(suffixes):
617
+ continue
618
+ if max_total_bytes is not None:
619
+ try:
620
+ total += min(entry.stat(follow_symlinks=False).st_size, MAX_SUB_AGENT_AUDIT_BYTES + 1)
621
+ except OSError:
622
+ continue
623
+ paths.append(Path(entry.path))
624
+ except FileNotFoundError, NotADirectoryError:
625
+ return [], False
626
+ except OSError:
627
+ logger.warning("Failed to scan sub-agent directory %s", directory.name, exc_info=True)
628
+ return [], False
629
+ return sorted(paths), truncated
630
+
631
+
632
+ class SubAgentSessionArtifactService:
633
+ """Profile-independent restore and orphan repair for one session directory."""
634
+
635
+ def __init__(self, session_dir: Path) -> None:
636
+ self._session_dir = session_dir
637
+
638
+ def drain_paused_records(self) -> list[dict[str, Any]]:
639
+ logs = self._log_files_by_invocation()
640
+ records: list[dict[str, Any]] = []
641
+ candidates: list[Path] = []
642
+ for directory in (self._session_dir / "sub_agents", pending_dir(self._session_dir)):
643
+ # Bound enumeration AND cumulative read BEFORE the per-record caps: a
644
+ # same-uid flood of tiny (even corrupt) files must not materialize an
645
+ # unbounded Path list, and a pile of huge/sparse records must not
646
+ # force an unbounded read. Corrupt/consumed files leave the scan over
647
+ # restores.
648
+ paths, truncated = scan_bounded_json_files(
649
+ directory,
650
+ max_files=MAX_SCANNED_PENDING_FILES,
651
+ max_total_bytes=MAX_PENDING_SCAN_TOTAL_BYTES,
652
+ )
653
+ if truncated:
654
+ logger.warning(
655
+ "Paused sub-agent scan of %s truncated; a later restore drains the rest",
656
+ directory.name,
657
+ )
658
+ candidates.extend(paths)
659
+ total_bytes = 0
660
+ for index, path in enumerate(candidates):
661
+ # A same-uid ACP child can plant hundreds of individually-valid
662
+ # (<4 MiB) records; without an aggregate ceiling this loop would
663
+ # hold them all in memory at once and OOM the restore. Stop past the
664
+ # cap and DEFER the rest — candidate sizes are unknown until read, so
665
+ # breaking is safer than scanning on. finalize removes the records
666
+ # consumed this restore, letting the remainder drain later.
667
+ if len(records) >= MAX_PENDING_RECORDS or total_bytes >= MAX_PENDING_RECORDS_TOTAL_BYTES:
668
+ logger.warning(
669
+ "Deferring %d paused sub-agent record(s) past the per-restore cap (%d drained, %d bytes)",
670
+ len(candidates) - index,
671
+ len(records),
672
+ total_bytes,
673
+ )
674
+ break
675
+ try:
676
+ raw = _secure_read_bytes(path)
677
+ except OSError, ValueError:
678
+ # ValueError == over the per-record read ceiling (an over-cap
679
+ # planted record); OSError == I/O failure. _archive drops the
680
+ # over-cap file via its own ceiling and quarantines the rest.
681
+ logger.warning("Failed to read paused sub-agent %s", path.name, exc_info=True)
682
+ self._archive(path, "corrupt")
683
+ continue
684
+ try:
685
+ data = json.loads(raw.decode("utf-8"))
686
+ except UnicodeError, json.JSONDecodeError, ValueError, RecursionError:
687
+ # RecursionError (not a ValueError) covers a deeply nested but
688
+ # byte-bounded record whose json.loads blows the stack; quarantine
689
+ # it like any other unreadable record rather than aborting restore.
690
+ logger.warning("Failed to load paused sub-agent %s", path.name, exc_info=True)
691
+ self._archive(path, "corrupt")
692
+ continue
693
+ if not isinstance(data, dict) or not _paused_record_is_well_formed(data):
694
+ logger.warning("Quarantining malformed paused sub-agent record %s", path.name)
695
+ self._archive(path, "corrupt")
696
+ continue
697
+ total_bytes += len(raw)
698
+ data[SUB_AGENT_PENDING_SOURCE_PATH_KEY] = str(path)
699
+ invocation_id = data.get("invocation_id")
700
+ if not data.get("sub_agent_log_file") and isinstance(invocation_id, str) and invocation_id in logs:
701
+ data["sub_agent_log_file"] = logs[invocation_id]
702
+ records.append(data)
703
+ records.sort(key=lambda record: str(record.get("created_at") or record.get("paused_at") or ""))
704
+ return records
705
+
706
+ def reconcile_orphaned_running_logs(
707
+ self,
708
+ parent_messages: list[Any],
709
+ paused_records: list[dict[str, Any]],
710
+ ) -> int:
711
+ pending_by_id: dict[str, dict[str, Any]] = {}
712
+ for record in paused_records:
713
+ value = record.get("invocation_id")
714
+ if isinstance(value, str) and value:
715
+ pending_by_id.setdefault(value, record)
716
+ pending_ids = set(pending_by_id)
717
+ backlink_ids, backlink_files = _sub_agent_backlinks(parent_messages)
718
+ repaired = 0
719
+ directory = sessions_dir(self._session_dir)
720
+ seen: set[Path] = set()
721
+ # Reconcile each pending record's OWN audit by its deterministic path
722
+ # FIRST, independent of the bounded scan below. The critical case — a
723
+ # pending record beside a still-"running" audit (the process died in the
724
+ # window between writing the pause record and stamping the audit
725
+ # "paused") — MUST be repaired THIS restore: injection then deletes the
726
+ # record and backlinks the log, after which the backlink-exemption would
727
+ # shield the stale "running" status from every future repair. A flood
728
+ # (or a legitimately huge session) can push that audit past the scan
729
+ # cap, so scanning alone is not enough — target it directly.
730
+ for invocation_id, record in pending_by_id.items():
731
+ tool_name = record.get("tool_name")
732
+ if not isinstance(tool_name, str) or not tool_name:
733
+ continue
734
+ # Only a chrys-generated 12-hex id can name a real audit; a pending
735
+ # record is same-uid attacker-plantable, so reject anything else
736
+ # before it reaches the path join below (defense in depth with the
737
+ # sanitization inside make_sub_agent_log_file_name).
738
+ if not validate_invocation_id(invocation_id):
739
+ continue
740
+ target = directory / make_sub_agent_log_file_name(tool_name, invocation_id)
741
+ if target in seen:
742
+ continue
743
+ seen.add(target)
744
+ if self._reconcile_running_audit(
745
+ target,
746
+ pending_by_id=pending_by_id,
747
+ pending_ids=pending_ids,
748
+ backlink_ids=backlink_ids,
749
+ backlink_files=backlink_files,
750
+ ):
751
+ repaired += 1
752
+ # Bound the audit scan by BOTH count and cumulative on-disk size: a flood
753
+ # of planted audit files (each up to the 128 MiB ceiling) would otherwise
754
+ # force reading+parsing every one before any repair. This best-effort
755
+ # pass catches genuinely-orphaned running audits (no pending record).
756
+ paths, truncated = scan_bounded_json_files(
757
+ directory,
758
+ max_files=MAX_SCANNED_AUDIT_FILES,
759
+ max_total_bytes=MAX_AUDIT_SCAN_TOTAL_BYTES,
760
+ )
761
+ if truncated:
762
+ logger.warning("Sub-agent audit reconcile scan truncated; a later restore repairs the rest")
763
+ for path in paths:
764
+ if path in seen:
765
+ continue
766
+ seen.add(path)
767
+ if self._reconcile_running_audit(
768
+ path,
769
+ pending_by_id=pending_by_id,
770
+ pending_ids=pending_ids,
771
+ backlink_ids=backlink_ids,
772
+ backlink_files=backlink_files,
773
+ ):
774
+ repaired += 1
775
+ return repaired
776
+
777
+ def _reconcile_running_audit(
778
+ self,
779
+ path: Path,
780
+ *,
781
+ pending_by_id: dict[str, dict[str, Any]],
782
+ pending_ids: set[str],
783
+ backlink_ids: set[str],
784
+ backlink_files: set[str],
785
+ ) -> bool:
786
+ """Flip one still-"running" audit to "orphaned"; return True if repaired.
787
+
788
+ Shared by the targeted (per pending record) and bounded-scan passes of
789
+ reconcile_orphaned_running_logs.
790
+ """
791
+ envelope = self._read_audit_envelope_or_drop(path)
792
+ if envelope is None:
793
+ return False
794
+ try:
795
+ meta = envelope.get("meta")
796
+ if isinstance(meta, dict) and meta.get("runner") == "acp":
797
+ acp_state = envelope.get("acp_state")
798
+ if not isinstance(acp_state, dict):
799
+ raise ValueError("ACP sub-agent log is missing ACP state")
800
+ validate_acp_state_file_references(
801
+ acp_state,
802
+ parent_session_dir=self._session_dir,
803
+ )
804
+ if not isinstance(meta, dict) or meta.get("status") != "running":
805
+ return False
806
+ invocation_id = meta.get("invocation_id")
807
+ if not isinstance(invocation_id, str):
808
+ return False
809
+ # A pending record beside a still-"running" audit means the
810
+ # process died between the pause-record and audit-"paused"
811
+ # writes — repair it NOW: the record is discarded during this
812
+ # restore and its injected error result backlinks the log, so
813
+ # both skips below would otherwise shield the stale status
814
+ # forever. Backlinked audits without a pending record keep
815
+ # their accounted-for exemption.
816
+ if invocation_id not in pending_ids and (invocation_id in backlink_ids or path.name in backlink_files):
817
+ return False
818
+ now = _utc_now()
819
+ meta.update(
820
+ {
821
+ "status": "orphaned",
822
+ "failure_reason": "process_terminated",
823
+ "last_error": "process terminated before sub-agent wrote a terminal audit status",
824
+ "updated_at": now,
825
+ "ended_at": now,
826
+ }
827
+ )
828
+ serialized = json.dumps(envelope, indent=2, ensure_ascii=False, allow_nan=False)
829
+ try:
830
+ atomic_write_owner_only_text(path, serialized)
831
+ except OSError, ValueError:
832
+ # The audit could not be flipped to "orphaned" this restore
833
+ # (transient write failure — NOT an encode failure, which
834
+ # stays with the outer handler). Flag its pending record so
835
+ # finalize keeps it: deleting it would leave the still-
836
+ # "running" audit backlinked and thus permanently exempt
837
+ # (the skip above) from repair on every future restore.
838
+ record = pending_by_id.get(invocation_id)
839
+ if record is not None:
840
+ record[SUB_AGENT_REPAIR_FAILED_KEY] = True
841
+ logger.warning(
842
+ "Failed to write reconciled sub-agent audit log %s; keeping its pending record for retry",
843
+ path.name,
844
+ exc_info=True,
845
+ )
846
+ return False
847
+ return True
848
+ except OSError, ValueError:
849
+ logger.warning("Failed to reconcile sub-agent audit log %s", path.name, exc_info=True)
850
+ return False
851
+
852
+ def finalize_restored_paused_records(self, records: list[dict[str, Any]]) -> None:
853
+ for record in records:
854
+ if record.get(SUB_AGENT_REPAIR_FAILED_KEY):
855
+ # reconcile could not repair this record's still-"running"
856
+ # audit; keep the pending record in place so the next restore
857
+ # retries (it stays in pending_ids, bypassing the backlink
858
+ # exemption). Deleting it now would strand the audit forever.
859
+ continue
860
+ source = record.get(SUB_AGENT_PENDING_SOURCE_PATH_KEY)
861
+ if not isinstance(source, str) or not source:
862
+ continue
863
+ path = Path(source)
864
+ if record.get(SUB_AGENT_RESTORE_CONSUMED_KEY):
865
+ secure_unlink_owner_verified(path)
866
+ else:
867
+ self._archive(path, "unmatched")
868
+
869
+ def archive_unconsumed_restored_paused_records(self, records: list[dict[str, Any]]) -> None:
870
+ for record in records:
871
+ if record.get(SUB_AGENT_RESTORE_CONSUMED_KEY) or record.get(SUB_AGENT_REPAIR_FAILED_KEY):
872
+ # Skip repair-failed records for the same reason as finalize:
873
+ # archiving them out of pending would strand the "running" audit.
874
+ continue
875
+ source = record.get(SUB_AGENT_PENDING_SOURCE_PATH_KEY)
876
+ if isinstance(source, str) and source:
877
+ self._archive(Path(source), "unmatched")
878
+
879
+ def _read_audit_envelope_or_drop(self, path: Path) -> dict[str, Any] | None:
880
+ """Read one audit envelope, dropping it if it exceeds the read ceiling.
881
+
882
+ An over-cap audit envelope can never be parsed, repaired, or forked;
883
+ leaving it in place re-reads/re-allocates the whole file on every
884
+ restore and would strand a stuck-"running" status forever. Over-cap
885
+ files are dropped (mirroring the over-cap pending-record policy);
886
+ malformed-but-bounded files are skipped in place for later inspection.
887
+ """
888
+ # Keep the bounded READ and the PARSE in separate try blocks: the
889
+ # over-cap ValueError (from read_owner_verified_bounded) means DROP,
890
+ # but a parse-time ValueError must NOT — json.loads raises a *plain*
891
+ # ValueError for a valid-but-oversized integer (>4300 digits) and a
892
+ # RecursionError (not a ValueError at all) for deeply nested input,
893
+ # both of which can occur well under the byte ceiling. Fusing the two
894
+ # would misclassify those as over-cap and delete bounded evidence, or
895
+ # let RecursionError escape and abort hydration entirely.
896
+ try:
897
+ raw = _secure_read_bytes(path, max_bytes=MAX_SUB_AGENT_AUDIT_BYTES)
898
+ except ValueError:
899
+ # read_owner_verified_bounded raises a plain ValueError before any
900
+ # decode/parse when the file is over the ceiling — drop it.
901
+ logger.warning("Dropping over-sized sub-agent audit log %s", path.name)
902
+ secure_unlink_owner_verified(path)
903
+ return None
904
+ except OSError:
905
+ logger.warning("Failed to read sub-agent audit log %s", path.name, exc_info=True)
906
+ return None
907
+ try:
908
+ envelope = json.loads(raw.decode("utf-8"))
909
+ except UnicodeError, ValueError, RecursionError:
910
+ # Malformed / oversized-int / deeply nested but byte-bounded:
911
+ # preserve in place for later inspection (never delete).
912
+ logger.warning("Skipping malformed sub-agent audit log %s", path.name, exc_info=True)
913
+ return None
914
+ return envelope if isinstance(envelope, dict) else None
915
+
916
+ def _log_files_by_invocation(self) -> dict[str, str]:
917
+ directory = sessions_dir(self._session_dir)
918
+ found: dict[str, str] = {}
919
+ duplicate: set[str] = set()
920
+ # Bound the audit scan by BOTH count and cumulative on-disk size: this
921
+ # builds a map by reading+parsing every audit file, so an unbounded
922
+ # listing (or unbounded reads) would OOM/stall restore before drain's
923
+ # per-record caps ever apply.
924
+ paths, truncated = scan_bounded_json_files(
925
+ directory,
926
+ max_files=MAX_SCANNED_AUDIT_FILES,
927
+ max_total_bytes=MAX_AUDIT_SCAN_TOTAL_BYTES,
928
+ )
929
+ if truncated:
930
+ logger.warning("Sub-agent audit log-file scan truncated; some records may lack a log backlink this restore")
931
+ for path in paths:
932
+ envelope = self._read_audit_envelope_or_drop(path)
933
+ if envelope is None:
934
+ continue
935
+ meta = envelope.get("meta")
936
+ if isinstance(meta, dict) and meta.get("runner") == "acp":
937
+ acp_state = envelope.get("acp_state")
938
+ try:
939
+ if not isinstance(acp_state, dict):
940
+ raise ValueError("ACP sub-agent log is missing ACP state")
941
+ validate_acp_state_file_references(
942
+ acp_state,
943
+ parent_session_dir=self._session_dir,
944
+ )
945
+ except ValueError:
946
+ continue
947
+ invocation_id = meta.get("invocation_id") if isinstance(meta, dict) else None
948
+ if not isinstance(invocation_id, str) or not invocation_id:
949
+ continue
950
+ if invocation_id in found:
951
+ duplicate.add(invocation_id)
952
+ else:
953
+ found[invocation_id] = path.name
954
+ for invocation_id in duplicate:
955
+ found.pop(invocation_id, None)
956
+ return found
957
+
958
+ def _archive(self, path: Path, category: str) -> None:
959
+ try:
960
+ payload = _secure_read_bytes(path)
961
+ except FileNotFoundError:
962
+ return
963
+ except ValueError:
964
+ # Over the read ceiling: an over-cap file is exactly why we are
965
+ # quarantining it — never copy it forward, just drop the source.
966
+ logger.warning("Dropping over-sized sub-agent pending record %s", path.name)
967
+ secure_unlink_owner_verified(path)
968
+ return
969
+ except OSError:
970
+ logger.warning("Failed to read sub-agent pending record %s", path, exc_info=True)
971
+ return
972
+ try:
973
+ destination_dir = pending_dir(self._session_dir) / category
974
+ destination = destination_dir / path.name
975
+ for _attempt in range(100):
976
+ if not destination.exists():
977
+ break
978
+ destination = destination_dir / f"{path.stem}-{secrets.token_hex(4)}{path.suffix}"
979
+ atomic_write_owner_only_bytes(destination, payload)
980
+ secure_unlink_owner_verified(path)
981
+ except FileNotFoundError:
982
+ return
983
+ except OSError:
984
+ logger.warning("Failed to archive sub-agent pending record %s", path, exc_info=True)
985
+
986
+
987
+ def _secure_read_bytes(path: Path, *, max_bytes: int = _MAX_PENDING_RECORD_BYTES) -> bytes:
988
+ # Owner-verified, not owner-only: pre-existing session artifacts predate
989
+ # the owner-only writers and must stay readable after upgrade.
990
+ return read_owner_verified_bounded(path, max_bytes=max_bytes)
991
+
992
+
993
+ def _sub_agent_backlinks(parent_messages: list[Any]) -> tuple[set[str], set[str]]:
994
+ invocation_ids: set[str] = set()
995
+ log_files: set[str] = set()
996
+ for message in parent_messages:
997
+ for content in getattr(message, "contents", ()):
998
+ properties = getattr(content, "additional_properties", {})
999
+ metadata = properties.get(TOOL_RESULT_METADATA_KEY) if isinstance(properties, dict) else None
1000
+ if not isinstance(metadata, dict):
1001
+ continue
1002
+ invocation_id = metadata.get("sub_agent_invocation_id")
1003
+ if isinstance(invocation_id, str) and invocation_id:
1004
+ invocation_ids.add(invocation_id)
1005
+ log_file = metadata.get("sub_agent_log_file")
1006
+ if isinstance(log_file, str) and log_file:
1007
+ log_files.add(log_file)
1008
+ return invocation_ids, log_files
1009
+
1010
+
1011
+ _MAX_REFERENCE_WALK_DEPTH = 200
1012
+
1013
+
1014
+ def collect_sub_agent_log_file_references(root: Any) -> set[str]:
1015
+ """Collect every ``sub_agent_log_file`` value anywhere in a parsed envelope.
1016
+
1017
+ Used by the fork copier to guarantee every audit the forked history
1018
+ references is carried forward, so a bounded artifact scan can never strand
1019
+ a dangling reference in a legitimately large session. The walk is
1020
+ schema-agnostic (it keys on the field name, not the message/content
1021
+ nesting, so serialization changes cannot silently drop references),
1022
+ iterative, and depth-bounded so a deeply nested — but ``json``-parseable —
1023
+ envelope cannot exhaust the interpreter stack.
1024
+ """
1025
+ references: set[str] = set()
1026
+ stack: list[tuple[int, Any]] = [(0, root)]
1027
+ while stack:
1028
+ depth, node = stack.pop()
1029
+ if depth > _MAX_REFERENCE_WALK_DEPTH:
1030
+ continue
1031
+ if isinstance(node, dict):
1032
+ value = node.get("sub_agent_log_file")
1033
+ if isinstance(value, str) and value:
1034
+ references.add(value)
1035
+ stack.extend((depth + 1, child) for child in node.values() if isinstance(child, (dict, list)))
1036
+ elif isinstance(node, list):
1037
+ stack.extend((depth + 1, child) for child in node if isinstance(child, (dict, list)))
1038
+ return references
1039
+
1040
+
1041
+ def is_safe_sub_agent_log_basename(name: str) -> bool:
1042
+ """True iff *name* is a bare ``.json`` audit basename safe to join to a dir.
1043
+
1044
+ A ``sub_agent_log_file`` reference is chrys-authored, but the fork joins it
1045
+ to the parent's owner-writable sessions dir, so reject anything that is not
1046
+ a plain in-directory filename (no separators, no ``.``/``..``, no leading
1047
+ dot) before using it as a copy source.
1048
+ """
1049
+ if not name or not name.endswith(".json"):
1050
+ return False
1051
+ if name.startswith("."):
1052
+ return False
1053
+ if name in (".", ".."):
1054
+ return False
1055
+ return name == os.path.basename(name) and "/" not in name and "\\" not in name
1056
+
1057
+
1058
+ def _atomic_write_json(path: Path, envelope: dict[str, Any]) -> None:
1059
+ # A surrogateescaped workspace path (meta primary_cwd/working_dirs) or any
1060
+ # other field is made write-safe at the sink: atomic_write_owner_only_text
1061
+ # encodes with backslashreplace, so this cannot crash on a lone surrogate.
1062
+ atomic_write_owner_only_text(
1063
+ path,
1064
+ json.dumps(envelope, indent=2, ensure_ascii=False, allow_nan=False),
1065
+ )
1066
+
1067
+
1068
+ def _utc_now() -> str:
1069
+ return datetime.now(UTC).isoformat()