rubino-agent 0.3.0

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 (376) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +115 -0
  4. data/.rubocop_todo.yml +955 -0
  5. data/.ruby-version +1 -0
  6. data/AGENTS.md +97 -0
  7. data/CHANGELOG.md +344 -0
  8. data/CONTRIBUTING.md +69 -0
  9. data/LICENSE +21 -0
  10. data/README.md +200 -0
  11. data/Rakefile +8 -0
  12. data/docs/agents.md +190 -0
  13. data/docs/api/v1.md +414 -0
  14. data/docs/architecture.md +177 -0
  15. data/docs/commands.md +375 -0
  16. data/docs/configuration.md +590 -0
  17. data/docs/getting-started.md +143 -0
  18. data/docs/jobs.md +332 -0
  19. data/docs/mcp.md +128 -0
  20. data/docs/memory.md +98 -0
  21. data/docs/models-and-keys.md +173 -0
  22. data/docs/oauth-providers.md +145 -0
  23. data/docs/plugins.md +195 -0
  24. data/docs/security.md +145 -0
  25. data/docs/skills.md +322 -0
  26. data/docs/tools.md +395 -0
  27. data/docs/troubleshooting.md +73 -0
  28. data/exe/rubino +9 -0
  29. data/install.sh +275 -0
  30. data/lib/rubino/active_skill.rb +50 -0
  31. data/lib/rubino/agent/agent_registry.rb +120 -0
  32. data/lib/rubino/agent/backoff_policy.rb +116 -0
  33. data/lib/rubino/agent/definition.rb +128 -0
  34. data/lib/rubino/agent/degenerate_recovery.rb +271 -0
  35. data/lib/rubino/agent/fallback_chain.rb +194 -0
  36. data/lib/rubino/agent/iteration_budget.rb +50 -0
  37. data/lib/rubino/agent/loop.rb +617 -0
  38. data/lib/rubino/agent/model_call_runner.rb +383 -0
  39. data/lib/rubino/agent/prompts/build.txt +69 -0
  40. data/lib/rubino/agent/prompts/compaction.txt +20 -0
  41. data/lib/rubino/agent/prompts/explore.txt +19 -0
  42. data/lib/rubino/agent/prompts/general.txt +20 -0
  43. data/lib/rubino/agent/prompts/plan.txt +31 -0
  44. data/lib/rubino/agent/response_validator.rb +70 -0
  45. data/lib/rubino/agent/router.rb +65 -0
  46. data/lib/rubino/agent/runner.rb +195 -0
  47. data/lib/rubino/agent/tool_executor.rb +402 -0
  48. data/lib/rubino/agent/truncation_continuation.rb +137 -0
  49. data/lib/rubino/api/middleware/auth.rb +43 -0
  50. data/lib/rubino/api/middleware/error_handler.rb +65 -0
  51. data/lib/rubino/api/middleware/json_parser.rb +100 -0
  52. data/lib/rubino/api/middleware/observability.rb +59 -0
  53. data/lib/rubino/api/middleware/rate_limit.rb +136 -0
  54. data/lib/rubino/api/operations/approvals/decide_operation.rb +49 -0
  55. data/lib/rubino/api/operations/clarifications/decide_operation.rb +44 -0
  56. data/lib/rubino/api/operations/cron_jobs/create_operation.rb +46 -0
  57. data/lib/rubino/api/operations/cron_jobs/delete_operation.rb +36 -0
  58. data/lib/rubino/api/operations/cron_jobs/list_operation.rb +55 -0
  59. data/lib/rubino/api/operations/cron_jobs/pause_operation.rb +34 -0
  60. data/lib/rubino/api/operations/cron_jobs/resume_operation.rb +34 -0
  61. data/lib/rubino/api/operations/cron_jobs/schedule_validation.rb +30 -0
  62. data/lib/rubino/api/operations/cron_jobs/show_operation.rb +32 -0
  63. data/lib/rubino/api/operations/cron_jobs/trigger_operation.rb +38 -0
  64. data/lib/rubino/api/operations/cron_jobs/update_operation.rb +42 -0
  65. data/lib/rubino/api/operations/files/read_operation.rb +40 -0
  66. data/lib/rubino/api/operations/files/upload_operation.rb +175 -0
  67. data/lib/rubino/api/operations/health_operation.rb +46 -0
  68. data/lib/rubino/api/operations/memory/delete_operation.rb +32 -0
  69. data/lib/rubino/api/operations/memory/index_operation.rb +80 -0
  70. data/lib/rubino/api/operations/memory/stats_operation.rb +28 -0
  71. data/lib/rubino/api/operations/metrics_operation.rb +18 -0
  72. data/lib/rubino/api/operations/mode/show_operation.rb +29 -0
  73. data/lib/rubino/api/operations/mode/update_operation.rb +42 -0
  74. data/lib/rubino/api/operations/models/list_operation.rb +45 -0
  75. data/lib/rubino/api/operations/oauth/connections/disconnect_operation.rb +77 -0
  76. data/lib/rubino/api/operations/oauth/connections/list_operation.rb +36 -0
  77. data/lib/rubino/api/operations/oauth/providers/callback_operation.rb +82 -0
  78. data/lib/rubino/api/operations/oauth/providers/connect_operation.rb +44 -0
  79. data/lib/rubino/api/operations/oauth/providers/list_operation.rb +35 -0
  80. data/lib/rubino/api/operations/oauth/serializer.rb +21 -0
  81. data/lib/rubino/api/operations/runs/create_operation.rb +77 -0
  82. data/lib/rubino/api/operations/runs/events_operation.rb +195 -0
  83. data/lib/rubino/api/operations/runs/stop_operation.rb +34 -0
  84. data/lib/rubino/api/operations/sessions/create_operation.rb +46 -0
  85. data/lib/rubino/api/operations/sessions/delete_operation.rb +33 -0
  86. data/lib/rubino/api/operations/sessions/index_operation.rb +82 -0
  87. data/lib/rubino/api/operations/sessions/retry_operation.rb +45 -0
  88. data/lib/rubino/api/operations/sessions/show_operation.rb +59 -0
  89. data/lib/rubino/api/operations/sessions/undo_operation.rb +38 -0
  90. data/lib/rubino/api/operations/skills/list_operation.rb +34 -0
  91. data/lib/rubino/api/operations/skills/toggle_operation.rb +40 -0
  92. data/lib/rubino/api/operations/tasks/index_operation.rb +30 -0
  93. data/lib/rubino/api/operations/tasks/serializer.rb +60 -0
  94. data/lib/rubino/api/operations/tasks/show_operation.rb +33 -0
  95. data/lib/rubino/api/operations/tasks/stop_operation.rb +47 -0
  96. data/lib/rubino/api/request.rb +54 -0
  97. data/lib/rubino/api/responses.rb +64 -0
  98. data/lib/rubino/api/router.rb +72 -0
  99. data/lib/rubino/api/schemas.rb +103 -0
  100. data/lib/rubino/api/server.rb +102 -0
  101. data/lib/rubino/api/tls.rb +108 -0
  102. data/lib/rubino/attachments/classification.rb +16 -0
  103. data/lib/rubino/attachments/classify.rb +171 -0
  104. data/lib/rubino/attachments/defang.rb +47 -0
  105. data/lib/rubino/attachments/policy.rb +36 -0
  106. data/lib/rubino/attachments/preamble.rb +120 -0
  107. data/lib/rubino/boot/encryption_key.rb +32 -0
  108. data/lib/rubino/cli/chat/bang_shell.rb +257 -0
  109. data/lib/rubino/cli/chat/completion_builder.rb +290 -0
  110. data/lib/rubino/cli/chat/idle_card_host.rb +69 -0
  111. data/lib/rubino/cli/chat/image_inbox.rb +168 -0
  112. data/lib/rubino/cli/chat/session_resolver.rb +176 -0
  113. data/lib/rubino/cli/chat_command.rb +1674 -0
  114. data/lib/rubino/cli/commands.rb +250 -0
  115. data/lib/rubino/cli/config_command.rb +96 -0
  116. data/lib/rubino/cli/doctor_command.rb +251 -0
  117. data/lib/rubino/cli/jobs_command.rb +60 -0
  118. data/lib/rubino/cli/memory_command.rb +135 -0
  119. data/lib/rubino/cli/onboarding_wizard.rb +207 -0
  120. data/lib/rubino/cli/server_command.rb +139 -0
  121. data/lib/rubino/cli/session_command.rb +125 -0
  122. data/lib/rubino/cli/setup_command.rb +107 -0
  123. data/lib/rubino/cli/skills_command.rb +85 -0
  124. data/lib/rubino/cli/tools_command.rb +81 -0
  125. data/lib/rubino/cli/trust_gate.rb +71 -0
  126. data/lib/rubino/commands/built_ins.rb +46 -0
  127. data/lib/rubino/commands/command.rb +116 -0
  128. data/lib/rubino/commands/executor.rb +550 -0
  129. data/lib/rubino/commands/handlers/agents.rb +510 -0
  130. data/lib/rubino/commands/handlers/config.rb +88 -0
  131. data/lib/rubino/commands/handlers/help.rb +148 -0
  132. data/lib/rubino/commands/handlers/jobs.rb +71 -0
  133. data/lib/rubino/commands/handlers/mcp.rb +229 -0
  134. data/lib/rubino/commands/handlers/memory.rb +200 -0
  135. data/lib/rubino/commands/handlers/sessions.rb +207 -0
  136. data/lib/rubino/commands/handlers/skills.rb +195 -0
  137. data/lib/rubino/commands/handlers/status.rb +211 -0
  138. data/lib/rubino/commands/loader.rb +90 -0
  139. data/lib/rubino/config/configuration.rb +455 -0
  140. data/lib/rubino/config/defaults.rb +569 -0
  141. data/lib/rubino/config/loader.rb +115 -0
  142. data/lib/rubino/config/reasoning_prefs.rb +67 -0
  143. data/lib/rubino/config/writer.rb +72 -0
  144. data/lib/rubino/context/compressor.rb +149 -0
  145. data/lib/rubino/context/environment_inspector.rb +176 -0
  146. data/lib/rubino/context/file_discovery.rb +45 -0
  147. data/lib/rubino/context/message_boundary.rb +39 -0
  148. data/lib/rubino/context/prompt_assembler.rb +382 -0
  149. data/lib/rubino/context/summary_builder.rb +159 -0
  150. data/lib/rubino/context/token_budget.rb +68 -0
  151. data/lib/rubino/context/tool_pair_sanitizer.rb +70 -0
  152. data/lib/rubino/database/connection.rb +77 -0
  153. data/lib/rubino/database/migrations/001_create_initial_schema.rb +156 -0
  154. data/lib/rubino/database/migrations/002_create_runs.rb +45 -0
  155. data/lib/rubino/database/migrations/003_create_skill_states.rb +15 -0
  156. data/lib/rubino/database/migrations/004_create_cron_jobs.rb +36 -0
  157. data/lib/rubino/database/migrations/005_create_oauth_connections.rb +27 -0
  158. data/lib/rubino/database/migrations/006_create_webhook_deliveries.rb +34 -0
  159. data/lib/rubino/database/migrations/007_create_messages_fts.rb +59 -0
  160. data/lib/rubino/database/migrations/008_create_memory_facts.rb +75 -0
  161. data/lib/rubino/database/migrations/009_create_memory_graph.rb +55 -0
  162. data/lib/rubino/database/migrations/010_add_owner_pid_to_sessions.rb +20 -0
  163. data/lib/rubino/database/migrator.rb +48 -0
  164. data/lib/rubino/documents/converters/csv.rb +79 -0
  165. data/lib/rubino/documents/converters/docx.rb +129 -0
  166. data/lib/rubino/documents/converters/html.rb +28 -0
  167. data/lib/rubino/documents/converters/json.rb +35 -0
  168. data/lib/rubino/documents/converters/pdf.rb +59 -0
  169. data/lib/rubino/documents/converters/plain.rb +68 -0
  170. data/lib/rubino/documents/converters/pptx.rb +64 -0
  171. data/lib/rubino/documents/converters/xlsx.rb +62 -0
  172. data/lib/rubino/documents/converters/xml.rb +45 -0
  173. data/lib/rubino/documents/html.rb +71 -0
  174. data/lib/rubino/documents/registry.rb +68 -0
  175. data/lib/rubino/documents/table.rb +63 -0
  176. data/lib/rubino/documents.rb +50 -0
  177. data/lib/rubino/errors.rb +119 -0
  178. data/lib/rubino/files/workspace.rb +93 -0
  179. data/lib/rubino/interaction/cancel_token.rb +43 -0
  180. data/lib/rubino/interaction/clipboard_image.rb +84 -0
  181. data/lib/rubino/interaction/event_bus.rb +48 -0
  182. data/lib/rubino/interaction/events.rb +101 -0
  183. data/lib/rubino/interaction/image_input.rb +127 -0
  184. data/lib/rubino/interaction/input_queue.rb +117 -0
  185. data/lib/rubino/interaction/lifecycle.rb +299 -0
  186. data/lib/rubino/interaction/probe.rb +65 -0
  187. data/lib/rubino/interaction/state.rb +56 -0
  188. data/lib/rubino/jobs/cron_job_repository.rb +75 -0
  189. data/lib/rubino/jobs/handlers/cleanup_sessions_job.rb +32 -0
  190. data/lib/rubino/jobs/handlers/compact_session_job.rb +21 -0
  191. data/lib/rubino/jobs/handlers/distill_skill_job.rb +186 -0
  192. data/lib/rubino/jobs/handlers/extract_memory_job.rb +37 -0
  193. data/lib/rubino/jobs/handlers/summarize_session_job.rb +21 -0
  194. data/lib/rubino/jobs/queue.rb +184 -0
  195. data/lib/rubino/jobs/registry.rb +45 -0
  196. data/lib/rubino/jobs/runner.rb +79 -0
  197. data/lib/rubino/jobs/scheduler.rb +138 -0
  198. data/lib/rubino/jobs/webhook_delivery.rb +225 -0
  199. data/lib/rubino/jobs/worker.rb +59 -0
  200. data/lib/rubino/llm/adapter_factory.rb +47 -0
  201. data/lib/rubino/llm/adapter_response.rb +65 -0
  202. data/lib/rubino/llm/auxiliary_client.rb +61 -0
  203. data/lib/rubino/llm/bedrock_bearer_client.rb +235 -0
  204. data/lib/rubino/llm/content_builder.rb +55 -0
  205. data/lib/rubino/llm/credential_check.rb +93 -0
  206. data/lib/rubino/llm/error_classifier.rb +364 -0
  207. data/lib/rubino/llm/fake_provider.rb +292 -0
  208. data/lib/rubino/llm/inline_think_filter.rb +58 -0
  209. data/lib/rubino/llm/model_catalog.rb +29 -0
  210. data/lib/rubino/llm/provider_resolver.rb +48 -0
  211. data/lib/rubino/llm/reasoning_manager.rb +100 -0
  212. data/lib/rubino/llm/request.rb +56 -0
  213. data/lib/rubino/llm/ruby_llm_adapter.rb +794 -0
  214. data/lib/rubino/llm/scenario_loader.rb +68 -0
  215. data/lib/rubino/llm/scenario_selector.rb +80 -0
  216. data/lib/rubino/llm/scenarios/agent-creates-cron-failure.yml +29 -0
  217. data/lib/rubino/llm/scenarios/agent-creates-cron.yml +36 -0
  218. data/lib/rubino/llm/scenarios/analysis.yml +501 -0
  219. data/lib/rubino/llm/scenarios/complex-analysis.yml +598 -0
  220. data/lib/rubino/llm/scenarios/failure.yml +65 -0
  221. data/lib/rubino/llm/scenarios/happy-path.yml +24 -0
  222. data/lib/rubino/llm/scenarios/provider-quota-completed.yml +14 -0
  223. data/lib/rubino/llm/scenarios/wide-table.yml +121 -0
  224. data/lib/rubino/llm/scenarios/with-approvals.yml +50 -0
  225. data/lib/rubino/llm/scenarios/with-artifacts.yml +98 -0
  226. data/lib/rubino/llm/scenarios/with-clarify.yml +32 -0
  227. data/lib/rubino/llm/scenarios/with-reasoning.yml +175 -0
  228. data/lib/rubino/llm/scenarios/with-uploads.yml +104 -0
  229. data/lib/rubino/llm/thinking_support.rb +84 -0
  230. data/lib/rubino/llm/tool_bridge.rb +89 -0
  231. data/lib/rubino/logger.rb +99 -0
  232. data/lib/rubino/mcp/manager.rb +180 -0
  233. data/lib/rubino/mcp/mcp_tool_wrapper.rb +69 -0
  234. data/lib/rubino/mcp.rb +57 -0
  235. data/lib/rubino/memory/backend.rb +104 -0
  236. data/lib/rubino/memory/backends/default.rb +101 -0
  237. data/lib/rubino/memory/backends/sqlite.rb +653 -0
  238. data/lib/rubino/memory/backends.rb +53 -0
  239. data/lib/rubino/memory/deduplicator.rb +74 -0
  240. data/lib/rubino/memory/extractor.rb +85 -0
  241. data/lib/rubino/memory/flusher.rb +31 -0
  242. data/lib/rubino/memory/retriever.rb +50 -0
  243. data/lib/rubino/memory/sqlite_extraction_prompt.rb +70 -0
  244. data/lib/rubino/memory/sqlite_graph.rb +154 -0
  245. data/lib/rubino/memory/store.rb +228 -0
  246. data/lib/rubino/memory/threat_scanner.rb +68 -0
  247. data/lib/rubino/metrics.rb +175 -0
  248. data/lib/rubino/modes.rb +93 -0
  249. data/lib/rubino/oauth/connection_repository.rb +95 -0
  250. data/lib/rubino/oauth/provider/github.rb +75 -0
  251. data/lib/rubino/oauth/provider/google.rb +59 -0
  252. data/lib/rubino/oauth/provider.rb +149 -0
  253. data/lib/rubino/oauth/registry.rb +86 -0
  254. data/lib/rubino/oauth/token_encryptor.rb +87 -0
  255. data/lib/rubino/plugins/registry.rb +75 -0
  256. data/lib/rubino/plugins.rb +86 -0
  257. data/lib/rubino/run/approval_gate.rb +243 -0
  258. data/lib/rubino/run/attachment_downloader.rb +166 -0
  259. data/lib/rubino/run/event_store.rb +74 -0
  260. data/lib/rubino/run/executor.rb +383 -0
  261. data/lib/rubino/run/gate_registry.rb +39 -0
  262. data/lib/rubino/run/recorder.rb +69 -0
  263. data/lib/rubino/run/repository.rb +118 -0
  264. data/lib/rubino/run/session_approval_cache.rb +118 -0
  265. data/lib/rubino/security/allowlist_persister.rb +55 -0
  266. data/lib/rubino/security/approval_policy.rb +227 -0
  267. data/lib/rubino/security/command_allowlist.rb +24 -0
  268. data/lib/rubino/security/dangerous_patterns.rb +118 -0
  269. data/lib/rubino/security/deny_persister.rb +73 -0
  270. data/lib/rubino/security/doom_loop_detector.rb +43 -0
  271. data/lib/rubino/security/hardline_guard.rb +105 -0
  272. data/lib/rubino/security/pattern_matcher.rb +62 -0
  273. data/lib/rubino/security/prefix_deriver.rb +124 -0
  274. data/lib/rubino/security/readonly_commands.rb +211 -0
  275. data/lib/rubino/session/exporter.rb +101 -0
  276. data/lib/rubino/session/message.rb +77 -0
  277. data/lib/rubino/session/repository.rb +295 -0
  278. data/lib/rubino/session/store.rb +198 -0
  279. data/lib/rubino/session/summary_store.rb +65 -0
  280. data/lib/rubino/skills/prompt_index.rb +85 -0
  281. data/lib/rubino/skills/registry.rb +208 -0
  282. data/lib/rubino/skills/skill.rb +176 -0
  283. data/lib/rubino/skills/skill_tool.rb +215 -0
  284. data/lib/rubino/skills/state_repository.rb +37 -0
  285. data/lib/rubino/skills/toggle.rb +26 -0
  286. data/lib/rubino/tools/answer_child_tool.rb +83 -0
  287. data/lib/rubino/tools/ask_parent_tool.rb +232 -0
  288. data/lib/rubino/tools/attach_file_tool.rb +120 -0
  289. data/lib/rubino/tools/background_tasks.rb +520 -0
  290. data/lib/rubino/tools/base.rb +222 -0
  291. data/lib/rubino/tools/custom_tool_loader.rb +119 -0
  292. data/lib/rubino/tools/edit_tool.rb +122 -0
  293. data/lib/rubino/tools/git_tool.rb +71 -0
  294. data/lib/rubino/tools/github_tool.rb +233 -0
  295. data/lib/rubino/tools/glob_tool.rb +69 -0
  296. data/lib/rubino/tools/grep_tool.rb +206 -0
  297. data/lib/rubino/tools/memory_tool.rb +184 -0
  298. data/lib/rubino/tools/multi_edit_tool.rb +110 -0
  299. data/lib/rubino/tools/patch_tool.rb +260 -0
  300. data/lib/rubino/tools/probe_tool.rb +175 -0
  301. data/lib/rubino/tools/question_tool.rb +128 -0
  302. data/lib/rubino/tools/read_attachment_tool.rb +180 -0
  303. data/lib/rubino/tools/read_tool.rb +212 -0
  304. data/lib/rubino/tools/read_tracker.rb +98 -0
  305. data/lib/rubino/tools/registry.rb +166 -0
  306. data/lib/rubino/tools/result.rb +113 -0
  307. data/lib/rubino/tools/ruby_tool.rb +0 -0
  308. data/lib/rubino/tools/session_search_tool.rb +103 -0
  309. data/lib/rubino/tools/shell_input_tool.rb +96 -0
  310. data/lib/rubino/tools/shell_kill_tool.rb +76 -0
  311. data/lib/rubino/tools/shell_output_tool.rb +72 -0
  312. data/lib/rubino/tools/shell_registry.rb +158 -0
  313. data/lib/rubino/tools/shell_tail_tool.rb +118 -0
  314. data/lib/rubino/tools/shell_tool.rb +330 -0
  315. data/lib/rubino/tools/steer_tool.rb +118 -0
  316. data/lib/rubino/tools/subagent_probe.rb +89 -0
  317. data/lib/rubino/tools/summarize_file_tool.rb +182 -0
  318. data/lib/rubino/tools/task_result_tool.rb +90 -0
  319. data/lib/rubino/tools/task_stop_tool.rb +80 -0
  320. data/lib/rubino/tools/task_tool.rb +622 -0
  321. data/lib/rubino/tools/test_tool.rb +454 -0
  322. data/lib/rubino/tools/todo_tool.rb +93 -0
  323. data/lib/rubino/tools/tool_call_repository.rb +33 -0
  324. data/lib/rubino/tools/vision_tool.rb +85 -0
  325. data/lib/rubino/tools/webfetch_tool.rb +153 -0
  326. data/lib/rubino/tools/websearch_tool.rb +179 -0
  327. data/lib/rubino/tools/write_tool.rb +61 -0
  328. data/lib/rubino/trust.rb +88 -0
  329. data/lib/rubino/ui/api.rb +296 -0
  330. data/lib/rubino/ui/base.rb +252 -0
  331. data/lib/rubino/ui/bottom_composer.rb +1599 -0
  332. data/lib/rubino/ui/cli.rb +1987 -0
  333. data/lib/rubino/ui/completion_menu.rb +321 -0
  334. data/lib/rubino/ui/completion_source.rb +284 -0
  335. data/lib/rubino/ui/escape_reader.rb +169 -0
  336. data/lib/rubino/ui/indented_io.rb +88 -0
  337. data/lib/rubino/ui/input_history.rb +108 -0
  338. data/lib/rubino/ui/live_region.rb +183 -0
  339. data/lib/rubino/ui/markdown_renderer.rb +506 -0
  340. data/lib/rubino/ui/notifier.rb +163 -0
  341. data/lib/rubino/ui/null.rb +195 -0
  342. data/lib/rubino/ui/paste_store.rb +176 -0
  343. data/lib/rubino/ui/printer_base.rb +79 -0
  344. data/lib/rubino/ui/probe_wait_indicator.rb +75 -0
  345. data/lib/rubino/ui/queued_indicators.rb +66 -0
  346. data/lib/rubino/ui/status_bar.rb +100 -0
  347. data/lib/rubino/ui/stdout_proxy.rb +161 -0
  348. data/lib/rubino/ui/streaming_markdown.rb +186 -0
  349. data/lib/rubino/ui/subagent_cards.rb +134 -0
  350. data/lib/rubino/ui/subagent_view.rb +255 -0
  351. data/lib/rubino/ui.rb +21 -0
  352. data/lib/rubino/update_check.rb +187 -0
  353. data/lib/rubino/util/duration.rb +23 -0
  354. data/lib/rubino/util/hyperlink.rb +105 -0
  355. data/lib/rubino/util/output.rb +145 -0
  356. data/lib/rubino/util/secrets_mask.rb +83 -0
  357. data/lib/rubino/version.rb +5 -0
  358. data/lib/rubino/workspace.rb +85 -0
  359. data/lib/rubino-agent.rb +5 -0
  360. data/lib/rubino.rb +318 -0
  361. data/mise.toml +2 -0
  362. data/rubino-agent.gemspec +103 -0
  363. data/skills/ruby-expert/SKILL.md +67 -0
  364. data/skills/ruby-expert/references/concurrency.md +357 -0
  365. data/skills/ruby-expert/references/datetime-and-encoding.md +363 -0
  366. data/skills/ruby-expert/references/errors-and-types.md +460 -0
  367. data/skills/ruby-expert/references/gem-authoring.md +459 -0
  368. data/skills/ruby-expert/references/language-idioms.md +465 -0
  369. data/skills/ruby-expert/references/metaprogramming.md +339 -0
  370. data/skills/ruby-expert/references/oo-design.md +553 -0
  371. data/skills/ruby-expert/references/performance.md +383 -0
  372. data/skills/ruby-expert/references/rails.md +424 -0
  373. data/skills/ruby-expert/references/security.md +404 -0
  374. data/skills/ruby-expert/references/testing.md +473 -0
  375. data/skills/ruby-expert/references/tooling.md +466 -0
  376. metadata +856 -0
data/docs/memory.md ADDED
@@ -0,0 +1,98 @@
1
+ # Memory
2
+
3
+ rubino remembers facts about you and the project across sessions. The default backend is a small SQLite "tiny-Zep" store — Zep/Graphiti-inspired, minus the graph database, the server, and the multi-call pipeline.
4
+
5
+ ## Backends
6
+
7
+ Memory backends are pluggable (registered like tools). Two ship:
8
+
9
+ | `memory.backend` | What it is |
10
+ |---|---|
11
+ | `sqlite` (**default**) | tiny-Zep: LLM-extracted atomic facts, bi-temporal supersession, hybrid FTS5/BM25 (+ optional vector) ranked recall, graph-lite 1-hop blend |
12
+ | `default` | the legacy non-ranked store (kept for back-compat) |
13
+
14
+ Switch backends:
15
+
16
+ ```bash
17
+ rubino memory backend # show the active backend + available names
18
+ rubino memory backend sqlite # switch (writes memory.backend to config.yml)
19
+ ```
20
+
21
+ The agent loop, the in-chat `/memory` view, the `/status` panel, the `rubino memory` CLI, and the HTTP `/v1/memory` operations all use the **active** backend (fixed in #94/#106/#83 — these surfaces previously read a hardwired legacy table and never saw the facts the agent actually persists).
22
+
23
+ ## The sqlite tiny-Zep backend
24
+
25
+ ### What's stored
26
+
27
+ One declarative **fact** per row. Facts carry a `kind` (`user_profile`, `preference`, `fact`, `project`, `env`, …), the source session, a confidence, optional entity tags, and bi-temporal validity columns (`valid_from` / `valid_to`).
28
+
29
+ - **User profile** (`user_profile` facts) — durable facts about you, metered against `memory.user_char_limit`.
30
+ - **Project context** (`project` / `env` facts) — facts about the codebase/environment.
31
+ - **General facts** — everything else.
32
+
33
+ ### How facts are extracted (write path)
34
+
35
+ When `memory.auto_extract` is on, auto-extraction runs as a post-turn job (`ExtractMemoryJob` — executed immediately after the turn in the default inline jobs mode): a single auxiliary-LLM call looks at the recent turn and returns `{add, supersede}`:
36
+
37
+ - **add** — new atomic facts (deduplicated via a Jaccard near-dup check against the live set, no second LLM call).
38
+ - **supersede** — a contradicted fact is **soft-retired** (its `valid_to` is set and `superseded_by` points at the replacement), not deleted — temporal correctness without losing provenance (Graphiti-style edge invalidation collapsed to one call).
39
+
40
+ When extraction stores facts, the chat prints a deterministic confirmation from the write path (`✓ saved to memory · 2 facts (e6bf776b, a91c03d2)`) — the agent's "I'll remember that" narration alone is not a save signal.
41
+
42
+ Every write goes through the same injection-defense floor as the legacy store: a `ThreatScanner` (prompt-injection / exfiltration patterns) plus a character budget. A fact that trips a guard is skipped, not allowed to splice tainted/over-budget content into a future system prompt.
43
+
44
+ Two budgets, deliberately separate:
45
+
46
+ - `memory.memory_char_limit` (2200) / `memory.user_char_limit` (1375) — the **injection** budget: how much is packed into the prompt at retrieval time.
47
+ - `memory.ingest_char_limit` (null = unbounded) — the **store** budget: storing facts isn't throttled by the injection budget, so long multi-session conversations don't stall once the injection budget fills.
48
+
49
+ ### How facts are recalled (read path)
50
+
51
+ `retrieve` runs a **hybrid ranked** recall over LIVE facts (`valid_to IS NULL`):
52
+
53
+ 1. **Direct relevance** — FTS5/BM25 over the query (and vector KNN when enabled), fused with Reciprocal Rank Fusion and lightly kind-weighted (durable `user_profile`/`preference`/`env` facts win ties). These are the only content-matching signals, so the fact a keyword probe ranks #1 stays #1.
54
+ 2. **Tail supplements** — graph (1-hop entity neighbours of the query) then recency only **backfill** the remaining budget after direct hits. They can never outrank a direct content match (this was the dominant cause of single-shot recall misses).
55
+
56
+ Results are greedily packed under the retrieval char budget. Common stopwords ("user", "project", "the", …) are excluded from the FTS MATCH so a probe doesn't match every fact on a trivial word.
57
+
58
+ ### Tuning
59
+
60
+ ```yaml
61
+ memory:
62
+ sqlite:
63
+ vector: false # opt-in sqlite-vec / RubyLLM.embed KNN on top of FTS5 (off by default — no extra deps needed)
64
+ graph: true # graph-lite 1-hop entity/edge blend (on by default; set false to A/B the graph signal)
65
+ ```
66
+
67
+ Vector mode requires both `vector: true` **and** `RubyLLM.embed` to be wired; otherwise it's FTS5-only.
68
+
69
+ ## The `memory` tool
70
+
71
+ The agent persists facts autonomously via the `memory` tool (gated by `tools.memory`, on by default):
72
+
73
+ - `action: add` — record a new fact.
74
+ - `action: replace` — supersede an existing fact (`old_text` selects it).
75
+ - `action: remove` — hard-delete a fact.
76
+ - `target: user` writes the user profile; `target: memory` writes general memory.
77
+
78
+ The tool stores **one atomic fact per call** — separate facts go in separate calls so each can be superseded or forgotten independently. Every write is confirmed deterministically in chat by the tool-result line, e.g.:
79
+
80
+ ```
81
+ └ ✓ Memory replaced (id=e6bf776b, kind=user_profile).
82
+ ```
83
+
84
+ Content is scanned for injection/exfiltration patterns and subject to the character budget. Because this lets the agent write to its own future context, see [security.md](security.md#autonomous-memory) for the trust model.
85
+
86
+ ## Inspecting and managing memory
87
+
88
+ ```bash
89
+ rubino memory list # most recent LIVE facts (active backend)
90
+ rubino memory list --all # include superseded (soft-retired) facts
91
+ rubino memory list --kind user_profile --limit 50
92
+ rubino memory show <id> # full fact incl. the temporal chain (id prefix accepted)
93
+ rubino memory delete <id> # hard-delete
94
+ ```
95
+
96
+ `list` and the in-chat `/memory` views show only **live** facts (`valid_to IS NULL`) — superseded facts are retained for provenance but hidden, so a contradicted fact is never presented as current and the header count always matches the rows. Pass `--all` to `rubino memory list` to see the supersession history; `rubino memory show <id>` prints a retired fact's `Retired:` / `Superseded by:` chain.
97
+
98
+ In-chat, `/memory` inspects, searches (`/memory <query>` or `/memory search <query>`), and forgets what the agent remembers. Both surfaces read the active backend.
@@ -0,0 +1,173 @@
1
+ # Models & keys
2
+
3
+ Which provider, which model, which key — answered in 60 seconds. The fastest path is `rubino setup`, which writes all of the blocks below for you. This page is the manual reference and the per-provider copy-paste.
4
+
5
+ ## The decision
6
+
7
+ | Provider | When | Default model the wizard writes |
8
+ |---|---|---|
9
+ | **OpenAI** | Recommended default; GPT models | `gpt-4.1` |
10
+ | **MiniMax** | Anthropic-compatible | `MiniMax-M2.7` |
11
+ | **Anthropic** | Claude models | `claude-sonnet-4-5` |
12
+ | **Google (Gemini)** | Gemini models | `gemini-2.5-pro` |
13
+ | **OpenAI-compatible gateway** | A gateway picks the upstream | `auto` |
14
+ | **fake** | Tests/demos only | `fake/happy-path` (needs `RUBINO_ALLOW_FAKE=1`) |
15
+
16
+ How resolution works: an explicit `model.provider` (anything other than `auto`) wins. When `provider: auto`, the provider is derived from the `model.default` id by ruby_llm's registry. A key for the resolved provider must be available either via `providers.<name>.api_key` in `config.yml` **or** the provider's native ENV var.
17
+
18
+ ## ⚠️ The default→OpenRouter trap (refs #93)
19
+
20
+ The shipped default is:
21
+
22
+ ```yaml
23
+ model:
24
+ default: "openai/gpt-4.1"
25
+ provider: "auto"
26
+ ```
27
+
28
+ In ruby_llm's model registry, the id `openai/gpt-4.1` resolves to **OpenRouter**, not OpenAI's own API. Historically, a brand-new user with no key hit ~80 seconds of silent retries against an endpoint they never chose, then got an empty answer and a success exit — a dead end with no signal.
29
+
30
+ **This is now fixed.** Before any model call, rubino checks that the resolved provider has a usable credential (`LLM::CredentialCheck`). If not:
31
+
32
+ - On a TTY → the onboarding wizard runs so you pick a provider + paste a key.
33
+ - Non-interactively (`-q` / piped / no TTY) → it prints a clear, actionable message and exits non-zero (no silent retry storm):
34
+
35
+ ```
36
+ No API key configured for provider 'openai' (model openai/gpt-4.1).
37
+ Set it up one of these ways:
38
+ • run `rubino setup` for a guided first-run setup, or
39
+ • add OPENAI_API_KEY=<your-key> to ~/.rubino/.env, or
40
+ • set providers.openai.api_key in ~/.rubino/config.yml.
41
+ ```
42
+
43
+ The simplest fix is `rubino setup`. To deliberately use OpenAI's own API (not OpenRouter), set a bare `gpt-4.1` with `provider: openai` as shown below.
44
+
45
+ ## Where keys live
46
+
47
+ - Keys go in `~/.rubino/.env` (mode `0600`) as `KEY=value`, or directly as `providers.<name>.api_key` in `config.yml`.
48
+ - In `config.yml` you can reference an env var with the substitution syntax: `api_key: "${MINIMAX_API_KEY}"` or `"{env:MINIMAX_API_KEY}"`.
49
+ - `RUBINO_HOME` relocates the whole home (config, `.env`, and the database follow it).
50
+
51
+ The native ENV var per provider: `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY` (or `GOOGLE_API_KEY`), `BEDROCK_API_KEY`, `MINIMAX_API_KEY`.
52
+
53
+ ---
54
+
55
+ ## Per-provider setup
56
+
57
+ Each block goes in `~/.rubino/config.yml`; the key goes in `~/.rubino/.env`.
58
+
59
+ ### MiniMax (Anthropic-compatible)
60
+
61
+ MiniMax speaks the Anthropic API, so it routes through the anthropic-compatible path.
62
+
63
+ ```yaml
64
+ model:
65
+ default: "MiniMax-M2.7"
66
+ provider: "minimax"
67
+
68
+ providers:
69
+ minimax:
70
+ anthropic_compatible: true
71
+ base_url: "https://api.minimax.io/anthropic"
72
+ api_key: "${MINIMAX_API_KEY}"
73
+ ```
74
+
75
+ ```bash
76
+ # ~/.rubino/.env
77
+ MINIMAX_API_KEY=...
78
+ ```
79
+
80
+ > MiniMax M2 ignores tool definitions and roleplays bash in markdown; use **MiniMax-M2.7** for working tool use.
81
+
82
+ ### OpenAI (GPT) (recommended default)
83
+
84
+ Uses OpenAI's own API (not OpenRouter) when `provider: openai` and the model id is a bare OpenAI id.
85
+
86
+ ```yaml
87
+ model:
88
+ default: "gpt-4.1"
89
+ provider: "openai"
90
+ ```
91
+
92
+ ```bash
93
+ # ~/.rubino/.env
94
+ OPENAI_API_KEY=sk-...
95
+ ```
96
+
97
+ For Azure/custom endpoints set `providers.openai.base_url`.
98
+
99
+ ### Anthropic (Claude)
100
+
101
+ ```yaml
102
+ model:
103
+ default: "claude-sonnet-4-5"
104
+ provider: "anthropic"
105
+ ```
106
+
107
+ ```bash
108
+ # ~/.rubino/.env
109
+ ANTHROPIC_API_KEY=sk-ant-...
110
+ ```
111
+
112
+ ### Google (Gemini)
113
+
114
+ The provider id is `google`.
115
+
116
+ ```yaml
117
+ model:
118
+ default: "gemini-2.5-pro"
119
+ provider: "google"
120
+ ```
121
+
122
+ ```bash
123
+ # ~/.rubino/.env
124
+ GEMINI_API_KEY=...
125
+ # GOOGLE_API_KEY is also accepted
126
+ ```
127
+
128
+ ### OpenAI-compatible gateway
129
+
130
+ Point this at any OpenAI-compatible gateway; the gateway decides which upstream (OpenAI/MiniMax/Anthropic/…) and which model to call. Route everything to it with `provider: gateway` and `model: auto`, and set `base_url` + `api_key` for your gateway.
131
+
132
+ ```yaml
133
+ model:
134
+ default: "auto"
135
+ provider: "gateway"
136
+ supports_vision: null # set true/false if the gateway hides the upstream model name
137
+
138
+ providers:
139
+ gateway:
140
+ openai_compatible: true
141
+ assume_model_exists: true
142
+ base_url: "https://your-gateway/v1"
143
+ api_key: "${OPENAI_API_KEY}"
144
+ ```
145
+
146
+ `openai_compatible` providers fall back to `OPENAI_API_KEY` when no `providers.<name>.api_key` is set.
147
+
148
+ ### fake (testing only)
149
+
150
+ ```yaml
151
+ model:
152
+ default: "fake/happy-path"
153
+ ```
154
+
155
+ ```bash
156
+ export RUBINO_ALLOW_FAKE=1 # required; chat/server refuse to boot fake otherwise
157
+ ```
158
+
159
+ See the [README](../README.md#fake-llm-provider) for scenario authoring.
160
+
161
+ ---
162
+
163
+ ## Auxiliary models
164
+
165
+ Compression, approval scoring, vision, and document summarization can each run on a separate (often cheaper) model. By default they reuse the primary (`provider: "main"`). See [configuration.md](configuration.md#auxiliary) — for example, set `auxiliary.vision.model: "auto-vision"` to let an OpenAI-compatible gateway pick a vision model for the `vision` tool.
166
+
167
+ ## Verifying
168
+
169
+ ```bash
170
+ rubino doctor
171
+ ```
172
+
173
+ `doctor` reports the resolved provider for your configured model and whether a usable credential is present — the same check the chat preflight uses.
@@ -0,0 +1,145 @@
1
+ # OAuth provider connectors
2
+
3
+ Built-in OAuth integration lets users connect third-party accounts (Github, Google, etc.) so tools running inside rubino can act on their behalf.
4
+
5
+ ## Design
6
+
7
+ Four pieces:
8
+
9
+ 1. **`Rubino::OAuth::Provider`** — abstract class. Subclasses describe one provider: authorize URL builder (PKCE S256), token exchange, default scopes, account info fetcher.
10
+ 2. **`Rubino::OAuth::Registry`** — Mutex-protected module. `load_from_config!` registers a provider instance per entry in `config.oauth.providers` at boot; lookup by id via `OAuth::Registry.fetch(id)`.
11
+ 3. **`Rubino::OAuth::ConnectionRepository`** — Sequel-backed CRUD on `oauth_connections`. Encrypts `access_token`/`refresh_token` on write and decrypts on read. Upsert keyed on `(provider, account_id)`.
12
+ 4. **`Rubino::OAuth::TokenEncryptor`** — AES-256-GCM with key from `RUBINO_ENCRYPTION_KEY` (32-byte base64). Wire format: `Base64(IV || ciphertext || tag)`.
13
+
14
+ Tools resolve tokens via the repository:
15
+ ```ruby
16
+ repo = Rubino::OAuth::ConnectionRepository.new
17
+ conn = repo.list.find { |c| c[:provider] == "github" }
18
+ client = Octokit::Client.new(access_token: conn[:access_token])
19
+ ```
20
+
21
+ > **Current scope:** no auto-refresh. Expired tokens are returned as-is; the tool that uses them is responsible for handling 401s (typically by surfacing a re-auth prompt). A future task will add transparent refresh inside the repository's read path.
22
+
23
+ ## Built-in providers
24
+
25
+ | ID | Class | Default scopes | Required env |
26
+ |---|---|---|---|
27
+ | `github` | `OAuth::Provider::Github` | `repo`, `user:email` | `GITHUB_OAUTH_CLIENT_ID`, `GITHUB_OAUTH_CLIENT_SECRET` |
28
+ | `google` | `OAuth::Provider::Google` | `openid`, `email`, `profile` | `GOOGLE_OAUTH_CLIENT_ID`, `GOOGLE_OAUTH_CLIENT_SECRET` |
29
+
30
+ Adding a new provider = new file under `lib/rubino/oauth/provider/`, add it to `Rubino::OAuth::Registry::BUILTINS`, declare it in `config.oauth.providers`. `load_from_config!` (called at boot) instantiates and registers every provider whose section in the config carries both `client_id` and `client_secret`. ~50 LOC for a standard OAuth 2.0 provider.
31
+
32
+ ## Flow (PKCE by default)
33
+
34
+ ```
35
+ client rubino provider
36
+ │ │ │
37
+ │ POST /v1/oauth/.../connect │ │
38
+ │ ───────────────────────────►│ │
39
+ │ │ generates state + │
40
+ │ │ PKCE code_verifier │
41
+ │ { authorize_url, state, │ │
42
+ │ code_verifier } │ │
43
+ │ ◄───────────────────────────│ │
44
+ │ │ │
45
+ │ user redirected to authorize_url │
46
+ │ ─────────────────────────────────────────────────────►│
47
+ │ │ │
48
+ │ provider redirects to client with code + state │
49
+ │ ◄─────────────────────────────────────────────────────│
50
+ │ │ │
51
+ │ POST /v1/oauth/.../callback│ │
52
+ │ { code, state, expected_state, │
53
+ │ code_verifier, redirect_uri } │
54
+ │ ───────────────────────────►│ │
55
+ │ │ POST /token │
56
+ │ │ ───────────────────────►│
57
+ │ │ ◄───────────────────────│
58
+ │ serialized connection │ │
59
+ │ (id, provider, account_id, │ │
60
+ │ account_email, scopes, │ │
61
+ │ expires_at, metadata) │ │
62
+ │ ◄───────────────────────────│ │
63
+ ```
64
+
65
+ The **client** (e.g. a web UI) keeps `state` + `code_verifier` between connect and callback. rubino does not maintain a per-flow session — keeps it stateless.
66
+
67
+ ## Storage
68
+
69
+ ```sql
70
+ CREATE TABLE oauth_connections (
71
+ id text PRIMARY KEY, -- uuid
72
+ provider text NOT NULL,
73
+ account_id text NOT NULL, -- provider's user id
74
+ account_email text,
75
+ access_token text NOT NULL, -- encrypted, Base64(IV||ct||tag)
76
+ refresh_token text, -- encrypted, Base64(IV||ct||tag)
77
+ expires_at text, -- iso8601
78
+ scopes_json text NOT NULL, -- json array
79
+ metadata_json text, -- json
80
+ created_at text NOT NULL,
81
+ updated_at text NOT NULL,
82
+ UNIQUE (provider, account_id)
83
+ );
84
+ ```
85
+
86
+ The repository transparently encodes/decodes `scopes_json`/`metadata_json` so callers see `:scopes` (Array) and `:metadata` (Hash) on read.
87
+
88
+ Encryption key from `RUBINO_ENCRYPTION_KEY` (32-byte base64). Boot fails if missing in production.
89
+
90
+ **Tokens are never logged. Ever.** The logger has a redaction filter on `access_token`, `refresh_token`, `client_secret`.
91
+
92
+ ## Configuration
93
+
94
+ `config/rubino.yml`:
95
+ ```yaml
96
+ oauth:
97
+ providers:
98
+ github:
99
+ client_id: ${GITHUB_OAUTH_CLIENT_ID}
100
+ client_secret: ${GITHUB_OAUTH_CLIENT_SECRET}
101
+ scopes: [repo, user:email]
102
+ google:
103
+ client_id: ${GOOGLE_OAUTH_CLIENT_ID}
104
+ client_secret: ${GOOGLE_OAUTH_CLIENT_SECRET}
105
+ scopes:
106
+ - openid
107
+ - email
108
+ - profile
109
+ - https://www.googleapis.com/auth/calendar.readonly
110
+ ```
111
+
112
+ Providers not declared in config are not registered — `GET /v1/oauth/providers` only lists configured ones.
113
+
114
+ ## Setup guides
115
+
116
+ ### Github
117
+
118
+ 1. Github → Settings → Developer settings → OAuth Apps → New
119
+ 2. Authorization callback URL: `<your-client>/oauth/callback`
120
+ 3. Copy Client ID + generate Client Secret
121
+ 4. Export `GITHUB_OAUTH_CLIENT_ID` / `GITHUB_OAUTH_CLIENT_SECRET`
122
+
123
+ ### Google
124
+
125
+ 1. Google Cloud Console → APIs & Services → Credentials → Create OAuth client ID
126
+ 2. Application type: Web. Authorized redirect URIs: `<your-client>/oauth/callback`
127
+ 3. Enable required APIs (Calendar, Gmail, Drive, ...) based on scopes you want
128
+ 4. Export `GOOGLE_OAUTH_CLIENT_ID` / `GOOGLE_OAUTH_CLIENT_SECRET`
129
+
130
+ ## Why we did this (and not "delegate to client")
131
+
132
+ Rich did it: it has `/api/providers/oauth/*`. Reason it makes sense in rubino too:
133
+
134
+ - **Tools need tokens.** A `GithubTool` needs a token to call the API. If OAuth is the client's responsibility, the client has to forward tokens with every run, which is ugly and leaky.
135
+ - **Refresh logic will be centralized.** Once auto-refresh lands, expired tokens get refreshed in one place, not duplicated per client.
136
+ - **Encrypted persistence.** Clients shouldn't store user tokens long-term; the agent does, encrypted, with a redaction-aware logger.
137
+
138
+ The client (a web UI) handles only the redirect dance — opening the authorize URL in a browser and POSTing the code back. Everything else stays here.
139
+
140
+ ## Non-goals
141
+
142
+ - **Apple Sign-In:** uses JWT-signed assertions, not standard OAuth. Postponed.
143
+ - **Multi-account per provider:** one connection per provider per instance is supported. Multi-account requires UI for selection — out of scope.
144
+ - **OAuth1.0:** Twitter/X is the only relevant one. Postponed.
145
+ - **OIDC discovery:** providers are explicit classes. No `.well-known/openid-configuration` autodiscovery.
data/docs/plugins.md ADDED
@@ -0,0 +1,195 @@
1
+ # Plugin System
2
+
3
+ rubino defines 38 hook points for extending and customizing behavior.
4
+
5
+ > **Status:** The plugin registry and DSL (`Rubino.plugin do ... end`) are
6
+ > functional, but the hook points themselves are **not yet fired from the
7
+ > lifecycle** — they are declared in `Plugins::HOOKS` but not all are wired into
8
+ > the runtime yet. This document describes the intended hooks and their context
9
+ > shape (design intent); subscribing to a hook that isn't fired yet is harmless
10
+ > but has no effect.
11
+
12
+ ## Creating a Plugin
13
+
14
+ Place Ruby files in `.rubino/plugins/` or `~/.rubino/plugins/`:
15
+
16
+ ```ruby
17
+ # .rubino/plugins/logging.rb
18
+ Rubino.plugin do
19
+ on(:tool_execute_before) do |context|
20
+ puts "[AUDIT] Tool: #{context[:tool_name]} args: #{context[:arguments]}"
21
+ context # Return context (optionally modified)
22
+ end
23
+
24
+ on(:tool_execute_after) do |context|
25
+ puts "[AUDIT] Result: #{context[:result]&.truncated_preview}"
26
+ context
27
+ end
28
+ end
29
+ ```
30
+
31
+ ## Hook Behavior
32
+
33
+ - Hooks receive a context hash and should return it (optionally modified)
34
+ - Multiple handlers per hook are supported (executed in registration order)
35
+ - If a handler returns a Hash, it's merged into the context for the next handler
36
+ - Errors in hooks are caught and logged but don't break the main flow
37
+
38
+ ## All 38 Hooks
39
+
40
+ ### Tool Lifecycle
41
+
42
+ | Hook | Trigger | Context Keys |
43
+ |------|---------|--------------|
44
+ | `tool_execute_before` | Before any tool runs | `tool_name`, `arguments`, `session_id` |
45
+ | `tool_execute_after` | After tool completes | `tool_name`, `arguments`, `result`, `duration` |
46
+ | `tool_approval_before` | Before approval check | `tool_name`, `risk_level`, `arguments` |
47
+ | `tool_approval_after` | After approval decision | `tool_name`, `decision` (:allow/:ask/:deny) |
48
+ | `tool_result_transform` | Transform tool output | `tool_name`, `result` |
49
+
50
+ ### Shell
51
+
52
+ | Hook | Trigger | Context Keys |
53
+ |------|---------|--------------|
54
+ | `shell_env` | Inject env vars into shell | `command`, `env` (mutable hash) |
55
+ | `shell_execute_before` | Before shell command | `command`, `cwd` |
56
+ | `shell_execute_after` | After shell command | `command`, `output`, `exit_code` |
57
+
58
+ ### File Operations
59
+
60
+ | Hook | Trigger | Context Keys |
61
+ |------|---------|--------------|
62
+ | `file_read_before` | Before reading a file | `path` |
63
+ | `file_read_after` | After reading a file | `path`, `content`, `size` |
64
+ | `file_write_before` | Before writing a file | `path`, `content` |
65
+ | `file_write_after` | After writing a file | `path`, `size` |
66
+
67
+ ### Context Compaction
68
+
69
+ | Hook | Trigger | Context Keys |
70
+ |------|---------|--------------|
71
+ | `compaction_before` | Before compaction starts | `session_id`, `message_count`, `token_estimate` |
72
+ | `compaction_after` | After compaction finishes | `session_id`, `saved_tokens`, `new_session_id` |
73
+ | `compaction_context_inject` | Inject custom context into compaction summary | `session_id`, `messages` |
74
+
75
+ ### Sessions
76
+
77
+ | Hook | Trigger | Context Keys |
78
+ |------|---------|--------------|
79
+ | `session_start` | New session created | `session_id`, `model`, `source` |
80
+ | `session_end` | Session ended | `session_id`, `message_count` |
81
+ | `session_fork` | Session forked | `source_id`, `forked_id`, `at_message` |
82
+ | `session_persist` | Session state saved | `session_id`, `token_count` |
83
+
84
+ ### Messages
85
+
86
+ | Hook | Trigger | Context Keys |
87
+ |------|---------|--------------|
88
+ | `message_before` | Before message is processed | `role`, `content`, `session_id` |
89
+ | `message_after` | After message persisted | `message_id`, `role`, `content` |
90
+ | `message_stream_chunk` | Each streaming chunk | `chunk`, `session_id` |
91
+
92
+ ### Memory
93
+
94
+ | Hook | Trigger | Context Keys |
95
+ |------|---------|--------------|
96
+ | `memory_extract` | During memory extraction | `session_id`, `candidates` |
97
+ | `memory_save_before` | Before saving a memory | `kind`, `content` |
98
+ | `memory_retrieve_after` | After retrieving memories | `memories`, `context` |
99
+
100
+ ### Jobs
101
+
102
+ | Hook | Trigger | Context Keys |
103
+ |------|---------|--------------|
104
+ | `job_before` | Before job execution | `job_type`, `payload` |
105
+ | `job_after` | After job completes | `job_type`, `result` |
106
+ | `job_failed` | When job fails | `job_type`, `error`, `attempts` |
107
+
108
+ ### Model/LLM
109
+
110
+ | Hook | Trigger | Context Keys |
111
+ |------|---------|--------------|
112
+ | `model_call_before` | Before LLM API call | `model`, `messages`, `tools` |
113
+ | `model_call_after` | After LLM response | `model`, `response`, `tokens` |
114
+ | `model_response_transform` | Transform LLM response | `content`, `tool_calls` |
115
+
116
+ ### Prompt Assembly
117
+
118
+ | Hook | Trigger | Context Keys |
119
+ |------|---------|--------------|
120
+ | `prompt_assemble_before` | Before building prompt | `session_id`, `memory_context` |
121
+ | `prompt_assemble_after` | After prompt built | `messages`, `token_estimate` |
122
+
123
+ ### Agent
124
+
125
+ | Hook | Trigger | Context Keys |
126
+ |------|---------|--------------|
127
+ | `agent_switch` | Agent switched | `from`, `to` |
128
+ | `agent_route` | Input routed to agent | `input`, `agent_name` |
129
+
130
+ ### System
131
+
132
+ | Hook | Trigger | Context Keys |
133
+ |------|---------|--------------|
134
+ | `config_reload` | Config reloaded | `config` |
135
+ | `startup` | Application starting | `version` |
136
+ | `shutdown` | Application stopping | `reason` |
137
+
138
+ ## Examples
139
+
140
+ ### Auto-format after writes
141
+
142
+ ```ruby
143
+ Rubino.plugin do
144
+ on(:file_write_after) do |context|
145
+ path = context[:path]
146
+ if path.end_with?(".rb")
147
+ system("rubocop -A '#{path}' 2>/dev/null")
148
+ elsif path.end_with?(".js", ".ts")
149
+ system("prettier --write '#{path}' 2>/dev/null")
150
+ end
151
+ context
152
+ end
153
+ end
154
+ ```
155
+
156
+ ### Inject environment into shell
157
+
158
+ ```ruby
159
+ Rubino.plugin do
160
+ on(:shell_env) do |context|
161
+ context[:env]["RAILS_ENV"] ||= "development"
162
+ context[:env]["BUNDLE_GEMFILE"] ||= File.join(Dir.pwd, "Gemfile")
163
+ context
164
+ end
165
+ end
166
+ ```
167
+
168
+ ### Block dangerous operations
169
+
170
+ ```ruby
171
+ Rubino.plugin do
172
+ on(:tool_execute_before) do |context|
173
+ if context[:tool_name] == "shell"
174
+ cmd = context.dig(:arguments, "command") || ""
175
+ if cmd.match?(/rm\s+-rf\s+\/|dd\s+if=|mkfs|format/)
176
+ raise Rubino::ToolError, "Blocked dangerous command: #{cmd}"
177
+ end
178
+ end
179
+ context
180
+ end
181
+ end
182
+ ```
183
+
184
+ ### Custom telemetry
185
+
186
+ ```ruby
187
+ Rubino.plugin do
188
+ on(:model_call_after) do |context|
189
+ tokens = context[:tokens] || 0
190
+ model = context[:model]
191
+ File.open("usage.log", "a") { |f| f.puts "#{Time.now.iso8601} #{model} #{tokens}" }
192
+ context
193
+ end
194
+ end
195
+ ```