octo-agent 0.11.2

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 (319) hide show
  1. checksums.yaml +7 -0
  2. data/.clacky/skills/commit/SKILL.md +423 -0
  3. data/.clacky/skills/gem-release/SKILL.md +199 -0
  4. data/.clacky/skills/gem-release/scripts/release.sh +304 -0
  5. data/.clacky/skills/oss-upload/SKILL.md +47 -0
  6. data/.octorules +106 -0
  7. data/.rspec +3 -0
  8. data/.rubocop.yml +8 -0
  9. data/CHANGELOG.md +76 -0
  10. data/CODE_OF_CONDUCT.md +132 -0
  11. data/CONTRIBUTING.md +92 -0
  12. data/Dockerfile +28 -0
  13. data/LICENSE.txt +22 -0
  14. data/POSITIONING.md +46 -0
  15. data/README.md +134 -0
  16. data/README_CN.md +134 -0
  17. data/Rakefile +34 -0
  18. data/benchmark/fixtures/sample_project/Gemfile +3 -0
  19. data/benchmark/fixtures/sample_project/lib/api_handler.rb +32 -0
  20. data/benchmark/fixtures/sample_project/lib/order_calculator.rb +23 -0
  21. data/benchmark/fixtures/sample_project/lib/user_renderer.rb +20 -0
  22. data/benchmark/fixtures/sample_project/spec/order_calculator_spec.rb +20 -0
  23. data/benchmark/results/EVALUATION_REPORT.md +165 -0
  24. data/benchmark/results/baseline_20260511_174424.json +128 -0
  25. data/benchmark/results/report_20260511_175256.json +271 -0
  26. data/benchmark/results/report_20260511_175444.json +271 -0
  27. data/benchmark/results/treatment_20260511_175103.json +130 -0
  28. data/benchmark/runner.rb +441 -0
  29. data/bin/octo +7 -0
  30. data/docs/agent-first-ui-design.md +77 -0
  31. data/docs/billing-system.md +318 -0
  32. data/docs/channel-architecture.md +235 -0
  33. data/docs/engineering-article.md +343 -0
  34. data/docs/session-skill-invocation.md +69 -0
  35. data/docs/time_machine_design.md +247 -0
  36. data/docs/ui2-architecture.md +124 -0
  37. data/homebrew/README.md +96 -0
  38. data/homebrew/openocto.rb +24 -0
  39. data/lib/octo/agent/hook_manager.rb +61 -0
  40. data/lib/octo/agent/llm_caller.rb +800 -0
  41. data/lib/octo/agent/memory_updater.rb +246 -0
  42. data/lib/octo/agent/message_compressor.rb +225 -0
  43. data/lib/octo/agent/message_compressor_helper.rb +869 -0
  44. data/lib/octo/agent/next_message_suggester.rb +215 -0
  45. data/lib/octo/agent/session_serializer.rb +685 -0
  46. data/lib/octo/agent/skill_auto_creator.rb +114 -0
  47. data/lib/octo/agent/skill_evolution.rb +61 -0
  48. data/lib/octo/agent/skill_manager.rb +466 -0
  49. data/lib/octo/agent/skill_reflector.rb +89 -0
  50. data/lib/octo/agent/system_prompt_builder.rb +101 -0
  51. data/lib/octo/agent/time_machine.rb +214 -0
  52. data/lib/octo/agent/tool_executor.rb +454 -0
  53. data/lib/octo/agent/tool_registry.rb +150 -0
  54. data/lib/octo/agent.rb +2180 -0
  55. data/lib/octo/agent_config.rb +989 -0
  56. data/lib/octo/agent_profile.rb +112 -0
  57. data/lib/octo/anthropic_stream_aggregator.rb +137 -0
  58. data/lib/octo/background_task_registry.rb +324 -0
  59. data/lib/octo/banner.rb +34 -0
  60. data/lib/octo/bedrock_stream_aggregator.rb +137 -0
  61. data/lib/octo/block_font.rb +331 -0
  62. data/lib/octo/cli.rb +968 -0
  63. data/lib/octo/client.rb +623 -0
  64. data/lib/octo/default_agents/SOUL.md +3 -0
  65. data/lib/octo/default_agents/USER.md +1 -0
  66. data/lib/octo/default_agents/base_prompt.md +66 -0
  67. data/lib/octo/default_agents/coding/profile.yml +2 -0
  68. data/lib/octo/default_agents/coding/system_prompt.md +67 -0
  69. data/lib/octo/default_agents/general/profile.yml +2 -0
  70. data/lib/octo/default_agents/general/system_prompt.md +16 -0
  71. data/lib/octo/default_parsers/doc_parser.rb +69 -0
  72. data/lib/octo/default_parsers/docx_parser.rb +188 -0
  73. data/lib/octo/default_parsers/pdf_parser.rb +120 -0
  74. data/lib/octo/default_parsers/pdf_parser_ocr.py +103 -0
  75. data/lib/octo/default_parsers/pdf_parser_plumber.py +62 -0
  76. data/lib/octo/default_parsers/pptx_parser.rb +140 -0
  77. data/lib/octo/default_parsers/xlsx_parser.rb +121 -0
  78. data/lib/octo/default_skills/browser-setup/SKILL.md +426 -0
  79. data/lib/octo/default_skills/channel-manager/SKILL.md +623 -0
  80. data/lib/octo/default_skills/channel-manager/dingtalk_setup.rb +191 -0
  81. data/lib/octo/default_skills/channel-manager/discord_setup.rb +199 -0
  82. data/lib/octo/default_skills/channel-manager/feishu_setup.rb +574 -0
  83. data/lib/octo/default_skills/channel-manager/import_lark_skills.rb +97 -0
  84. data/lib/octo/default_skills/channel-manager/install_feishu_skills.rb +105 -0
  85. data/lib/octo/default_skills/channel-manager/weixin_setup.rb +274 -0
  86. data/lib/octo/default_skills/code-explorer/SKILL.md +36 -0
  87. data/lib/octo/default_skills/cron-task-creator/SKILL.md +257 -0
  88. data/lib/octo/default_skills/cron-task-creator/evals/evals.json +38 -0
  89. data/lib/octo/default_skills/onboard/SKILL.md +578 -0
  90. data/lib/octo/default_skills/onboard/scripts/import_external_skills.rb +413 -0
  91. data/lib/octo/default_skills/onboard/scripts/install_builtin_skills.rb +97 -0
  92. data/lib/octo/default_skills/persist-memory/SKILL.md +59 -0
  93. data/lib/octo/default_skills/personal-website/SKILL.md +113 -0
  94. data/lib/octo/default_skills/personal-website/publish.rb +235 -0
  95. data/lib/octo/default_skills/product-help/SKILL.md +123 -0
  96. data/lib/octo/default_skills/product-help/docs/agent-config.md +74 -0
  97. data/lib/octo/default_skills/product-help/docs/best-practices.md +49 -0
  98. data/lib/octo/default_skills/product-help/docs/browser-tool.md +53 -0
  99. data/lib/octo/default_skills/product-help/docs/built-in-skills.md +43 -0
  100. data/lib/octo/default_skills/product-help/docs/cli-reference.md +82 -0
  101. data/lib/octo/default_skills/product-help/docs/create-your-first-skill.md +47 -0
  102. data/lib/octo/default_skills/product-help/docs/faq.md +98 -0
  103. data/lib/octo/default_skills/product-help/docs/how-to-use-a-skill.md +58 -0
  104. data/lib/octo/default_skills/product-help/docs/installation.md +59 -0
  105. data/lib/octo/default_skills/product-help/docs/memory-system.md +61 -0
  106. data/lib/octo/default_skills/product-help/docs/octorules.md +62 -0
  107. data/lib/octo/default_skills/product-help/docs/session-management.md +63 -0
  108. data/lib/octo/default_skills/product-help/docs/skill-basics.md +55 -0
  109. data/lib/octo/default_skills/product-help/docs/skill-frontmatter.md +61 -0
  110. data/lib/octo/default_skills/product-help/docs/web-server.md +49 -0
  111. data/lib/octo/default_skills/product-help/docs/what-is-octo.md +37 -0
  112. data/lib/octo/default_skills/product-help/docs/windows-installation.md +36 -0
  113. data/lib/octo/default_skills/product-help/docs/writing-tips.md +53 -0
  114. data/lib/octo/default_skills/recall-memory/SKILL.md +65 -0
  115. data/lib/octo/default_skills/skill-add/SKILL.md +59 -0
  116. data/lib/octo/default_skills/skill-add/scripts/install_from_zip.rb +295 -0
  117. data/lib/octo/default_skills/skill-creator/SKILL.md +602 -0
  118. data/lib/octo/default_skills/skill-creator/agents/analyzer.md +274 -0
  119. data/lib/octo/default_skills/skill-creator/agents/comparator.md +202 -0
  120. data/lib/octo/default_skills/skill-creator/agents/grader.md +223 -0
  121. data/lib/octo/default_skills/skill-creator/eval-viewer/generate_review.py +471 -0
  122. data/lib/octo/default_skills/skill-creator/eval-viewer/viewer.html +1325 -0
  123. data/lib/octo/default_skills/skill-creator/references/schemas.md +430 -0
  124. data/lib/octo/default_skills/skill-creator/scripts/__init__.py +0 -0
  125. data/lib/octo/default_skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
  126. data/lib/octo/default_skills/skill-creator/scripts/generate_report.py +326 -0
  127. data/lib/octo/default_skills/skill-creator/scripts/improve_description.py +310 -0
  128. data/lib/octo/default_skills/skill-creator/scripts/quick_validate.py +103 -0
  129. data/lib/octo/default_skills/skill-creator/scripts/run_eval.py +317 -0
  130. data/lib/octo/default_skills/skill-creator/scripts/run_loop.py +331 -0
  131. data/lib/octo/default_skills/skill-creator/scripts/utils.py +47 -0
  132. data/lib/octo/default_skills/skill-creator/scripts/validate_skill_frontmatter.rb +143 -0
  133. data/lib/octo/idle_compression_timer.rb +115 -0
  134. data/lib/octo/json_ui_controller.rb +204 -0
  135. data/lib/octo/message_format/anthropic.rb +409 -0
  136. data/lib/octo/message_format/bedrock.rb +361 -0
  137. data/lib/octo/message_format/open_ai.rb +222 -0
  138. data/lib/octo/message_history.rb +373 -0
  139. data/lib/octo/openai_stream_aggregator.rb +130 -0
  140. data/lib/octo/plain_ui_controller.rb +166 -0
  141. data/lib/octo/providers.rb +534 -0
  142. data/lib/octo/server/browser_manager.rb +397 -0
  143. data/lib/octo/server/channel/adapters/base.rb +82 -0
  144. data/lib/octo/server/channel/adapters/dingtalk/adapter.rb +314 -0
  145. data/lib/octo/server/channel/adapters/dingtalk/api_client.rb +391 -0
  146. data/lib/octo/server/channel/adapters/dingtalk/stream_client.rb +203 -0
  147. data/lib/octo/server/channel/adapters/discord/adapter.rb +229 -0
  148. data/lib/octo/server/channel/adapters/discord/api_client.rb +107 -0
  149. data/lib/octo/server/channel/adapters/discord/gateway_client.rb +270 -0
  150. data/lib/octo/server/channel/adapters/feishu/adapter.rb +320 -0
  151. data/lib/octo/server/channel/adapters/feishu/bot.rb +478 -0
  152. data/lib/octo/server/channel/adapters/feishu/file_processor.rb +36 -0
  153. data/lib/octo/server/channel/adapters/feishu/message_parser.rb +129 -0
  154. data/lib/octo/server/channel/adapters/feishu/ws_client.rb +423 -0
  155. data/lib/octo/server/channel/adapters/telegram/adapter.rb +375 -0
  156. data/lib/octo/server/channel/adapters/telegram/api_client.rb +205 -0
  157. data/lib/octo/server/channel/adapters/wecom/adapter.rb +148 -0
  158. data/lib/octo/server/channel/adapters/wecom/media_downloader.rb +115 -0
  159. data/lib/octo/server/channel/adapters/wecom/ws_client.rb +395 -0
  160. data/lib/octo/server/channel/adapters/weixin/adapter.rb +692 -0
  161. data/lib/octo/server/channel/adapters/weixin/api_client.rb +402 -0
  162. data/lib/octo/server/channel/channel_config.rb +178 -0
  163. data/lib/octo/server/channel/channel_manager.rb +468 -0
  164. data/lib/octo/server/channel/channel_ui_controller.rb +224 -0
  165. data/lib/octo/server/channel.rb +33 -0
  166. data/lib/octo/server/discover.rb +77 -0
  167. data/lib/octo/server/epipe_safe_io.rb +105 -0
  168. data/lib/octo/server/http_server.rb +3554 -0
  169. data/lib/octo/server/scheduler.rb +317 -0
  170. data/lib/octo/server/server_master.rb +325 -0
  171. data/lib/octo/server/session_registry.rb +431 -0
  172. data/lib/octo/server/web_ui_controller.rb +487 -0
  173. data/lib/octo/session_manager.rb +385 -0
  174. data/lib/octo/skill.rb +466 -0
  175. data/lib/octo/skill_loader.rb +328 -0
  176. data/lib/octo/tools/base.rb +118 -0
  177. data/lib/octo/tools/browser.rb +625 -0
  178. data/lib/octo/tools/edit.rb +165 -0
  179. data/lib/octo/tools/file_reader.rb +549 -0
  180. data/lib/octo/tools/glob.rb +162 -0
  181. data/lib/octo/tools/grep.rb +356 -0
  182. data/lib/octo/tools/invoke_skill.rb +96 -0
  183. data/lib/octo/tools/list_tasks.rb +54 -0
  184. data/lib/octo/tools/redo_task.rb +41 -0
  185. data/lib/octo/tools/request_user_feedback.rb +84 -0
  186. data/lib/octo/tools/security.rb +333 -0
  187. data/lib/octo/tools/terminal/output_cleaner.rb +63 -0
  188. data/lib/octo/tools/terminal/persistent_session.rb +268 -0
  189. data/lib/octo/tools/terminal/safe_rm.sh +106 -0
  190. data/lib/octo/tools/terminal/session_manager.rb +213 -0
  191. data/lib/octo/tools/terminal.rb +1828 -0
  192. data/lib/octo/tools/todo_manager.rb +374 -0
  193. data/lib/octo/tools/trash_manager.rb +388 -0
  194. data/lib/octo/tools/undo_task.rb +35 -0
  195. data/lib/octo/tools/web_fetch.rb +242 -0
  196. data/lib/octo/tools/web_search.rb +260 -0
  197. data/lib/octo/tools/write.rb +77 -0
  198. data/lib/octo/ui2/block_font.rb +10 -0
  199. data/lib/octo/ui2/components/base_component.rb +163 -0
  200. data/lib/octo/ui2/components/command_suggestions.rb +290 -0
  201. data/lib/octo/ui2/components/common_component.rb +96 -0
  202. data/lib/octo/ui2/components/inline_input.rb +226 -0
  203. data/lib/octo/ui2/components/input_area.rb +1338 -0
  204. data/lib/octo/ui2/components/message_component.rb +99 -0
  205. data/lib/octo/ui2/components/modal_component.rb +419 -0
  206. data/lib/octo/ui2/components/todo_area.rb +149 -0
  207. data/lib/octo/ui2/components/tool_component.rb +107 -0
  208. data/lib/octo/ui2/components/welcome_banner.rb +139 -0
  209. data/lib/octo/ui2/layout_manager.rb +807 -0
  210. data/lib/octo/ui2/line_editor.rb +363 -0
  211. data/lib/octo/ui2/markdown_renderer.rb +100 -0
  212. data/lib/octo/ui2/output_buffer.rb +370 -0
  213. data/lib/octo/ui2/progress_handle.rb +362 -0
  214. data/lib/octo/ui2/progress_indicator.rb +55 -0
  215. data/lib/octo/ui2/screen_buffer.rb +273 -0
  216. data/lib/octo/ui2/terminal_detector.rb +119 -0
  217. data/lib/octo/ui2/theme_manager.rb +85 -0
  218. data/lib/octo/ui2/themes/base_theme.rb +105 -0
  219. data/lib/octo/ui2/themes/hacker_theme.rb +62 -0
  220. data/lib/octo/ui2/themes/minimal_theme.rb +56 -0
  221. data/lib/octo/ui2/thinking_verbs.rb +26 -0
  222. data/lib/octo/ui2/ui_controller.rb +1625 -0
  223. data/lib/octo/ui2/view_renderer.rb +177 -0
  224. data/lib/octo/ui2.rb +40 -0
  225. data/lib/octo/ui_interface.rb +154 -0
  226. data/lib/octo/utils/arguments_parser.rb +191 -0
  227. data/lib/octo/utils/browser_detector.rb +195 -0
  228. data/lib/octo/utils/encoding.rb +92 -0
  229. data/lib/octo/utils/environment_detector.rb +140 -0
  230. data/lib/octo/utils/file_ignore_helper.rb +170 -0
  231. data/lib/octo/utils/file_processor.rb +601 -0
  232. data/lib/octo/utils/gitignore_parser.rb +154 -0
  233. data/lib/octo/utils/limit_stack.rb +152 -0
  234. data/lib/octo/utils/logger.rb +124 -0
  235. data/lib/octo/utils/login_shell.rb +72 -0
  236. data/lib/octo/utils/model_pricing.rb +646 -0
  237. data/lib/octo/utils/parser_manager.rb +165 -0
  238. data/lib/octo/utils/path_helper.rb +15 -0
  239. data/lib/octo/utils/scripts_manager.rb +59 -0
  240. data/lib/octo/utils/string_matcher.rb +158 -0
  241. data/lib/octo/utils/trash_directory.rb +112 -0
  242. data/lib/octo/utils/workspace_rules.rb +46 -0
  243. data/lib/octo/version.rb +5 -0
  244. data/lib/octo/web/app.css +7141 -0
  245. data/lib/octo/web/app.js +543 -0
  246. data/lib/octo/web/apple-touch-icon.png +0 -0
  247. data/lib/octo/web/auth.js +150 -0
  248. data/lib/octo/web/channels.js +276 -0
  249. data/lib/octo/web/datepicker.js +205 -0
  250. data/lib/octo/web/favicon.png +0 -0
  251. data/lib/octo/web/i18n.js +1073 -0
  252. data/lib/octo/web/icon-512.png +0 -0
  253. data/lib/octo/web/icon-dark.svg +25 -0
  254. data/lib/octo/web/icon.svg +29 -0
  255. data/lib/octo/web/index.html +871 -0
  256. data/lib/octo/web/marked.min.js +69 -0
  257. data/lib/octo/web/onboard.js +491 -0
  258. data/lib/octo/web/profile.js +442 -0
  259. data/lib/octo/web/sessions.js +4421 -0
  260. data/lib/octo/web/settings.js +913 -0
  261. data/lib/octo/web/sidebar.js +32 -0
  262. data/lib/octo/web/skills.js +885 -0
  263. data/lib/octo/web/tasks.js +297 -0
  264. data/lib/octo/web/theme.js +105 -0
  265. data/lib/octo/web/trash.js +343 -0
  266. data/lib/octo/web/vendor/hljs/highlight.min.js +1244 -0
  267. data/lib/octo/web/vendor/hljs/hljs-theme.css +95 -0
  268. data/lib/octo/web/vendor/katex/auto-render.min.js +1 -0
  269. data/lib/octo/web/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  270. data/lib/octo/web/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  271. data/lib/octo/web/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  272. data/lib/octo/web/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  273. data/lib/octo/web/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  274. data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  275. data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  276. data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  277. data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  278. data/lib/octo/web/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  279. data/lib/octo/web/vendor/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  280. data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  281. data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  282. data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  283. data/lib/octo/web/vendor/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  284. data/lib/octo/web/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  285. data/lib/octo/web/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  286. data/lib/octo/web/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  287. data/lib/octo/web/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  288. data/lib/octo/web/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  289. data/lib/octo/web/vendor/katex/katex.min.css +1 -0
  290. data/lib/octo/web/vendor/katex/katex.min.js +1 -0
  291. data/lib/octo/web/version.js +449 -0
  292. data/lib/octo/web/weixin-qr.html +209 -0
  293. data/lib/octo/web/ws-dispatcher.js +357 -0
  294. data/lib/octo/web/ws.js +128 -0
  295. data/lib/octo.rb +145 -0
  296. data/scripts/build/build.sh +329 -0
  297. data/scripts/build/lib/apt.sh +56 -0
  298. data/scripts/build/lib/brew.sh +89 -0
  299. data/scripts/build/lib/colors.sh +17 -0
  300. data/scripts/build/lib/gem.sh +95 -0
  301. data/scripts/build/lib/mise.sh +125 -0
  302. data/scripts/build/lib/network.sh +157 -0
  303. data/scripts/build/lib/os.sh +57 -0
  304. data/scripts/build/lib/shell.sh +37 -0
  305. data/scripts/build/src/install.sh.cc +174 -0
  306. data/scripts/build/src/install_browser.sh.cc +101 -0
  307. data/scripts/build/src/install_full.sh.cc +290 -0
  308. data/scripts/build/src/install_rails_deps.sh.cc +145 -0
  309. data/scripts/build/src/install_system_deps.sh.cc +123 -0
  310. data/scripts/build/src/uninstall.sh.cc +101 -0
  311. data/scripts/install.ps1 +532 -0
  312. data/scripts/install.sh +567 -0
  313. data/scripts/install_browser.sh +479 -0
  314. data/scripts/install_full.sh +838 -0
  315. data/scripts/install_rails_deps.sh +746 -0
  316. data/scripts/install_system_deps.sh +518 -0
  317. data/scripts/uninstall.sh +287 -0
  318. data/sig/octo.rbs +4 -0
  319. metadata +614 -0
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Octo
4
+ module Tools
5
+ # Tool for redoing a task after undo (Time Machine feature)
6
+ class RedoTask < Base
7
+ self.tool_name = "redo_task"
8
+ self.tool_description = "Redo to a specific task after undo. Restores files to that task's state. " \
9
+ "Use when user wants to go forward to a future task or switch to a different branch."
10
+ self.tool_category = "time_machine"
11
+ self.tool_parameters = {
12
+ type: "object",
13
+ properties: {
14
+ task_id: {
15
+ type: "integer",
16
+ description: "The task ID to redo to (must be greater than current active task)"
17
+ }
18
+ },
19
+ required: ["task_id"]
20
+ }
21
+
22
+ def execute(agent:, task_id:, **_args)
23
+ result = agent.switch_to_task(task_id)
24
+
25
+ if result[:success]
26
+ result[:message]
27
+ else
28
+ "Error: #{result[:message]}"
29
+ end
30
+ end
31
+
32
+ def format_call(task_id:, **_args)
33
+ "Redoing to task #{task_id}..."
34
+ end
35
+
36
+ def format_result(result)
37
+ result
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Octo
4
+ module Tools
5
+ class RequestUserFeedback < Base
6
+ self.tool_name = "request_user_feedback"
7
+ self.tool_description = <<~DESC
8
+ Request feedback or clarification from the user when you need more information to complete a task.
9
+ Use this tool when:
10
+ - You need clarification on ambiguous requirements
11
+ - You need the user to choose between multiple options
12
+ - You need additional information that you cannot infer
13
+ - You want to confirm your understanding before proceeding
14
+
15
+ After calling this tool, STOP and wait for the user's response.
16
+ Do NOT continue with other actions until you receive user feedback.
17
+ DESC
18
+
19
+ self.tool_parameters = {
20
+ type: "object",
21
+ properties: {
22
+ question: {
23
+ type: "string",
24
+ description: "The question or clarification request to ask the user"
25
+ },
26
+ context: {
27
+ type: "string",
28
+ description: "Optional context explaining why you need this information (helps user understand)"
29
+ },
30
+ options: {
31
+ type: "array",
32
+ items: { type: "string" },
33
+ description: "Optional array of choices/options if asking user to select from predefined options"
34
+ }
35
+ },
36
+ required: ["question"]
37
+ }
38
+
39
+ self.tool_category = "interaction"
40
+
41
+ def execute(question:, context: nil, options: nil, working_dir: nil)
42
+ # Build the feedback request message
43
+ message_parts = []
44
+
45
+ if context && !context.strip.empty?
46
+ message_parts << "**Context:** #{context.strip}"
47
+ message_parts << ""
48
+ end
49
+
50
+ message_parts << "**Question:** #{question.strip}"
51
+
52
+ if options && !options.empty?
53
+ message_parts << ""
54
+ message_parts << "**Options:**"
55
+ options.each_with_index do |option, index|
56
+ message_parts << " #{index + 1}. #{option}"
57
+ end
58
+ end
59
+
60
+ formatted_message = message_parts.join("\n")
61
+
62
+ {
63
+ success: true,
64
+ message: formatted_message,
65
+ awaiting_feedback: true # Special flag to indicate we're waiting for user
66
+ }
67
+ end
68
+
69
+ def format_call(args)
70
+ question = args[:question] || args["question"]
71
+ preview = question.length > 60 ? "#{question[0..60]}..." : question
72
+ "request_user_feedback(\"#{preview}\")"
73
+ end
74
+
75
+ def format_result(result)
76
+ if result.is_a?(Hash) && result[:message]
77
+ result[:message]
78
+ else
79
+ "Waiting for user feedback..."
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,333 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "shellwords"
4
+ require "json"
5
+ require "fileutils"
6
+ require_relative "../utils/trash_directory"
7
+ require_relative "../utils/encoding"
8
+
9
+ module Octo
10
+ module Tools
11
+ # Pre-execution safety layer for shell-style commands.
12
+ #
13
+ # Design principle: protect against the handful of commands that are
14
+ # irreversibly destructive or can compromise the host. Everything else
15
+ # is the user's (or agent's) business. Over-protection burns tool-call
16
+ # rounds and forces awkward work-arounds (e.g. the infamous "cp ~/.octo
17
+ # /xxx ./ ... Blocked: outside project directory" dance).
18
+ #
19
+ # Responsibilities (applied to the `command` string BEFORE it is handed
20
+ # to a shell / PTY for execution):
21
+ #
22
+ # 1. Block hard-dangerous commands: sudo, pkill octo, eval, exec,
23
+ # `...`, $(...), | sh, | bash,
24
+ # redirect to /etc /usr /bin.
25
+ # 2. Rewrite `curl ... | bash` → save script to a file for manual
26
+ # review instead of exec.
27
+ # 3. Protect credential/secret files: .env, .ssh/, .aws/ — block
28
+ # writes to these only. Other
29
+ # "project" files (Gemfile,
30
+ # README.md, package.json, …)
31
+ # are NOT protected — editing
32
+ # them is a normal dev task.
33
+ #
34
+ # Note on `rm`:
35
+ # `rm` is NOT rewritten here — it's intercepted at runtime by a shell
36
+ # function installed in each PTY session (see Terminal::SAFE_RM_BASH
37
+ # and Terminal#install_marker). This lets the shell's own parser
38
+ # handle heredocs / multi-line / globs / variables correctly. A
39
+ # static Ruby-side rewrite cannot — it would mis-parse heredoc
40
+ # bodies and destroy legitimate commands.
41
+ #
42
+ # Notes:
43
+ # - `cp`, `mv`, `mkdir`, `touch`, `echo` are allowed to touch ANY path
44
+ # (including outside the project root). The source of a `cp` is
45
+ # read-only to the FS, and writing to arbitrary dirs is a legitimate
46
+ # need (copying from ~/.octo/skills/..., writing to /tmp, etc.).
47
+ #
48
+ # Raises SecurityError on block. Returns a (possibly rewritten) command
49
+ # string on success.
50
+ #
51
+ # This module was extracted from the former `SafeShell` tool. It is now
52
+ # shared by any tool that executes shell-style commands (currently:
53
+ # `terminal`).
54
+ module Security
55
+ # Raised when a command cannot be made safe.
56
+ class Blocked < StandardError; end
57
+
58
+ # Read-only commands that are considered safe for auto-execution
59
+ # (permission mode :confirm_safes).
60
+ SAFE_READONLY_COMMANDS = %w[
61
+ ls pwd cat less more head tail
62
+ grep find which whereis whoami
63
+ ps top htop df du
64
+ git echo printf wc
65
+ date file stat
66
+ env printenv
67
+ curl wget
68
+ ].freeze
69
+
70
+ class << self
71
+ # Process `command` and return a (possibly rewritten) safe version.
72
+ # Raises SecurityError when the command cannot be made safe.
73
+ #
74
+ # @param command [String] command to check
75
+ # @param project_root [String] path treated as the allowed root for writes
76
+ # @return [String] safe command to execute
77
+ def make_safe(command, project_root: Dir.pwd)
78
+ Replacer.new(project_root).make_command_safe(command)
79
+ end
80
+
81
+ # True iff the command is safe to auto-execute in :confirm_safes mode.
82
+ # (Either a known read-only command, or one that Security.make_safe
83
+ # returns unchanged.)
84
+ def command_safe_for_auto_execution?(command)
85
+ return false unless command
86
+
87
+ cmd_name = command.strip.split.first
88
+ return true if SAFE_READONLY_COMMANDS.include?(cmd_name)
89
+
90
+ begin
91
+ safe = make_safe(command, project_root: Dir.pwd)
92
+ command.strip == safe.strip
93
+ rescue SecurityError
94
+ false
95
+ end
96
+ end
97
+ end
98
+
99
+ # Internal class that owns per-project state (trash dir, log dir, ...).
100
+ # Extracted almost verbatim from the old SafeShell::CommandSafetyReplacer.
101
+ class Replacer
102
+ def initialize(project_root)
103
+ @project_root = File.expand_path(project_root)
104
+
105
+ trash_directory = Octo::TrashDirectory.new(@project_root)
106
+ @backup_dir = trash_directory.backup_dir
107
+
108
+ @project_hash = trash_directory.generate_project_hash(@project_root)
109
+ @safety_log_dir = File.join(Dir.home, ".octo", "safety_logs", @project_hash)
110
+ FileUtils.mkdir_p(@safety_log_dir) unless Dir.exist?(@safety_log_dir)
111
+ @safety_log_file = File.join(@safety_log_dir, "safety.log")
112
+ end
113
+
114
+ def make_command_safe(command)
115
+ command = command.strip
116
+
117
+ # Use a UTF-8-scrubbed copy ONLY for regex checks. The original
118
+ # bytes are returned unchanged so the shell receives exact paths
119
+ # (e.g. GBK-encoded Chinese filenames in zip archives).
120
+ @safe_check_command = Octo::Utils::Encoding.safe_check(command)
121
+
122
+ case @safe_check_command
123
+ # Block attempts to terminate the octo server process.
124
+ # IMPORTANT: each verb is anchored with \b so substrings like
125
+ # "Skill" (contains "kill") or "Bill Killalina" don't trigger
126
+ # false positives. We also require `octo` to appear as a whole
127
+ # word AND within a reasonable distance (same logical command,
128
+ # not hundreds of chars later in an unrelated echo string).
129
+ when /\bpkill\b[^\n;|&]{0,80}\bocto\b|\bkillall\b[^\n;|&]{0,80}\bocto\b|\bkill\s+(?:-\S+\s+)*[^\n;|&]{0,40}\bocto\b/i
130
+ raise SecurityError, "Killing the octo server process is not allowed. To restart, use: #{restart_hint}"
131
+ when /\bocto\s+server\b/
132
+ raise SecurityError, "Managing the octo server from within a session is not allowed. To restart, use: #{restart_hint}"
133
+ when /^chmod\s+x/
134
+ replace_chmod_command(command)
135
+ when /^curl.*\|\s*(sh|bash)/
136
+ replace_curl_pipe_command(command)
137
+ when /^sudo\s+/
138
+ block_sudo_command(command)
139
+ when />\s*\/dev\/null\s*$/
140
+ allow_dev_null_redirect(command)
141
+ when /^(mv|cp|mkdir|touch|echo)\s+/
142
+ validate_and_allow(command)
143
+ else
144
+ validate_general_command(@safe_check_command)
145
+ command
146
+ end
147
+ end
148
+
149
+ def replace_chmod_command(command)
150
+ begin
151
+ parts = Shellwords.split(command)
152
+ rescue ArgumentError
153
+ parts = command.split(/\s+/)
154
+ end
155
+
156
+ files = parts[2..-1] || []
157
+ files.each { |file| validate_file_path(file) unless file.start_with?('-') }
158
+
159
+ log_replacement("chmod", command, "chmod +x is allowed - file permissions will be modified")
160
+ command
161
+ end
162
+
163
+ def replace_curl_pipe_command(command)
164
+ if command.match(/curl\s+(.*?)\s*\|\s*(sh|bash)/)
165
+ url = $1
166
+ shell_type = $2
167
+ timestamp = Time.now.strftime("%Y%m%d_%H%M%S")
168
+ safe_file = File.join(@backup_dir, "downloaded_script_#{timestamp}.sh")
169
+
170
+ result = "curl #{url} -o #{Shellwords.escape(safe_file)} && echo '🔒 Script downloaded to #{safe_file} for manual review. Run: cat #{safe_file}'"
171
+ log_replacement("curl | #{shell_type}", result, "Script saved for manual review instead of automatic execution")
172
+ result
173
+ else
174
+ command
175
+ end
176
+ end
177
+
178
+ def block_sudo_command(_command)
179
+ raise SecurityError, "sudo commands are not allowed for security reasons"
180
+ end
181
+
182
+ def allow_dev_null_redirect(command)
183
+ command
184
+ end
185
+
186
+ # Build a copy-pasteable "how to restart octo server" hint.
187
+ # When running inside a octo server worker, `OCTO_MASTER_PID` is
188
+ # injected by ServerMaster (see server_master.rb). We keep the
189
+ # variable name in the hint (so the AI / user learns the standard
190
+ # convention) AND append the resolved PID in parentheses so it's
191
+ # immediately actionable. When the variable isn't set (e.g. one-shot
192
+ # CLI invocation), we just show the variable name.
193
+ def restart_hint
194
+ pid = ENV["OCTO_MASTER_PID"].to_s
195
+ if pid =~ /\A\d+\z/
196
+ "kill -USR1 $OCTO_MASTER_PID (current master PID: #{pid})"
197
+ else
198
+ "kill -USR1 $OCTO_MASTER_PID"
199
+ end
200
+ end
201
+
202
+ # Relaxed validator for mv / cp / mkdir / touch / echo.
203
+ #
204
+ # Historical behavior was to forbid any path outside @project_root,
205
+ # which broke legitimate workflows like copying skill templates from
206
+ # ~/.octo/skills/... into the project. We now only block writes to
207
+ # true credential directories (.ssh, .aws) and .env files. Everything
208
+ # else is allowed.
209
+ def validate_and_allow(command)
210
+ begin
211
+ parts = Shellwords.split(command)
212
+ rescue ArgumentError
213
+ parts = command.split(/\s+/)
214
+ end
215
+
216
+ cmd = parts.first
217
+ args = parts[1..-1] || []
218
+
219
+ case cmd
220
+ when 'mv', 'cp'
221
+ # For mv/cp only the DESTINATION (last non-flag arg) is a write
222
+ # target; earlier args are sources and are read-only to the FS.
223
+ write_targets = args.reject { |a| a.start_with?('-') }
224
+ dest = write_targets.last
225
+ validate_secret_write(dest) if dest
226
+ when 'mkdir', 'touch'
227
+ args.each { |path| validate_secret_write(path) unless path.start_with?('-') }
228
+ when 'echo'
229
+ # `echo foo > path` — best-effort: block only if redirecting to a
230
+ # secret path. The redirect target will also be caught by
231
+ # validate_general_command for /etc /usr /bin; here we add .env,
232
+ # .ssh/, .aws/.
233
+ if command =~ />\s*([^\s|&;]+)/
234
+ validate_secret_write(Regexp.last_match(1))
235
+ end
236
+ end
237
+
238
+ command
239
+ end
240
+
241
+ def validate_general_command(command)
242
+ cmd_without_quotes = command.gsub(/'[^']*'|"[^"]*"/, '')
243
+
244
+ dangerous_patterns = [
245
+ /eval\s*\(/,
246
+ /exec\s*\(/,
247
+ /system\s*\(/,
248
+ /`[^`]+`/,
249
+ /\$\([^)]+\)/,
250
+ /\|\s*sh\s*$/,
251
+ /\|\s*bash\s*$/,
252
+ />\s*\/etc\//,
253
+ />\s*\/usr\//,
254
+ />\s*\/bin\//
255
+ ]
256
+
257
+ dangerous_patterns.each do |pattern|
258
+ if cmd_without_quotes.match?(pattern)
259
+ raise SecurityError, "Dangerous command pattern detected: #{pattern.source}"
260
+ end
261
+ end
262
+
263
+ command
264
+ end
265
+
266
+ # Block writes that would clobber credentials / secrets.
267
+ # These are the only paths truly dangerous to write to by accident:
268
+ # - ~/.ssh/* (SSH private keys)
269
+ # - ~/.aws/* (AWS credentials)
270
+ # - any *.env file (API keys, DB URLs, etc.)
271
+ #
272
+ # Paths in / outside the project root, Gemfile, README, package.json,
273
+ # etc. are all allowed — the agent is expected to edit them normally.
274
+ SECRET_WRITE_PATTERNS = [
275
+ %r{(?:\A|/)\.ssh/},
276
+ %r{(?:\A|/)\.aws/},
277
+ /(?:\A|\/)\.env(?:\.|\z)/,
278
+ /\.env\z/
279
+ ].freeze
280
+
281
+ def validate_secret_write(path)
282
+ return if path.nil? || path.empty? || path.start_with?('-')
283
+
284
+ expanded_path = File.expand_path(path)
285
+
286
+ SECRET_WRITE_PATTERNS.each do |pattern|
287
+ if expanded_path.match?(pattern)
288
+ raise SecurityError,
289
+ "Write to credential/secret path blocked: #{path} " \
290
+ "(matched protected pattern). If intentional, edit the " \
291
+ "file manually outside the agent."
292
+ end
293
+ end
294
+ end
295
+
296
+ # Alias retained for readability — chmod handler validates that
297
+ # the target is not a credential/secret file.
298
+ def validate_file_path(path)
299
+ validate_secret_write(path)
300
+ end
301
+
302
+ def log_replacement(original, replacement, reason)
303
+ write_log(
304
+ action: 'command_replacement',
305
+ original_command: original,
306
+ safe_replacement: replacement,
307
+ reason: reason
308
+ )
309
+ end
310
+
311
+ def log_warning(message)
312
+ write_log(action: 'warning', message: message)
313
+ end
314
+
315
+ def write_log(**fields)
316
+ log_entry = { timestamp: Time.now.iso8601 }.merge(fields)
317
+ File.open(@safety_log_file, 'a') { |f| f.puts JSON.generate(log_entry) }
318
+ rescue StandardError
319
+ # Logging must never break main functionality.
320
+ end
321
+
322
+ private :replace_chmod_command,
323
+ :replace_curl_pipe_command, :block_sudo_command,
324
+ :allow_dev_null_redirect, :validate_and_allow,
325
+ :validate_general_command,
326
+ :validate_file_path, :validate_secret_write,
327
+ :restart_hint,
328
+ :log_replacement,
329
+ :log_warning, :write_log
330
+ end
331
+ end
332
+ end
333
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Octo
4
+ module Tools
5
+ class Terminal < Base
6
+ # Output cleaning for raw PTY bytes.
7
+ #
8
+ # A PTY emits whatever the child writes plus terminal control codes.
9
+ # Since the Terminal tool is targeted at LINE-BASED interactive shells
10
+ # (not full-screen TUIs like vim/top), we aggressively strip visual
11
+ # control sequences rather than maintain a screen model.
12
+ #
13
+ # Cleaning steps (in order):
14
+ # 1. Strip CSI sequences (ESC[...letter) — colors, cursor, SGR
15
+ # 2. Strip OSC sequences (ESC]...BEL/ST) — window title, etc.
16
+ # 3. Strip simple 2-byte esc (ESC= / ESC>) — keypad modes
17
+ # 4. Collapse \r-overwrites (spinner/progress)
18
+ # 5. Drop backspace erase (char + \x08)
19
+ # 6. Normalize CRLF → LF
20
+ #
21
+ # This is lossy for full-screen apps (you'll see a pile of text without
22
+ # cursor positioning), but for line-based commands it yields clean,
23
+ # diff-friendly output.
24
+ module OutputCleaner
25
+ CSI_REGEX = /\e\[[\d;?]*[a-zA-Z@]/.freeze
26
+ OSC_REGEX = /\e\].*?(\a|\e\\)/m.freeze
27
+ SIMPLE_ESC_REGEX = /\e[=>\(\)].?/.freeze
28
+ BACKSPACE_REGEX = /[^\x08]\x08/.freeze
29
+
30
+ module_function
31
+
32
+ # Clean raw PTY bytes for LLM consumption.
33
+ # @param raw [String] raw PTY bytes
34
+ # @return [String] cleaned, UTF-8-safe text
35
+ def clean(raw)
36
+ return "" if raw.nil? || raw.empty?
37
+
38
+ s = raw.dup
39
+ s.force_encoding(Encoding::UTF_8)
40
+ s = s.scrub("?") unless s.valid_encoding?
41
+
42
+ s = s.gsub(CSI_REGEX, "")
43
+ s = s.gsub(OSC_REGEX, "")
44
+ s = s.gsub(SIMPLE_ESC_REGEX, "")
45
+
46
+ # Handle \r overwrites within each line. "50%\r100%" → "100%".
47
+ # Split on \n KEEPING the terminators (-1 preserves trailing empty),
48
+ # then for each segment keep only the portion after the last \r
49
+ # (which is what would actually be visible).
50
+ s = s.split("\n", -1).map { |line| line.split("\r").last || "" }.join("\n")
51
+
52
+ # Erase "X\b" pairs repeatedly (readline rubout).
53
+ s = s.gsub(BACKSPACE_REGEX, "") while s =~ BACKSPACE_REGEX
54
+
55
+ # Normalize any leftover isolated \r.
56
+ s = s.gsub(/\r/, "")
57
+
58
+ s
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end