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
chrys/app/cli/serve.py ADDED
@@ -0,0 +1,1593 @@
1
+ # Copyright (c) 2024 Textualize
2
+ # Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
3
+ # Contains code adapted from textual-serve (MIT License; see NOTICE).
4
+
5
+ """Browser-hosted ``chrys serve`` command."""
6
+
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import asyncio
11
+ import getpass
12
+ import hashlib
13
+ import hmac
14
+ import html
15
+ import ipaddress
16
+ import json
17
+ import logging
18
+ import os
19
+ import secrets
20
+ import shlex
21
+ import subprocess
22
+ import sys
23
+ import time
24
+ from collections.abc import Sequence
25
+ from dataclasses import dataclass
26
+ from pathlib import Path
27
+ from typing import Any, Protocol
28
+ from urllib.parse import urlsplit
29
+
30
+ import idna
31
+ from rich.console import Console
32
+
33
+ from chrys import __version__
34
+ from chrys.app.cli.app import _TUI_SUBPROCESS_COMMAND
35
+ from chrys.app.tui.clipboard import BROWSER_CLIPBOARD_META_TYPE
36
+ from chrys.app.tui.util.logo import TUI_LOGO
37
+ from chrys.foundation.branding import APP_COMMAND, APP_DISPLAY_NAME
38
+
39
+ _DEFAULT_HOST = "localhost"
40
+ _DEFAULT_PORT = 7777
41
+ _WILDCARD_HOSTS = frozenset({"0.0.0.0", "::"})
42
+ _WARNING_STYLE = "yellow"
43
+ _ERROR_STYLE = "red"
44
+ _AUTH_COOKIE_NAME = "chrys_serve_auth"
45
+ _AUTH_SESSION_TTL_SECONDS = 12 * 60 * 60
46
+ _AUTH_SCRYPT_N = 2**14
47
+ _AUTH_SCRYPT_R = 8
48
+ _AUTH_SCRYPT_P = 1
49
+ _AUTH_LOGIN_PATH = "/auth/login"
50
+ _AUTH_LOGOUT_PATH = "/auth/logout"
51
+ _AUTH_MAX_FAILURES = 5
52
+ _AUTH_LOCKOUT_MESSAGE = "Too many failed password attempts. Browser access is locked."
53
+ _AUTH_CSRF_FIELD = "csrf_token"
54
+ _AUTH_CSRF_TTL_SECONDS = 10 * 60
55
+ _BROWSER_CLIPBOARD_SCRIPT_MARKER = "data-chrys-browser-clipboard"
56
+ _BROWSER_BRANDING_STYLE_MARKER = "data-chrys-serve-branding"
57
+ _BROWSER_CAPABILITY_SCRIPT_MARKER = "data-chrys-serve-capability"
58
+ _BROWSER_CAPABILITY_MESSAGE_TYPE = "chrys_serve_capability"
59
+ _BROWSER_CAPABILITY_TIMEOUT_SECONDS = 5.0
60
+ _SECURITY_HEADERS = {
61
+ "Content-Security-Policy": "frame-ancestors 'none'; base-uri 'none'; form-action 'self'",
62
+ "Cross-Origin-Opener-Policy": "same-origin",
63
+ "Cross-Origin-Resource-Policy": "same-origin",
64
+ "Referrer-Policy": "same-origin",
65
+ "X-Content-Type-Options": "nosniff",
66
+ "X-Frame-Options": "DENY",
67
+ }
68
+
69
+ logger = logging.getLogger(__name__)
70
+
71
+
72
+ @dataclass(frozen=True)
73
+ class ServeAuthConfig:
74
+ """Password authentication settings for browser-hosted Chrys."""
75
+
76
+ verifier: PasswordVerifier
77
+ cookie_name: str = _AUTH_COOKIE_NAME
78
+ session_ttl_seconds: int = _AUTH_SESSION_TTL_SECONDS
79
+ trust_forwarded_for: bool = False
80
+
81
+
82
+ class _TextualServeServer(Protocol):
83
+ """Subset of textual-serve's Server used by Chrys."""
84
+
85
+ def serve(self, *, debug: bool = False) -> None:
86
+ """Start serving browser sessions."""
87
+ ...
88
+
89
+
90
+ class _TextualServeServerFactory(Protocol):
91
+ """Callable constructor for textual-serve's Server."""
92
+
93
+ def __call__(
94
+ self,
95
+ command: str,
96
+ *,
97
+ host: str,
98
+ port: int,
99
+ title: str,
100
+ public_url: str | None,
101
+ auth_config: ServeAuthConfig | None = None,
102
+ ) -> _TextualServeServer:
103
+ """Create a textual-serve server."""
104
+ ...
105
+
106
+
107
+ @dataclass(frozen=True)
108
+ class PasswordVerifier:
109
+ """Salted password verifier for browser auth."""
110
+
111
+ salt: bytes
112
+ digest: bytes
113
+
114
+ @classmethod
115
+ def from_password(cls, password: str) -> PasswordVerifier:
116
+ """Hash *password* for later constant-time verification."""
117
+ if not password:
118
+ msg = "browser auth password cannot be empty"
119
+ raise RuntimeError(msg)
120
+ salt = secrets.token_bytes(16)
121
+ return cls(salt=salt, digest=_hash_password(password, salt))
122
+
123
+ def verify(self, password: str) -> bool:
124
+ """Return true when *password* matches this verifier."""
125
+ return hmac.compare_digest(self.digest, _hash_password(password, self.salt))
126
+
127
+
128
+ class _AuthSessionStore:
129
+ """In-memory browser auth sessions."""
130
+
131
+ def __init__(self, ttl_seconds: int) -> None:
132
+ self._ttl_seconds = ttl_seconds
133
+ self._sessions: dict[str, float] = {}
134
+
135
+ def create(self) -> str:
136
+ """Create a new session id and return it."""
137
+ self._prune_expired()
138
+ session_id = secrets.token_urlsafe(32)
139
+ self._sessions[session_id] = time.monotonic() + self._ttl_seconds
140
+ return session_id
141
+
142
+ def validate(self, session_id: str | None) -> bool:
143
+ """Return true when *session_id* exists and has not expired."""
144
+ if not session_id:
145
+ return False
146
+ expires_at = self._sessions.get(session_id)
147
+ now = time.monotonic()
148
+ if expires_at is None:
149
+ return False
150
+ if expires_at <= now:
151
+ self._sessions.pop(session_id, None)
152
+ return False
153
+ return True
154
+
155
+ def revoke(self, session_id: str | None) -> None:
156
+ """Remove a browser auth session if present."""
157
+ if session_id:
158
+ self._sessions.pop(session_id, None)
159
+
160
+ def _prune_expired(self) -> None:
161
+ """Remove expired sessions opportunistically."""
162
+ now = time.monotonic()
163
+ expired = [session_id for session_id, expires_at in self._sessions.items() if expires_at <= now]
164
+ for session_id in expired:
165
+ self._sessions.pop(session_id, None)
166
+
167
+
168
+ class _LoginCsrfStore:
169
+ """Short-lived one-time CSRF tokens for the browser auth form."""
170
+
171
+ def __init__(self, ttl_seconds: int = _AUTH_CSRF_TTL_SECONDS) -> None:
172
+ self._ttl_seconds = ttl_seconds
173
+ self._tokens: dict[str, float] = {}
174
+
175
+ def create(self) -> str:
176
+ """Create and remember a new CSRF token."""
177
+ self._prune_expired()
178
+ token = secrets.token_urlsafe(32)
179
+ self._tokens[token] = time.monotonic() + self._ttl_seconds
180
+ return token
181
+
182
+ def consume(self, token: str | None) -> bool:
183
+ """Consume *token* and return true when it existed and had not expired."""
184
+ if not token:
185
+ return False
186
+ expires_at = self._tokens.pop(token, None)
187
+ if expires_at is None:
188
+ return False
189
+ return expires_at > time.monotonic()
190
+
191
+ def _prune_expired(self) -> None:
192
+ """Remove expired CSRF tokens opportunistically."""
193
+ now = time.monotonic()
194
+ expired = [token for token, expires_at in self._tokens.items() if expires_at <= now]
195
+ for token in expired:
196
+ self._tokens.pop(token, None)
197
+
198
+
199
+ class _LoginRateLimiter:
200
+ """Process-lifetime failed-login limiter keyed by client address.
201
+
202
+ Clearing a locked client requires restarting ``chrys serve``.
203
+ """
204
+
205
+ def __init__(
206
+ self,
207
+ *,
208
+ max_failures: int = _AUTH_MAX_FAILURES,
209
+ ) -> None:
210
+ self._max_failures = max_failures
211
+ self._failures: dict[str, int] = {}
212
+ self._locked: set[str] = set()
213
+
214
+ def is_locked(self, key: str) -> bool:
215
+ """Return true when *key* is locked until process restart."""
216
+ return key in self._locked
217
+
218
+ def record_failure(self, key: str) -> bool:
219
+ """Record one failed login attempt for *key* and return true if locked."""
220
+ if key in self._locked:
221
+ return True
222
+ failures = self._failures.get(key, 0) + 1
223
+ if failures >= self._max_failures:
224
+ self._locked.add(key)
225
+ self._failures.pop(key, None)
226
+ return True
227
+ self._failures[key] = failures
228
+ return False
229
+
230
+ def record_success(self, key: str) -> None:
231
+ """Clear failed-login state for *key*."""
232
+ self._failures.pop(key, None)
233
+
234
+
235
+ def build_parser() -> argparse.ArgumentParser:
236
+ """Build the ``chrys serve`` argument parser."""
237
+ parser = argparse.ArgumentParser(
238
+ prog=f"{APP_COMMAND} serve",
239
+ description=f"Host the {APP_DISPLAY_NAME} TUI in a browser.",
240
+ add_help=False,
241
+ )
242
+ parser.add_argument(
243
+ "-h", "--help", action="help", default=argparse.SUPPRESS, help="Show this help message and exit"
244
+ )
245
+ parser.add_argument(
246
+ "--host",
247
+ default=_DEFAULT_HOST,
248
+ help="Host or IP address to bind; use 0.0.0.0 or :: to listen on all interfaces.",
249
+ )
250
+ parser.add_argument(
251
+ "--port",
252
+ type=_parse_port,
253
+ default=_DEFAULT_PORT,
254
+ help=f"Port to bind (default: {_DEFAULT_PORT}).",
255
+ )
256
+ parser.add_argument(
257
+ "--public-url",
258
+ default=None,
259
+ help=(
260
+ "Externally reachable URL to show in browser assets when behind a proxy or bound to all interfaces. "
261
+ "Reverse proxies must preserve the original Host header. For HTTPS proxy auth, the bound HTTP port "
262
+ "should not be reachable bypassing the proxy."
263
+ ),
264
+ )
265
+ parser.add_argument(
266
+ "--auth-required",
267
+ "--auth",
268
+ dest="auth_required",
269
+ action="store_true",
270
+ help=f"Require password authentication before browsers can access the {APP_DISPLAY_NAME} TUI.",
271
+ )
272
+ password_source = parser.add_mutually_exclusive_group()
273
+ password_source.add_argument(
274
+ "--auth-password-file",
275
+ type=Path,
276
+ default=None,
277
+ help="Read the browser auth password from a file instead of prompting.",
278
+ )
279
+ password_source.add_argument(
280
+ "--auth-password-env",
281
+ default=None,
282
+ metavar="ENV_VAR",
283
+ help="Read the browser auth password from an environment variable instead of prompting.",
284
+ )
285
+ parser.add_argument(
286
+ "--allow-insecure-auth",
287
+ action="store_true",
288
+ help=(
289
+ "Allow browser auth over non-loopback HTTP. Use only on trusted networks or through a tunnel; "
290
+ "passwords can be observed if the port is exposed."
291
+ ),
292
+ )
293
+ parser.add_argument(
294
+ "--auth-trust-forwarded-for",
295
+ action="store_true",
296
+ help=(
297
+ "Trust X-Forwarded-For for browser auth lockout keys. Use only behind a trusted reverse proxy that "
298
+ "overwrites this header."
299
+ ),
300
+ )
301
+ parser.add_argument(
302
+ "--debug",
303
+ action="store_true",
304
+ help="Enable textual-serve debug mode and Textual devtools for spawned app sessions.",
305
+ )
306
+ return parser
307
+
308
+
309
+ def build_tui_command() -> str:
310
+ """Return the shell command used by textual-serve to launch the Chrys TUI."""
311
+ return _shell_join([*_entrypoint_argv(), _TUI_SUBPROCESS_COMMAND])
312
+
313
+
314
+ def _entrypoint_argv() -> list[str]:
315
+ """Return a Chrys entrypoint argv that works for Python installs and PyApp."""
316
+ pyapp_launcher = os.environ.get("PYAPP")
317
+ if pyapp_launcher:
318
+ return [pyapp_launcher]
319
+ if _looks_like_python(sys.executable):
320
+ return [sys.executable, "-m", "chrys.app.cli.app"]
321
+ return [sys.argv[0]]
322
+
323
+
324
+ def _looks_like_python(executable: str) -> bool:
325
+ name = Path(executable).name.lower()
326
+ return name.startswith(("python", "pypy"))
327
+
328
+
329
+ def _parse_port(value: str) -> int:
330
+ try:
331
+ port = int(value)
332
+ except ValueError as exc:
333
+ msg = f"invalid port: {value!r}"
334
+ raise argparse.ArgumentTypeError(msg) from exc
335
+ if not 1 <= port <= 65535:
336
+ msg = f"port must be between 1 and 65535: {port}"
337
+ raise argparse.ArgumentTypeError(msg)
338
+ return port
339
+
340
+
341
+ def _to_int(value: str | None, default: int) -> int:
342
+ """Parse *value* as an int, returning *default* for invalid values."""
343
+ if value is None:
344
+ return default
345
+ try:
346
+ return int(value)
347
+ except ValueError:
348
+ return default
349
+
350
+
351
+ def _shell_join(argv: Sequence[str]) -> str:
352
+ """Join argv for the platform shell used by ``create_subprocess_shell``."""
353
+ if os.name == "nt":
354
+ return subprocess.list2cmdline(list(argv))
355
+ return shlex.join(argv)
356
+
357
+
358
+ def _load_server_class() -> _TextualServeServerFactory:
359
+ from textual_serve.server import Server
360
+
361
+ return _make_chrys_server_class(Server)
362
+
363
+
364
+ def _uses_textual_serve_base(base_server_class: type[Any]) -> bool:
365
+ """Return true when *base_server_class* is the real textual-serve server."""
366
+ return base_server_class.__module__.startswith("textual_serve.")
367
+
368
+
369
+ def _browser_clipboard_websocket_message(text: str) -> str:
370
+ """Return the browser websocket message used to request a clipboard write."""
371
+ return json.dumps([BROWSER_CLIPBOARD_META_TYPE, {"text": text}])
372
+
373
+
374
+ async def _send_browser_clipboard_message(meta_data: dict[str, object], remote_write_str: Any) -> bool:
375
+ """Forward a Chrys clipboard meta packet to the browser websocket."""
376
+ if meta_data.get("type") != BROWSER_CLIPBOARD_META_TYPE:
377
+ return False
378
+ text = meta_data.get("text")
379
+ if not isinstance(text, str):
380
+ logger.warning("Ignoring malformed browser clipboard metadata: text must be a string.")
381
+ return True
382
+ await remote_write_str(_browser_clipboard_websocket_message(text))
383
+ return True
384
+
385
+
386
+ def _browser_capability_script(capability: str) -> str:
387
+ """Return a same-origin bootstrap that authenticates browser WebSockets."""
388
+ script = r"""<script data-chrys-serve-capability>
389
+ (() => {
390
+ const MESSAGE_TYPE = __MESSAGE_TYPE__;
391
+ const CAPABILITY = __CAPABILITY__;
392
+ const NativeWebSocket = window.WebSocket;
393
+
394
+ function isTerminalSocket(url) {
395
+ const terminal = document.querySelector("[data-session-websocket-url]");
396
+ if (!terminal) {
397
+ return false;
398
+ }
399
+ const expected = new URL(terminal.dataset.sessionWebsocketUrl, window.location.href);
400
+ const actual = new URL(url, window.location.href);
401
+ return actual.origin === expected.origin && actual.pathname === expected.pathname;
402
+ }
403
+
404
+ // textual-serve constructs its socket after window.load. Registering this
405
+ // listener in the constructor guarantees the capability packet is queued
406
+ // before textual-serve's open listener sends resize, focus, or input data.
407
+ window.WebSocket = class ChrysAuthenticatedWebSocket extends NativeWebSocket {
408
+ constructor(url, protocols) {
409
+ if (protocols === undefined) {
410
+ super(url);
411
+ } else {
412
+ super(url, protocols);
413
+ }
414
+ if (isTerminalSocket(url)) {
415
+ this.addEventListener("open", () => {
416
+ this.send(JSON.stringify([MESSAGE_TYPE, CAPABILITY]));
417
+ }, {once: true});
418
+ }
419
+ }
420
+ };
421
+ })();
422
+ </script>"""
423
+ return script.replace("__MESSAGE_TYPE__", json.dumps(_BROWSER_CAPABILITY_MESSAGE_TYPE)).replace(
424
+ "__CAPABILITY__", json.dumps(capability)
425
+ )
426
+
427
+
428
+ async def _receive_browser_capability(websocket: Any, expected_capability: str) -> bool:
429
+ """Validate the first browser WebSocket packet before starting Chrys."""
430
+ try:
431
+ async with asyncio.timeout(_BROWSER_CAPABILITY_TIMEOUT_SECONDS):
432
+ message = await websocket.receive()
433
+ except TimeoutError:
434
+ return False
435
+ if not isinstance(message.data, str):
436
+ return False
437
+ try:
438
+ packet = json.loads(message.data)
439
+ except json.JSONDecodeError:
440
+ return False
441
+ if (
442
+ not isinstance(packet, list)
443
+ or len(packet) != 2
444
+ or packet[0] != _BROWSER_CAPABILITY_MESSAGE_TYPE
445
+ or not isinstance(packet[1], str)
446
+ ):
447
+ return False
448
+ return hmac.compare_digest(
449
+ packet[1].encode("utf-8", errors="surrogatepass"),
450
+ expected_capability.encode(),
451
+ )
452
+
453
+
454
+ def _browser_clipboard_support_script() -> str:
455
+ """Return client-side support for browser clipboard writes and fallback UI."""
456
+ script = r"""<script data-chrys-browser-clipboard>
457
+ (() => {
458
+ const TYPE = __TYPE__;
459
+ const DIALOG_ID = "chrys-browser-copy-dialog";
460
+ const STYLE_ID = "chrys-browser-copy-style";
461
+
462
+ function fallbackCopy(text) {
463
+ const textarea = document.createElement("textarea");
464
+ textarea.value = text;
465
+ textarea.setAttribute("readonly", "readonly");
466
+ textarea.style.position = "fixed";
467
+ textarea.style.left = "-10000px";
468
+ textarea.style.top = "0";
469
+ document.body.appendChild(textarea);
470
+ textarea.focus();
471
+ textarea.select();
472
+ try {
473
+ return document.execCommand("copy");
474
+ } catch {
475
+ return false;
476
+ } finally {
477
+ textarea.remove();
478
+ }
479
+ }
480
+
481
+ async function writeClipboard(text) {
482
+ if (navigator.clipboard && window.isSecureContext) {
483
+ await navigator.clipboard.writeText(text);
484
+ return true;
485
+ }
486
+ return fallbackCopy(text);
487
+ }
488
+
489
+ async function writeClipboardAutomatically(text) {
490
+ if (!(navigator.clipboard && window.isSecureContext)) {
491
+ return false;
492
+ }
493
+ await navigator.clipboard.writeText(text);
494
+ return true;
495
+ }
496
+
497
+ function ensureStyles() {
498
+ if (document.getElementById(STYLE_ID)) {
499
+ return;
500
+ }
501
+ const style = document.createElement("style");
502
+ style.id = STYLE_ID;
503
+ style.textContent = `
504
+ #${DIALOG_ID} {
505
+ position: fixed;
506
+ inset: 0;
507
+ z-index: 2147483647;
508
+ display: flex;
509
+ align-items: center;
510
+ justify-content: center;
511
+ background: rgba(0, 0, 0, 0.54);
512
+ color: #eef1f6;
513
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
514
+ }
515
+ #${DIALOG_ID} .chrys-copy-card {
516
+ width: min(780px, calc(100vw - 32px));
517
+ max-height: calc(100vh - 32px);
518
+ box-sizing: border-box;
519
+ display: grid;
520
+ grid-template-rows: auto minmax(260px, auto) auto auto;
521
+ gap: 8px;
522
+ overflow: hidden;
523
+ padding: 18px 18px 16px;
524
+ border: 1px solid #3a4354;
525
+ border-radius: 8px;
526
+ background: #10141c;
527
+ box-shadow: 0 24px 80px rgba(0, 0, 0, 0.5);
528
+ }
529
+ #${DIALOG_ID} .chrys-copy-title {
530
+ font-size: 15px;
531
+ font-weight: 700;
532
+ }
533
+ #${DIALOG_ID} textarea {
534
+ width: 100%;
535
+ height: min(430px, 54vh);
536
+ max-height: calc(100vh - 220px);
537
+ min-height: min(260px, calc(100vh - 220px));
538
+ resize: vertical;
539
+ box-sizing: border-box;
540
+ padding: 10px;
541
+ border: 1px solid #303744;
542
+ border-radius: 6px;
543
+ color: #f8fafc;
544
+ background: #080a0f;
545
+ font: 13px/1.45 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
546
+ }
547
+ #${DIALOG_ID} .chrys-copy-actions {
548
+ display: flex;
549
+ justify-content: flex-end;
550
+ gap: 4px;
551
+ }
552
+ #${DIALOG_ID} button {
553
+ display: inline-flex;
554
+ align-items: center;
555
+ justify-content: center;
556
+ min-height: 36px;
557
+ min-width: 64px;
558
+ margin: 0;
559
+ padding: 0 12px;
560
+ border: 1px solid #3a4354;
561
+ border-radius: 6px;
562
+ color: #eef1f6;
563
+ background: #1c2330;
564
+ font: 700 13px/1 ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
565
+ cursor: pointer;
566
+ }
567
+ #${DIALOG_ID} button:hover {
568
+ background: #263244;
569
+ }
570
+ #${DIALOG_ID} button[data-primary="true"] {
571
+ color: #ffffff;
572
+ background: #1769e0;
573
+ border-color: #2f89ff;
574
+ box-shadow: 0 0 0 1px rgba(47, 137, 255, 0.24);
575
+ }
576
+ #${DIALOG_ID} button[data-primary="true"]:hover {
577
+ background: #2180ff;
578
+ border-color: #61a8ff;
579
+ }
580
+ #${DIALOG_ID} .chrys-copy-status {
581
+ min-height: 0;
582
+ color: #c3cbd8;
583
+ font-size: 12px;
584
+ }
585
+ #${DIALOG_ID} .chrys-copy-status:empty {
586
+ display: none;
587
+ }
588
+ `;
589
+ document.head.appendChild(style);
590
+ }
591
+
592
+ function closeDialog() {
593
+ document.getElementById(DIALOG_ID)?.remove();
594
+ }
595
+
596
+ function showDialog(text) {
597
+ ensureStyles();
598
+ closeDialog();
599
+ const overlay = document.createElement("div");
600
+ overlay.id = DIALOG_ID;
601
+
602
+ const card = document.createElement("div");
603
+ card.className = "chrys-copy-card";
604
+
605
+ const title = document.createElement("div");
606
+ title.className = "chrys-copy-title";
607
+ title.textContent = __APP_DISPLAY_NAME__;
608
+
609
+ const textarea = document.createElement("textarea");
610
+ textarea.value = text;
611
+
612
+ const status = document.createElement("div");
613
+ status.className = "chrys-copy-status";
614
+
615
+ const actions = document.createElement("div");
616
+ actions.className = "chrys-copy-actions";
617
+
618
+ const close = document.createElement("button");
619
+ close.type = "button";
620
+ close.textContent = "Close";
621
+ close.addEventListener("click", closeDialog);
622
+
623
+ const copy = document.createElement("button");
624
+ copy.type = "button";
625
+ copy.dataset.primary = "true";
626
+ copy.textContent = "Copy";
627
+ copy.addEventListener("click", async () => {
628
+ try {
629
+ if (await writeClipboard(text)) {
630
+ closeDialog();
631
+ return;
632
+ }
633
+ } catch {}
634
+ textarea.focus();
635
+ textarea.select();
636
+ status.textContent = "Copy is blocked. Use the selected text with your browser copy shortcut.";
637
+ });
638
+
639
+ actions.append(close, copy);
640
+ card.append(title, textarea, status, actions);
641
+ overlay.appendChild(card);
642
+ overlay.addEventListener("click", (event) => {
643
+ if (event.target === overlay) {
644
+ closeDialog();
645
+ }
646
+ });
647
+ document.body.appendChild(overlay);
648
+ copy.focus();
649
+ }
650
+
651
+ async function handleClipboardMessage(text) {
652
+ try {
653
+ if (await writeClipboardAutomatically(text)) {
654
+ return;
655
+ }
656
+ } catch {}
657
+ showDialog(text);
658
+ }
659
+
660
+ // textual-serve serves one WebSocket-driven terminal per page. This wrapper
661
+ // assumes that page shape; if future pages host unrelated WebSockets, move
662
+ // this hook to the terminal's specific socket setup instead of wrapping the
663
+ // prototype globally.
664
+ const originalAddEventListener = WebSocket.prototype.addEventListener;
665
+ WebSocket.prototype.addEventListener = function(type, listener, options) {
666
+ if (type !== "message" || typeof listener !== "function") {
667
+ return originalAddEventListener.call(this, type, listener, options);
668
+ }
669
+ const wrapped = function(event) {
670
+ if (typeof event.data === "string") {
671
+ try {
672
+ const packet = JSON.parse(event.data);
673
+ if (Array.isArray(packet) && packet[0] === TYPE) {
674
+ const payload = packet[1] || {};
675
+ handleClipboardMessage(typeof payload.text === "string" ? payload.text : "");
676
+ return;
677
+ }
678
+ } catch {}
679
+ }
680
+ return listener.call(this, event);
681
+ };
682
+ return originalAddEventListener.call(this, type, wrapped, options);
683
+ };
684
+ })();
685
+ </script>"""
686
+ return script.replace("__TYPE__", json.dumps(BROWSER_CLIPBOARD_META_TYPE)).replace(
687
+ "__APP_DISPLAY_NAME__", json.dumps(f"Copy from {APP_DISPLAY_NAME}")
688
+ )
689
+
690
+
691
+ def _browser_serve_branding_style() -> str:
692
+ """Return CSS that replaces textual-serve splash branding with Chrys branding."""
693
+ return f"""<style {_BROWSER_BRANDING_STYLE_MARKER}>
694
+ .intro {{
695
+ width: min(720px, calc(100vw - 32px));
696
+ height: auto;
697
+ min-height: 260px;
698
+ box-sizing: border-box;
699
+ flex-direction: column;
700
+ }}
701
+ .intro > svg,
702
+ .intro > svg + div {{
703
+ display: none !important;
704
+ }}
705
+ .intro .chrys-serve-logo {{
706
+ margin: 0;
707
+ padding: 0;
708
+ color: rgba(255, 255, 255, 0.95);
709
+ font: 700 16px/1 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
710
+ letter-spacing: 0;
711
+ white-space: pre;
712
+ text-align: left;
713
+ }}
714
+ </style>"""
715
+
716
+
717
+ def _inject_browser_serve_branding_logo(body: str) -> str:
718
+ """Inject the Chrys TUI logo into textual-serve's splash markup."""
719
+ if "chrys-serve-logo" in body:
720
+ return body
721
+ logo = html.escape(TUI_LOGO.strip("\n"))
722
+ intro_marker = '<div class="intro">'
723
+ if intro_marker not in body:
724
+ return body
725
+ label = html.escape(APP_DISPLAY_NAME, quote=True)
726
+ return body.replace(
727
+ intro_marker, f'{intro_marker}<pre class="chrys-serve-logo" aria-label="{label}">{logo}</pre>', 1
728
+ )
729
+
730
+
731
+ def _make_chrys_server_class(base_server_class: type[Any]) -> _TextualServeServerFactory:
732
+ """Return a textual-serve server class with Chrys-branded startup output."""
733
+ from aiohttp import web
734
+
735
+ use_browser_clipboard_bridge = _uses_textual_serve_base(base_server_class)
736
+ response_prepared_request_key = web.RequestKey("chrys_serve_response_prepared", bool)
737
+
738
+ if use_browser_clipboard_bridge:
739
+ from textual_serve.app_service import AppService as TextualServeAppService
740
+
741
+ # Relies on textual-serve 1.1.3 internals: AppService.on_meta() and
742
+ # Server.handle_websocket() constructing AppService directly. Keep
743
+ # tests/app/cli/test_serve.py's contract check aligned when upgrading.
744
+ class ChrysAppService(TextualServeAppService):
745
+ """textual-serve app service that forwards Chrys clipboard meta packets."""
746
+
747
+ async def on_meta(self, data: bytes) -> None:
748
+ meta_data: dict[str, object] = json.loads(data)
749
+ if await _send_browser_clipboard_message(meta_data, self.remote_write_str):
750
+ return
751
+ await super().on_meta(data)
752
+
753
+ class ChrysServeServer(base_server_class):
754
+ """textual-serve Server with Chrys-branded startup output."""
755
+
756
+ def __init__(
757
+ self,
758
+ command: str,
759
+ *,
760
+ host: str,
761
+ port: int,
762
+ title: str,
763
+ public_url: str | None,
764
+ auth_config: ServeAuthConfig | None = None,
765
+ ) -> None:
766
+ """Create a Chrys browser server."""
767
+ effective_public_url = _normalize_public_url(
768
+ _effective_public_url(host=host, port=port, public_url=public_url)
769
+ )
770
+ super().__init__(
771
+ command,
772
+ host=host,
773
+ port=port,
774
+ title=title,
775
+ public_url=effective_public_url,
776
+ )
777
+ self._auth_config = auth_config
778
+ self._auth_sessions = (
779
+ _AuthSessionStore(auth_config.session_ttl_seconds) if auth_config is not None else None
780
+ )
781
+ self._auth_rate_limiter = _LoginRateLimiter() if auth_config is not None else None
782
+ self._auth_csrf_tokens = _LoginCsrfStore() if auth_config is not None else None
783
+ self._browser_capability = secrets.token_urlsafe(32)
784
+
785
+ async def _make_app(self) -> Any:
786
+ """Create the aiohttp app and install security/auth middleware."""
787
+ app = await super()._make_app()
788
+ app.middlewares.insert(0, self._security_middleware)
789
+ app.on_response_prepare.append(self._prepare_browser_security_headers)
790
+ if use_browser_clipboard_bridge:
791
+ app.middlewares.append(self._browser_support_middleware)
792
+ if self._auth_config is not None:
793
+ app.middlewares.append(self._auth_middleware)
794
+ app.add_routes(
795
+ [
796
+ web.get(_AUTH_LOGIN_PATH, self.handle_auth_login_page, name="auth_login"),
797
+ web.post(_AUTH_LOGIN_PATH, self.handle_auth_login_submit, name="auth_login_submit"),
798
+ web.get(_AUTH_LOGOUT_PATH, self.handle_auth_logout, name="auth_logout"),
799
+ ]
800
+ )
801
+ return app
802
+
803
+ async def _prepare_browser_security_headers(self, request: Any, response: Any) -> None:
804
+ """Apply security headers before aiohttp sends a prepared response."""
805
+ _apply_browser_security_headers(response)
806
+ request[response_prepared_request_key] = True
807
+
808
+ # Relies on textual-serve 1.1.3 internals: on_startup(), self.console,
809
+ # and self.public_url. Re-audit this override when bumping textual-serve.
810
+ async def on_startup(self, _app: object) -> None:
811
+ """Print the Chrys serve banner."""
812
+ self.console.print("")
813
+ self.console.print(_icode_serve_logo(), highlight=False)
814
+ self.console.print(
815
+ f"Serving {APP_DISPLAY_NAME} TUI on {self.public_url} [cyan](Press Ctrl+C to quit)[/cyan]\n"
816
+ )
817
+
818
+ async def handle_websocket(self, request: Any) -> Any:
819
+ """Handle browser websocket sessions with Chrys clipboard metadata support."""
820
+ if not _request_matches_public_origin(request, self.public_url, allow_referer=False):
821
+ raise web.HTTPForbidden(text="Invalid WebSocket origin")
822
+ if not use_browser_clipboard_bridge:
823
+ return await super().handle_websocket(request)
824
+
825
+ # Intentional copy of textual-serve 1.1.3's handle_websocket()
826
+ # with AppService swapped for ChrysAppService. This preserves
827
+ # upstream's stop-on-normal-path plus stop-in-finally behavior.
828
+ websocket = web.WebSocketResponse(heartbeat=15)
829
+ width = _to_int(request.query.get("width"), 80)
830
+ height = _to_int(request.query.get("height"), 24)
831
+
832
+ app_service: Any | None = None
833
+ try:
834
+ await websocket.prepare(request)
835
+ if not await _receive_browser_capability(websocket, self._browser_capability):
836
+ await websocket.close(code=1008, message=b"Invalid browser capability")
837
+ return websocket
838
+
839
+ async def close_websocket() -> None:
840
+ await websocket.close()
841
+
842
+ app_service = ChrysAppService(
843
+ self.command,
844
+ write_bytes=websocket.send_bytes,
845
+ write_str=websocket.send_str,
846
+ close=close_websocket,
847
+ download_manager=self.download_manager,
848
+ debug=self.debug,
849
+ )
850
+ await app_service.start(width, height)
851
+ try:
852
+ await self._process_messages(websocket, app_service)
853
+ finally:
854
+ await app_service.stop()
855
+ except asyncio.CancelledError:
856
+ await websocket.close()
857
+ except Exception:
858
+ logger.exception("Browser websocket session failed")
859
+ finally:
860
+ if app_service is not None:
861
+ await app_service.stop()
862
+
863
+ return websocket
864
+
865
+ @web.middleware
866
+ async def _security_middleware(self, request: Any, handler: Any) -> Any:
867
+ """Reject unrecognized authorities and apply browser hardening headers."""
868
+ if not _request_matches_public_host(request, self.public_url):
869
+ response = web.Response(status=421, text="Invalid Host header")
870
+ return _apply_browser_security_headers(response)
871
+ try:
872
+ response = await handler(request)
873
+ except web.HTTPException as exc:
874
+ if request.get(response_prepared_request_key, False):
875
+ msg = "HTTP exception raised after response headers were sent"
876
+ raise RuntimeError(msg) from exc
877
+ _apply_browser_security_headers(exc)
878
+ raise
879
+ except Exception:
880
+ if request.get(response_prepared_request_key, False):
881
+ raise
882
+ logger.exception("Unhandled browser HTTP request")
883
+ response = web.Response(status=500, text="Internal Server Error")
884
+ return _apply_browser_security_headers(response)
885
+ if not response.prepared:
886
+ _apply_browser_security_headers(response)
887
+ return response
888
+
889
+ @web.middleware
890
+ async def _browser_support_middleware(self, request: Any, handler: Any) -> Any:
891
+ """Inject Chrys browser support into textual-serve's app page."""
892
+ response = await handler(request)
893
+ if request.method != "GET" or not isinstance(response, web.Response):
894
+ return response
895
+ if response.content_type != "text/html" or response.body is None:
896
+ return response
897
+
898
+ charset = response.charset or "utf-8"
899
+ try:
900
+ body = response.body.decode(charset)
901
+ except UnicodeDecodeError:
902
+ return response
903
+ if "data-session-websocket-url" not in body or "</head>" not in body:
904
+ return response
905
+
906
+ branding = "" if _BROWSER_BRANDING_STYLE_MARKER in body else _browser_serve_branding_style()
907
+ capability = (
908
+ ""
909
+ if _BROWSER_CAPABILITY_SCRIPT_MARKER in body
910
+ else _browser_capability_script(self._browser_capability)
911
+ )
912
+ clipboard = "" if _BROWSER_CLIPBOARD_SCRIPT_MARKER in body else _browser_clipboard_support_script()
913
+ body = _inject_browser_serve_branding_logo(body)
914
+ response.text = body.replace(
915
+ "</head>",
916
+ f"{branding}{capability}{clipboard}</head>",
917
+ 1,
918
+ )
919
+ return response
920
+
921
+ @web.middleware
922
+ async def _auth_middleware(self, request: Any, handler: Any) -> Any:
923
+ """Require a valid auth session for TUI entrypoints."""
924
+ if self._auth_config is None or self._auth_sessions is None:
925
+ return await handler(request)
926
+ if _is_auth_public_path(request.path):
927
+ return await handler(request)
928
+ if self._auth_sessions.validate(request.cookies.get(self._auth_config.cookie_name)):
929
+ return await handler(request)
930
+ if request.method == "GET" and request.path == "/":
931
+ return self._render_auth_login_page()
932
+ raise web.HTTPUnauthorized(text="Authentication required")
933
+
934
+ async def handle_auth_login_page(self, _request: Any) -> Any:
935
+ """Render the browser auth login page."""
936
+ return self._render_auth_login_page()
937
+
938
+ async def handle_auth_login_submit(self, request: Any) -> Any:
939
+ """Verify the browser auth password and create a session cookie."""
940
+ if (
941
+ self._auth_config is None
942
+ or self._auth_sessions is None
943
+ or self._auth_rate_limiter is None
944
+ or self._auth_csrf_tokens is None
945
+ ):
946
+ raise web.HTTPNotFound()
947
+ if not _request_matches_public_origin(request, self.public_url):
948
+ raise web.HTTPForbidden(text="Invalid login origin")
949
+
950
+ remote_key = _request_remote_key(request, trust_forwarded_for=self._auth_config.trust_forwarded_for)
951
+ if self._auth_rate_limiter.is_locked(remote_key):
952
+ return self._render_auth_login_page(status=429, error=_AUTH_LOCKOUT_MESSAGE)
953
+
954
+ form = await request.post()
955
+ token = form.get(_AUTH_CSRF_FIELD, "")
956
+ if not isinstance(token, str) or not self._auth_csrf_tokens.consume(token):
957
+ raise web.HTTPForbidden(text="Invalid login token")
958
+
959
+ password = form.get("password", "")
960
+ if not isinstance(password, str) or not self._auth_config.verifier.verify(password):
961
+ if self._auth_rate_limiter.record_failure(remote_key):
962
+ return self._render_auth_login_page(status=429, error=_AUTH_LOCKOUT_MESSAGE)
963
+ return self._render_auth_login_page(status=401, error="Invalid password.")
964
+
965
+ self._auth_rate_limiter.record_success(remote_key)
966
+ session_id = self._auth_sessions.create()
967
+ response = web.HTTPFound(_browser_root_path(self.public_url))
968
+ response.set_cookie(
969
+ self._auth_config.cookie_name,
970
+ session_id,
971
+ max_age=self._auth_config.session_ttl_seconds,
972
+ httponly=True,
973
+ secure=_is_https_url(self.public_url),
974
+ samesite="Strict",
975
+ path=_auth_cookie_path(self.public_url),
976
+ )
977
+ raise response
978
+
979
+ async def handle_auth_logout(self, request: Any) -> Any:
980
+ """Revoke the browser auth session cookie."""
981
+ if self._auth_config is not None and self._auth_sessions is not None:
982
+ self._auth_sessions.revoke(request.cookies.get(self._auth_config.cookie_name))
983
+ cookie_name = self._auth_config.cookie_name if self._auth_config is not None else _AUTH_COOKIE_NAME
984
+ response = web.HTTPFound(_browser_path(self.public_url, _AUTH_LOGIN_PATH))
985
+ response.del_cookie(cookie_name, path=_auth_cookie_path(self.public_url))
986
+ raise response
987
+
988
+ def _render_auth_login_page(self, *, status: int = 200, error: str = "") -> Any:
989
+ """Render a standalone login page before the TUI starts."""
990
+ title = html.escape(self.title)
991
+ logo = html.escape(TUI_LOGO.strip("\n"))
992
+ error_html = f'<div class="error">{html.escape(error)}</div>' if error else ""
993
+ csrf_token = html.escape(self._auth_csrf_tokens.create() if self._auth_csrf_tokens is not None else "")
994
+ login_action = html.escape(_browser_path(self.public_url, _AUTH_LOGIN_PATH), quote=True)
995
+ return web.Response(
996
+ status=status,
997
+ text=f"""<!doctype html>
998
+ <html lang="en">
999
+ <head>
1000
+ <meta charset="utf-8">
1001
+ <meta name="viewport" content="width=device-width, initial-scale=1">
1002
+ <title>{title} Authentication</title>
1003
+ <style>
1004
+ :root {{
1005
+ color-scheme: dark;
1006
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
1007
+ background: #141923;
1008
+ color: #eef1f6;
1009
+ }}
1010
+ body {{
1011
+ min-height: 100vh;
1012
+ margin: 0;
1013
+ display: grid;
1014
+ place-items: center;
1015
+ background: #141923;
1016
+ }}
1017
+ main {{
1018
+ width: min(440px, calc(100vw - 32px));
1019
+ box-sizing: border-box;
1020
+ padding: 32px;
1021
+ background: #10141c;
1022
+ border: 1px solid #333b49;
1023
+ border-radius: 8px;
1024
+ box-shadow: 0 30px 90px rgba(0, 0, 0, 0.56), 0 0 0 1px rgba(255, 255, 255, 0.03);
1025
+ }}
1026
+ .logo {{
1027
+ width: fit-content;
1028
+ max-width: 100%;
1029
+ margin: 0 auto 30px;
1030
+ overflow: hidden;
1031
+ color: #f8fafc;
1032
+ font: 700 10px/1.05 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
1033
+ letter-spacing: 0;
1034
+ white-space: pre;
1035
+ }}
1036
+ label {{
1037
+ display: block;
1038
+ margin-bottom: 8px;
1039
+ color: #a8b0bd;
1040
+ font-size: 13px;
1041
+ line-height: 1.3;
1042
+ }}
1043
+ input {{
1044
+ width: 100%;
1045
+ box-sizing: border-box;
1046
+ min-height: 44px;
1047
+ padding: 10px 12px;
1048
+ color: #f8fafc;
1049
+ background: #080a0f;
1050
+ border: 1px solid #303744;
1051
+ border-radius: 6px;
1052
+ font: 15px/1.4 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
1053
+ }}
1054
+ input:focus {{
1055
+ border-color: #8ca3ff;
1056
+ outline: 2px solid #8ca3ff;
1057
+ outline-offset: 2px;
1058
+ }}
1059
+ button {{
1060
+ width: 100%;
1061
+ min-height: 44px;
1062
+ margin-top: 18px;
1063
+ border: 0;
1064
+ border-radius: 6px;
1065
+ color: #080a0f;
1066
+ background: #f8fafc;
1067
+ font: 700 14px/1.2 ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
1068
+ cursor: pointer;
1069
+ }}
1070
+ button:hover {{
1071
+ background: #dbe4f0;
1072
+ }}
1073
+ .error {{
1074
+ margin-bottom: 16px;
1075
+ color: #ffd7d7;
1076
+ background: #2a1116;
1077
+ border: 1px solid #67313a;
1078
+ border-radius: 6px;
1079
+ padding: 10px 12px;
1080
+ font-size: 13px;
1081
+ line-height: 1.4;
1082
+ }}
1083
+ </style>
1084
+ </head>
1085
+ <body>
1086
+ <main>
1087
+ <pre class="logo" aria-label="{title}">{logo}</pre>
1088
+ {error_html}
1089
+ <form method="post" action="{login_action}">
1090
+ <input type="hidden" name="{_AUTH_CSRF_FIELD}" value="{csrf_token}">
1091
+ <label for="password">Password</label>
1092
+ <input id="password" name="password" type="password" autocomplete="current-password" autofocus required>
1093
+ <button type="submit">Unlock</button>
1094
+ </form>
1095
+ </main>
1096
+ </body>
1097
+ </html>
1098
+ """,
1099
+ content_type="text/html",
1100
+ charset="utf-8",
1101
+ headers={
1102
+ "Cache-Control": "no-store",
1103
+ "Content-Security-Policy": (
1104
+ "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; "
1105
+ "base-uri 'none'; frame-ancestors 'none'"
1106
+ ),
1107
+ },
1108
+ )
1109
+
1110
+ return ChrysServeServer
1111
+
1112
+
1113
+ def _icode_serve_logo() -> str:
1114
+ """Return the iCode-branded serve startup banner."""
1115
+ return f"""[bold magenta]░▀█▀░█▀▀░█▀█░█▀▄░█▀▀░░░░░█▀▀░█▀▀░█▀▄░█░█░█▀▀
1116
+ ░░█░░█░░░█░█░█░█░█▀▀░▄▄▄░▀▀█░█▀▀░█▀▄░▀▄▀░█▀▀
1117
+ ░▀▀▀░▀▀▀░▀▀▀░▀▀░░▀▀▀░░░░░▀▀▀░▀▀▀░▀░▀░░▀░░▀▀▀ v{__version__}[/bold magenta]\n"""
1118
+
1119
+
1120
+ def run_command(args: argparse.Namespace) -> None:
1121
+ """Run textual-serve with the parsed command arguments."""
1122
+ effective_public_url = _normalize_public_url(
1123
+ _effective_public_url(host=args.host, port=args.port, public_url=args.public_url)
1124
+ )
1125
+ auth_requested = _auth_requested(args)
1126
+ if auth_requested:
1127
+ _validate_auth_transport_security(args)
1128
+ auth_config = _resolve_auth_config(args)
1129
+ _warn_if_public_url_missing_for_wildcard_host(host=args.host, port=args.port, public_url=args.public_url)
1130
+ if auth_config is not None and args.allow_insecure_auth:
1131
+ _warn_if_auth_uses_plain_http(host=args.host, port=args.port, public_url=args.public_url)
1132
+ server_class = _load_server_class()
1133
+ server_kwargs: dict[str, Any] = {
1134
+ "host": args.host,
1135
+ "port": args.port,
1136
+ "title": APP_DISPLAY_NAME,
1137
+ "public_url": effective_public_url,
1138
+ }
1139
+ if auth_config is not None:
1140
+ server_kwargs["auth_config"] = auth_config
1141
+ server = server_class(
1142
+ build_tui_command(),
1143
+ **server_kwargs,
1144
+ )
1145
+ server.serve(debug=args.debug)
1146
+
1147
+
1148
+ def _resolve_auth_config(args: argparse.Namespace) -> ServeAuthConfig | None:
1149
+ """Resolve browser auth configuration from parsed CLI args."""
1150
+ if not _auth_requested(args):
1151
+ return None
1152
+ if args.auth_password_file is not None:
1153
+ password = _read_password_file(args.auth_password_file)
1154
+ elif args.auth_password_env is not None:
1155
+ password = _read_password_env(args.auth_password_env)
1156
+ else:
1157
+ password = _prompt_for_password()
1158
+ return ServeAuthConfig(
1159
+ verifier=PasswordVerifier.from_password(password),
1160
+ trust_forwarded_for=getattr(args, "auth_trust_forwarded_for", False),
1161
+ )
1162
+
1163
+
1164
+ def _auth_requested(args: argparse.Namespace) -> bool:
1165
+ """Return true when any browser auth option requests auth."""
1166
+ return args.auth_required or args.auth_password_file is not None or args.auth_password_env is not None
1167
+
1168
+
1169
+ def _validate_auth_transport_security(args: argparse.Namespace) -> None:
1170
+ """Reject browser auth over externally reachable HTTP unless explicitly allowed."""
1171
+ if args.allow_insecure_auth:
1172
+ return
1173
+ if _auth_transport_requires_insecure_override(host=args.host, port=args.port, public_url=args.public_url):
1174
+ msg = (
1175
+ "browser auth over non-loopback HTTP is unsafe because passwords can be observed on the network. "
1176
+ "Use an HTTPS --public-url through a reverse proxy, bind to localhost, or pass --allow-insecure-auth "
1177
+ "only on a trusted network or tunnel."
1178
+ )
1179
+ raise RuntimeError(msg)
1180
+
1181
+
1182
+ def _normalize_public_url(public_url: str) -> str:
1183
+ """Validate and normalize a browser-facing URL for textual-serve."""
1184
+ origin = _origin_tuple(public_url)
1185
+ if origin is None:
1186
+ msg = "--public-url must be an absolute http:// or https:// URL without credentials, query, or fragment"
1187
+ raise RuntimeError(msg)
1188
+ parsed = urlsplit(public_url)
1189
+ if parsed.query or parsed.fragment or _path_requires_browser_rewrite(parsed.path):
1190
+ msg = (
1191
+ "--public-url must be an absolute http:// or https:// URL without credentials, query, fragment, "
1192
+ "backslashes, or dot path segments"
1193
+ )
1194
+ raise RuntimeError(msg)
1195
+ scheme, host, port = origin
1196
+ url_host = _format_url_host(host)
1197
+ default_port = 80 if scheme == "http" else 443
1198
+ authority = url_host if port == default_port else f"{url_host}:{port}"
1199
+ path = parsed.path.strip("/")
1200
+ base_path = f"/{path}" if path else ""
1201
+ return f"{scheme}://{authority}{base_path}"
1202
+
1203
+
1204
+ def _path_requires_browser_rewrite(path: str) -> bool:
1205
+ """Return true when a special-URL browser would rewrite *path*."""
1206
+ if "\\" in path:
1207
+ return True
1208
+ browser_dot_segments = {".", "%2e", "..", ".%2e", "%2e.", "%2e%2e"}
1209
+ return any(segment.lower() in browser_dot_segments for segment in path.split("/"))
1210
+
1211
+
1212
+ def _read_password_file(path: Path) -> str:
1213
+ """Read a browser auth password from *path*."""
1214
+ expanded = path.expanduser()
1215
+ try:
1216
+ return expanded.read_text(encoding="utf-8").rstrip("\r\n")
1217
+ except (OSError, UnicodeError) as exc:
1218
+ msg = f"failed to read --auth-password-file {expanded}: {exc}"
1219
+ raise RuntimeError(msg) from exc
1220
+
1221
+
1222
+ def _read_password_env(env_name: str) -> str:
1223
+ """Read a browser auth password from an environment variable, then remove it."""
1224
+ password = os.environ.get(env_name)
1225
+ if password is None:
1226
+ msg = f"--auth-password-env {env_name!r} is not set"
1227
+ raise RuntimeError(msg)
1228
+ os.environ.pop(env_name, None)
1229
+ return password
1230
+
1231
+
1232
+ def _prompt_for_password() -> str:
1233
+ """Prompt securely for the browser auth password."""
1234
+ password = getpass.getpass("Password for browser access: ")
1235
+ confirm = getpass.getpass("Confirm password: ")
1236
+ if password != confirm:
1237
+ msg = "browser auth passwords did not match"
1238
+ raise RuntimeError(msg)
1239
+ return password
1240
+
1241
+
1242
+ def _hash_password(password: str, salt: bytes) -> bytes:
1243
+ """Hash a browser auth password with scrypt."""
1244
+ return hashlib.scrypt(
1245
+ password.encode("utf-8"),
1246
+ salt=salt,
1247
+ n=_AUTH_SCRYPT_N,
1248
+ r=_AUTH_SCRYPT_R,
1249
+ p=_AUTH_SCRYPT_P,
1250
+ )
1251
+
1252
+
1253
+ def _is_auth_public_path(path: str) -> bool:
1254
+ """Return true for routes that must be reachable before login."""
1255
+ return path == _AUTH_LOGIN_PATH
1256
+
1257
+
1258
+ def _request_remote_key(request: Any, *, trust_forwarded_for: bool = False) -> str:
1259
+ """Return the client key used for login rate limiting."""
1260
+ if trust_forwarded_for:
1261
+ forwarded_for = _trusted_forwarded_for(request)
1262
+ if forwarded_for is not None:
1263
+ return forwarded_for
1264
+ if request.remote:
1265
+ return str(request.remote)
1266
+ peername = request.transport.get_extra_info("peername") if request.transport is not None else None
1267
+ if isinstance(peername, tuple) and peername:
1268
+ return str(peername[0])
1269
+ return "unknown"
1270
+
1271
+
1272
+ def _trusted_forwarded_for(request: Any) -> str | None:
1273
+ """Return a trusted X-Forwarded-For client address if the header is valid."""
1274
+ forwarded_for = request.headers.get("X-Forwarded-For")
1275
+ if forwarded_for is None:
1276
+ return None
1277
+ candidate = forwarded_for.split(",", 1)[0].strip().removeprefix("[").removesuffix("]")
1278
+ try:
1279
+ return str(ipaddress.ip_address(candidate))
1280
+ except ValueError:
1281
+ return None
1282
+
1283
+
1284
+ def _apply_browser_security_headers(response: Any) -> Any:
1285
+ """Apply browser security headers to an aiohttp response."""
1286
+ for name, value in _SECURITY_HEADERS.items():
1287
+ response.headers.setdefault(name, value)
1288
+ if response.content_type == "text/html":
1289
+ response.headers.setdefault("Cache-Control", "no-store")
1290
+ return response
1291
+
1292
+
1293
+ def _request_matches_public_host(request: Any, public_url: str) -> bool:
1294
+ """Return true when the HTTP Host matches the configured public URL."""
1295
+ expected = _origin_tuple(public_url)
1296
+ if expected is None:
1297
+ return False
1298
+ actual = _authority_tuple(request.headers.get("Host"), scheme=expected[0])
1299
+ return actual == expected[1:]
1300
+
1301
+
1302
+ def _request_matches_public_origin(request: Any, public_url: str, *, allow_referer: bool = True) -> bool:
1303
+ """Return true only when Origin or an allowed Referer matches the public URL."""
1304
+ expected = _origin_tuple(public_url)
1305
+ if expected is None:
1306
+ return False
1307
+ origin = request.headers.get("Origin")
1308
+ if origin:
1309
+ return _origin_tuple(origin) == expected
1310
+ if allow_referer:
1311
+ referer = request.headers.get("Referer")
1312
+ if referer:
1313
+ return _origin_tuple(referer) == expected
1314
+ return False
1315
+
1316
+
1317
+ def _authority_tuple(authority: str | None, *, scheme: str) -> tuple[str, int] | None:
1318
+ """Return normalized ``(host, port)`` for an HTTP authority."""
1319
+ if not authority or any(separator in authority for separator in "/?#"):
1320
+ return None
1321
+ try:
1322
+ parsed = urlsplit(f"{scheme}://{authority}")
1323
+ except ValueError:
1324
+ return None
1325
+ if parsed.username is not None or parsed.password is not None:
1326
+ return None
1327
+ origin = _origin_tuple(parsed.geturl())
1328
+ if origin is None:
1329
+ return None
1330
+ return origin[1], origin[2]
1331
+
1332
+
1333
+ def _origin_tuple(url: str) -> tuple[str, str, int] | None:
1334
+ """Return normalized ``(scheme, host, port)`` for *url*."""
1335
+ try:
1336
+ parsed = urlsplit(url)
1337
+ except ValueError:
1338
+ return None
1339
+ if not parsed.scheme or parsed.hostname is None:
1340
+ return None
1341
+ if parsed.username is not None or parsed.password is not None:
1342
+ return None
1343
+ host = _canonicalize_browser_host(parsed.hostname)
1344
+ if host is None:
1345
+ return None
1346
+ scheme = parsed.scheme.lower()
1347
+ if scheme not in {"http", "https"}:
1348
+ return None
1349
+ try:
1350
+ port = parsed.port
1351
+ except ValueError:
1352
+ return None
1353
+ if port is None:
1354
+ port = 80 if scheme == "http" else 443
1355
+ return scheme, host, port
1356
+
1357
+
1358
+ def _canonicalize_browser_host(host: str) -> str | None:
1359
+ """Return an ASCII/IP host matching browser URL serialization."""
1360
+ if "%" in host:
1361
+ return None
1362
+ try:
1363
+ return ipaddress.ip_address(host).compressed.lower()
1364
+ except ValueError:
1365
+ pass
1366
+
1367
+ if any(ord(character) <= 0x20 or ord(character) == 0x7F for character in host):
1368
+ return None
1369
+ if any(character in "#/:<>?@[\\]^|" for character in host):
1370
+ return None
1371
+
1372
+ if host.isascii() and not any(label.lower().startswith("xn--") for label in host.split(".")):
1373
+ ascii_host = host.lower()
1374
+ else:
1375
+ try:
1376
+ remapped_host = idna.uts46_remap(
1377
+ host,
1378
+ std3_rules=False,
1379
+ transitional=False,
1380
+ )
1381
+ except UnicodeError:
1382
+ return None
1383
+ ascii_labels: list[str] = []
1384
+ for label in remapped_host.split("."):
1385
+ if label.isascii() and not label.lower().startswith("xn--"):
1386
+ ascii_labels.append(label.lower())
1387
+ continue
1388
+ ascii_label = _browser_idna_label_to_ascii(label)
1389
+ if ascii_label is None:
1390
+ return None
1391
+ ascii_labels.append(ascii_label)
1392
+ ascii_host = ".".join(ascii_labels)
1393
+
1394
+ try:
1395
+ return ipaddress.ip_address(ascii_host).compressed.lower()
1396
+ except ValueError:
1397
+ if _ends_in_ipv4_number(ascii_host):
1398
+ return None
1399
+ return ascii_host
1400
+
1401
+
1402
+ def _browser_idna_label_to_ascii(label: str) -> str | None:
1403
+ """Encode one UTS #46 label while preserving browser-relaxed ASCII."""
1404
+ normalized_label = label.lower()
1405
+ is_ascii_label = normalized_label.startswith("xn--")
1406
+ if is_ascii_label:
1407
+ punycode_payload = normalized_label.removeprefix("xn--")
1408
+ if not punycode_payload:
1409
+ return None
1410
+ try:
1411
+ unicode_label = punycode_payload.encode("ascii").decode("punycode")
1412
+ except UnicodeError:
1413
+ return None
1414
+ else:
1415
+ unicode_label = label
1416
+
1417
+ try:
1418
+ unicode_label = idna.uts46_remap(
1419
+ unicode_label,
1420
+ std3_rules=False,
1421
+ transitional=False,
1422
+ )
1423
+ idna.check_initial_combiner(unicode_label)
1424
+ idna.check_bidi(unicode_label)
1425
+ validation_label = "".join(
1426
+ character if not character.isascii() or character.isalnum() else "a" for character in unicode_label
1427
+ )
1428
+ idna.check_label(validation_label)
1429
+ ascii_label = f"xn--{unicode_label.encode('punycode').decode('ascii')}"
1430
+ except UnicodeError:
1431
+ return None
1432
+ if is_ascii_label and ascii_label != normalized_label:
1433
+ return None
1434
+ return ascii_label
1435
+
1436
+
1437
+ def _ends_in_ipv4_number(host: str) -> bool:
1438
+ """Return true when the final domain label uses an IPv4 number syntax."""
1439
+ last_label = host.removesuffix(".").rsplit(".", 1)[-1].lower()
1440
+ if not last_label:
1441
+ return False
1442
+ if last_label.startswith("0x"):
1443
+ return all(character in "0123456789abcdef" for character in last_label[2:])
1444
+ return last_label.isascii() and last_label.isdigit()
1445
+
1446
+
1447
+ def _warn_if_public_url_missing_for_wildcard_host(*, host: str, port: int, public_url: str | None) -> None:
1448
+ """Warn when textual-serve would publish an unusable browser URL."""
1449
+ if host not in _WILDCARD_HOSTS or public_url:
1450
+ return
1451
+ _write_warning(
1452
+ f"Warning: --host {host} binds on all interfaces, but browsers cannot use {host} as the "
1453
+ f"{APP_DISPLAY_NAME} serve URL. Pass --public-url http://<reachable-host>:{port} to avoid a blank page "
1454
+ f"(ex: --public-url http://localhost:{port})."
1455
+ )
1456
+
1457
+
1458
+ def _warn_if_auth_uses_plain_http(*, host: str, port: int, public_url: str | None) -> None:
1459
+ """Warn when browser auth may send passwords over non-loopback HTTP."""
1460
+ if not _auth_transport_requires_insecure_override(host=host, port=port, public_url=public_url):
1461
+ return
1462
+ _write_warning(
1463
+ "Warning: browser auth is being served over HTTP for a non-loopback URL. Use HTTPS via a reverse "
1464
+ f"proxy before exposing {APP_DISPLAY_NAME} externally; passwords can be observed on the network."
1465
+ )
1466
+
1467
+
1468
+ def _auth_transport_requires_insecure_override(*, host: str, port: int, public_url: str | None) -> bool:
1469
+ """Return true when auth would be reachable through non-loopback plain HTTP."""
1470
+ if not _is_loopback_host(host):
1471
+ return public_url is None or not _is_https_url(public_url)
1472
+ effective_url = _effective_public_url(host=host, port=port, public_url=public_url)
1473
+ parsed = urlsplit(effective_url)
1474
+ return parsed.scheme.lower() == "http" and not _is_loopback_host(parsed.hostname)
1475
+
1476
+
1477
+ def _effective_public_url(*, host: str, port: int, public_url: str | None) -> str:
1478
+ """Return the configured public URL or Chrys' normalized default."""
1479
+ return public_url or _default_public_url(host=host, port=port)
1480
+
1481
+
1482
+ def _browser_root_path(public_url: str) -> str:
1483
+ """Return the browser-facing root path for redirects."""
1484
+ base_path = _public_base_path(public_url)
1485
+ if not base_path:
1486
+ return "/"
1487
+ return f"{base_path}/"
1488
+
1489
+
1490
+ def _browser_path(public_url: str, path: str) -> str:
1491
+ """Return *path* under the browser-facing public URL base path."""
1492
+ base_path = _public_base_path(public_url)
1493
+ normalized_path = path if path.startswith("/") else f"/{path}"
1494
+ return f"{base_path}{normalized_path}" if base_path else normalized_path
1495
+
1496
+
1497
+ def _auth_cookie_path(public_url: str) -> str:
1498
+ """Return the browser cookie path for auth sessions."""
1499
+ return _public_base_path(public_url) or "/"
1500
+
1501
+
1502
+ def _public_base_path(public_url: str) -> str:
1503
+ """Return the normalized path prefix from *public_url*."""
1504
+ origin = _origin_tuple(public_url)
1505
+ if origin is None:
1506
+ return ""
1507
+ path = urlsplit(public_url).path.strip("/")
1508
+ if not path:
1509
+ return ""
1510
+ return f"/{path}"
1511
+
1512
+
1513
+ def _default_public_url(*, host: str, port: int) -> str:
1514
+ """Match textual-serve's default public URL construction."""
1515
+ url_host = _format_url_host(host)
1516
+ if port == 80:
1517
+ return f"http://{url_host}"
1518
+ if port == 443:
1519
+ return f"https://{url_host}"
1520
+ return f"http://{url_host}:{port}"
1521
+
1522
+
1523
+ def _format_url_host(host: str) -> str:
1524
+ """Return *host* formatted for use in a URL authority; non-IP hosts are returned verbatim."""
1525
+ normalized = host.removeprefix("[").removesuffix("]")
1526
+ try:
1527
+ address = ipaddress.ip_address(normalized)
1528
+ except ValueError:
1529
+ return host
1530
+ if address.version == 6:
1531
+ return f"[{normalized}]"
1532
+ return normalized
1533
+
1534
+
1535
+ def _is_loopback_host(hostname: str | None) -> bool:
1536
+ """Return true when *hostname* identifies this local host."""
1537
+ if hostname is None:
1538
+ return False
1539
+ normalized = hostname.lower().removeprefix("[").removesuffix("]")
1540
+ if normalized == "localhost":
1541
+ return True
1542
+ try:
1543
+ return ipaddress.ip_address(normalized).is_loopback
1544
+ except ValueError:
1545
+ return False
1546
+
1547
+
1548
+ def _is_https_url(url: str) -> bool:
1549
+ """Return true when *url* uses HTTPS."""
1550
+ origin = _origin_tuple(url)
1551
+ return origin is not None and origin[0] == "https"
1552
+
1553
+
1554
+ def _write_warning(message: str) -> None:
1555
+ """Write a terminal-styled warning without requiring ANSI in captured output."""
1556
+ _write_styled_stderr(message, style=_WARNING_STYLE)
1557
+
1558
+
1559
+ def _write_error(message: str) -> None:
1560
+ """Write a terminal-styled error without requiring ANSI in captured output."""
1561
+ _write_styled_stderr(message, style=_ERROR_STYLE)
1562
+
1563
+
1564
+ def _write_styled_stderr(message: str, *, style: str) -> None:
1565
+ """Write a styled stderr message."""
1566
+ _make_warning_console().print(message, style=style, markup=False, soft_wrap=True)
1567
+
1568
+
1569
+ def _make_warning_console() -> Console:
1570
+ """Create the Rich console used for terminal warnings."""
1571
+ return Console(file=sys.stderr, highlight=False)
1572
+
1573
+
1574
+ def main(argv: list[str] | None = None) -> int:
1575
+ """Entry point for ``chrys serve``."""
1576
+ from chrys.orchestration.startup import set_process_title
1577
+
1578
+ set_process_title()
1579
+
1580
+ parser = build_parser()
1581
+ args = parser.parse_args(argv)
1582
+ try:
1583
+ run_command(args)
1584
+ except RuntimeError as exc:
1585
+ _write_error(f"Error: {exc}")
1586
+ parser.exit(1)
1587
+ except KeyboardInterrupt:
1588
+ return 130
1589
+ return 0
1590
+
1591
+
1592
+ if __name__ == "__main__":
1593
+ raise SystemExit(main())