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
@@ -0,0 +1,143 @@
1
+ # Getting started
2
+
3
+ From nothing to a working first answer in about five minutes. This is the happy path; the model/key decision is made interactively, not by hand-editing YAML.
4
+
5
+ ## 1. Install
6
+
7
+ The fastest path on Linux (x86_64 / arm64) is the one-line installer. It installs a compatible Ruby via [`rv`](https://github.com/spinel-coop/rv), then the gem — all in user space, no sudo:
8
+
9
+ ```bash
10
+ curl -fsSL https://raw.githubusercontent.com/Jhonnyr97/rubino-agent/main/install.sh | bash
11
+ ```
12
+
13
+ Piping a script into your shell runs whatever it contains, so review it first if you like:
14
+
15
+ ```bash
16
+ curl -fsSL https://raw.githubusercontent.com/Jhonnyr97/rubino-agent/main/install.sh -o install.sh
17
+ less install.sh && bash install.sh
18
+ ```
19
+
20
+ The installer is idempotent (safe to re-run) and prints the exact `PATH` line for the `rubino` executable when it finishes.
21
+
22
+ **Already manage Ruby yourself?** Requirements are Ruby >= 3.1 and SQLite3; then:
23
+
24
+ ```bash
25
+ gem install rubino-agent
26
+ ```
27
+
28
+ Verify the binary is on your `PATH`:
29
+
30
+ ```bash
31
+ rubino version
32
+ ```
33
+
34
+ In a development checkout, prefix commands with `bundle exec` (`bundle exec rubino ...`).
35
+
36
+ ## 2. Run the setup wizard
37
+
38
+ ```bash
39
+ rubino setup
40
+ ```
41
+
42
+ `setup` creates the home directory (`~/.rubino`, mode `0700`), a default `config.yml` (`0600`), an `.env` template (`0600`), and initializes the SQLite database with all migrations. Then, **if no usable API key is configured and you're on a real terminal**, it launches the onboarding wizard:
43
+
44
+ ```
45
+ Welcome to rubino — let's get you connected to a model.
46
+ No API key is configured yet. Pick a provider (or press Enter to skip).
47
+
48
+ 1) OpenAI (GPT) — recommended default
49
+ 2) MiniMax (Anthropic-compatible)
50
+ 3) Anthropic (Claude)
51
+ 4) Google (Gemini)
52
+ 5) OpenAI-compatible gateway
53
+ Choose a provider [1-5, Enter to skip]: 1
54
+ Paste your OPENAI_API_KEY (input hidden; Enter to skip): ••••••••
55
+ Configured OpenAI (GPT) — recommended default with model gpt-4.1.
56
+ Saved to ~/.rubino/config.yml and ~/.rubino/.env.
57
+ ```
58
+
59
+ What the wizard does, exactly:
60
+
61
+ - Writes `model.default` and `model.provider` for the chosen provider into `config.yml`.
62
+ - Writes any provider block it needs (e.g. MiniMax sets `providers.minimax.base_url` + `anthropic_compatible: true` + `api_key: ${MINIMAX_API_KEY}`).
63
+ - Appends `KEY=value` to `~/.rubino/.env` (mode `0600`); the key is **never echoed back** and is exported into the current process so the very next message works.
64
+ - The **OpenAI-compatible gateway** option additionally asks for the gateway base URL.
65
+
66
+ The defaults written per provider:
67
+
68
+ | Choice | provider | default model | key var |
69
+ |---|---|---|---|
70
+ | OpenAI (default) | `openai` | `gpt-4.1` | `OPENAI_API_KEY` |
71
+ | MiniMax | `minimax` | `MiniMax-M2.7` | `MINIMAX_API_KEY` |
72
+ | Anthropic | `anthropic` | `claude-sonnet-4-5` | `ANTHROPIC_API_KEY` |
73
+ | Google | `google` | `gemini-2.5-pro` | `GEMINI_API_KEY` |
74
+ | OpenAI-compatible gateway | `gateway` | `auto` | `OPENAI_API_KEY` |
75
+
76
+ Press **Enter** at the provider prompt to skip. You can also configure things by hand — see [models-and-keys.md](models-and-keys.md).
77
+
78
+ ## 3. Start chatting
79
+
80
+ ```bash
81
+ rubino chat
82
+ ```
83
+
84
+ The first thing you see is a banner with the workspace, git branch, and model. The input line leads with a red `▍` rail and a clean `❯` caret; the dim status bar underneath shows the session mode, model, and context saturation. Then ask something:
85
+
86
+ ```
87
+ ▍❯ what does this project do?
88
+ default · MiniMax-M3 · ctx ~0/128k
89
+ ```
90
+
91
+ > If you skipped the wizard during `setup`, a bare `rubino chat` re-runs it before the first turn (when on a TTY). If you're piping input or using `-q`, there's no prompt to run — instead you get a clear, actionable error telling you how to set a key (see below).
92
+
93
+ ## 4. Make a first edit
94
+
95
+ Ask the agent to change something:
96
+
97
+ ```
98
+ ▍❯ add a docstring to the top of lib/foo.rb
99
+ ```
100
+
101
+ When the agent wants to run `shell` (or any approval-gated tool), it pauses and asks for your decision. Approve once, approve for the session, or deny — see [security.md](security.md) for the full approval model.
102
+
103
+ You can keep typing while the agent works: **Enter** interrupts the current turn and runs your line next; **Alt+Enter** (or `/queued <message>`) queues it to run after the turn finishes. See [commands.md](commands.md#typing-while-the-agent-is-working).
104
+
105
+ ## 5. Exit and resume
106
+
107
+ Type `exit` (or `/exit`, Ctrl+D, or a double Ctrl+C) to end the session. On exit you get a resume hint:
108
+
109
+ ```
110
+ Resume with: rubino chat --resume "my session title"
111
+ ```
112
+
113
+ Resuming:
114
+
115
+ - `rubino chat` — a **bare** interactive chat auto-resumes your most recent resumable session and replays its history.
116
+ - `rubino chat --new` — force a fresh session instead.
117
+ - `rubino chat --continue` (`-c`) — resume the most recent session explicitly.
118
+ - `rubino chat --resume <id|title>` (`-r`) — resume a specific session.
119
+
120
+ In-chat, `/sessions` lists recent sessions and resumes one in place, and `/new` starts a fresh one without leaving the REPL.
121
+
122
+ ## If the first message fails
123
+
124
+ A brand-new user with no key used to see ~80 seconds of silent retries then an empty answer. That trap is fixed: the run now fails fast with guidance. In a non-interactive context (e.g. `rubino prompt "hi"` with no key) you'll see:
125
+
126
+ ```
127
+ No API key configured for provider 'openai' (model openai/gpt-4.1).
128
+ Set it up one of these ways:
129
+ • run `rubino setup` for a guided first-run setup, or
130
+ • add OPENAI_API_KEY=<your-key> to ~/.rubino/.env, or
131
+ • set providers.openai.api_key in ~/.rubino/config.yml.
132
+ ```
133
+
134
+ (The shipped default model `openai/gpt-4.1` resolves to OpenRouter in ruby_llm's registry; this is why a first run without a key or the right provider fails. The cleanest fix is `rubino setup`. See [models-and-keys.md](models-and-keys.md) and [troubleshooting.md](troubleshooting.md).)
135
+
136
+ Run `rubino doctor` at any time to check config, the resolved provider, credentials, and database health.
137
+
138
+ ## Next steps
139
+
140
+ - [Models & keys](models-and-keys.md) — per-provider setup blocks and the default→OpenRouter note.
141
+ - [Commands](commands.md) — every CLI subcommand and slash command.
142
+ - [Configuration](configuration.md) — full reference, env vars, precedence.
143
+ - [Tools](tools.md) — what the agent can do and how each tool is gated.
data/docs/jobs.md ADDED
@@ -0,0 +1,332 @@
1
+ # Jobs
2
+
3
+ Background work in rubino is split into two surfaces that share the `Rubino::Jobs::*` namespace but operate independently:
4
+
5
+ 1. **Internal background queue** — async side-effects the agent enqueues for itself (memory extraction, context compaction, session summarization, retention sweeps).
6
+ 2. **Cron jobs** — user-defined schedules that fire fresh agent runs on a cron expression, with optional webhook delivery on completion. HTTP surface lives at [`/v1/jobs`](api/v1.md#cron-jobs).
7
+
8
+ This doc describes both, plus the **Backend Adapter contract** that the queue and the scheduler are designed to be plugged into so the gem can be hosted on Sidekiq / SolidQueue / GoodJob / ActiveJob without rewriting handlers.
9
+
10
+ ---
11
+
12
+ ## Internal background queue
13
+
14
+ ### Purpose
15
+
16
+ Defer slow or out-of-band work off the request and chat paths. Anything an agent doesn't need to block on — extracting memories from a finished turn, compacting a session that crossed the threshold, GC'ing ended sessions — goes through the queue.
17
+
18
+ ### Storage (default `Sqlite` backend)
19
+
20
+ ```sql
21
+ CREATE TABLE jobs (
22
+ id text PRIMARY KEY, -- uuid
23
+ type text NOT NULL, -- Jobs::Registry key
24
+ status text NOT NULL, -- queued | running | completed | failed | dead
25
+ priority integer NOT NULL, -- lower runs first
26
+ payload_json text NOT NULL, -- JSON-serialised hash
27
+ attempts integer NOT NULL,
28
+ max_attempts integer NOT NULL,
29
+ run_at text NOT NULL, -- iso8601, "not before"
30
+ locked_at text,
31
+ locked_by text, -- worker_id
32
+ last_error text,
33
+ created_at text NOT NULL,
34
+ updated_at text NOT NULL
35
+ );
36
+
37
+ CREATE TABLE job_runs (
38
+ id text PRIMARY KEY, -- uuid, one row per execution attempt
39
+ job_id text NOT NULL,
40
+ status text NOT NULL,
41
+ started_at text,
42
+ finished_at text,
43
+ error text
44
+ );
45
+ ```
46
+
47
+ ### Lifecycle
48
+
49
+ ```
50
+ enqueue ──► (status=queued) ──► dequeue (worker locks row)
51
+
52
+
53
+ handler.perform(payload)
54
+
55
+ ┌──────────┴───────────┐
56
+ ▼ ▼
57
+ Queue#complete! Queue#fail!(error:)
58
+ status=completed attempts += 1
59
+ retry_at = now + backoff·attempts
60
+ status=queued (or dead at max_attempts)
61
+ ```
62
+
63
+ Retry uses linear backoff: `retry_backoff_seconds * attempts`. After `max_attempts` the row stays at `status=dead` for inspection — nothing reaps it automatically.
64
+
65
+ ### Execution modes
66
+
67
+ `config.jobs_mode` selects how enqueued jobs actually run:
68
+
69
+ | Mode | Behavior | When to use |
70
+ |---|---|---|
71
+ | `inline` | `Queue#enqueue` runs the handler synchronously in the same call stack. A failed inline job is marked terminal (`failed`) rather than re-queued, since nothing drains it. | dev, tests, smoke runs |
72
+ | `manual` | enqueue only — nothing runs until `rubino jobs process` is invoked. | air-gapped or CI batch flows |
73
+ | `worker` | a long-running `rubino jobs worker` polls and dequeues. | production single-process |
74
+
75
+ The worker is a single-threaded poll loop with `SIGINT`/`SIGTERM` graceful stop. It is not safe to run more than one worker per SQLite file — locking is row-level but `WAL` contention will dominate. Multi-process scaling is what the [Backend Adapter](#backend-adapter-planned-design) section is for.
76
+
77
+ ### Handler contract
78
+
79
+ A handler is any class that responds to `#perform(payload)`. The Registry maps a type string → handler class:
80
+
81
+ ```ruby
82
+ module Rubino
83
+ module Jobs
84
+ module Handlers
85
+ class MyJob
86
+ def perform(payload)
87
+ # payload comes back as a symbol-keyed Hash
88
+ session_id = payload[:session_id]
89
+ # ...
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+
96
+ Rubino::Jobs::Registry.register("MyJob", Rubino::Jobs::Handlers::MyJob)
97
+ ```
98
+
99
+ Enqueue:
100
+
101
+ ```ruby
102
+ Rubino::Jobs::Queue.new.enqueue("MyJob", session_id: "abc")
103
+ ```
104
+
105
+ ### Built-in handlers
106
+
107
+ | Type | Handler | Payload | Side-effect |
108
+ |---|---|---|---|
109
+ | `ExtractMemoryJob` | `Handlers::ExtractMemoryJob` | `{session_id}` | `Memory::Extractor#extract_from_session` |
110
+ | `CompactSessionJob` | `Handlers::CompactSessionJob` | `{session_id}` | `Context::Compressor#compact!` |
111
+ | `SummarizeSessionJob` | `Handlers::SummarizeSessionJob` | `{session_id}` | `Context::SummaryBuilder#build_and_save!` |
112
+ | `CleanupSessionsJob` | `Handlers::CleanupSessionsJob` | `{retention_days?}` | deletes `sessions` rows with `status="ended"` older than retention (default 30d) |
113
+ | `DistillSkillJob` | `Handlers::DistillSkillJob` | `{session_id}` | post-turn skill distillation — one aux-LLM call distils a tool-heavy turn into a reusable `SKILL.md` (gated on `skills.auto_distill`; see [skills.md](skills.md#creating-skills)) |
114
+
115
+ ### CLI
116
+
117
+ ```bash
118
+ rubino jobs list # show recent rows from jobs table
119
+ rubino jobs process # drain queued rows once (uses Runner#run_pending)
120
+ rubino jobs worker # start the polling worker (long-running)
121
+ ```
122
+
123
+ In an interactive chat, `/jobs` shows the same list (with status counts) and
124
+ `/jobs <id>` one job in full, including its last error — see
125
+ [commands.md](commands.md#jobs-in-chat-jobs). Running jobs stays CLI-only.
126
+
127
+ ---
128
+
129
+ ## Cron jobs
130
+
131
+ User-defined cron schedules, persisted in the `cron_jobs` table, dispatched by `Jobs::Scheduler` — a process-wide singleton wrapping `rufus-scheduler`.
132
+
133
+ Each cron tick:
134
+
135
+ 1. Creates a fresh `Session` (source=`"cron"`).
136
+ 2. Creates a `Run` stamped with `cron_job_id`.
137
+ 3. Hands the run to `Run::Executor#start`.
138
+ 4. On completion: optionally posts a payload to `RUBINO_WEBHOOK_URL` when `deliver: "webhook"`.
139
+
140
+ Configuration is HTTP-only — there is no YAML loader for cron jobs. Full request/response shapes are in [`docs/api/v1.md`](api/v1.md#cron-jobs); the routes are:
141
+
142
+ ```
143
+ POST /v1/jobs # create
144
+ GET /v1/jobs # list
145
+ GET /v1/jobs/:id # show
146
+ PATCH /v1/jobs/:id # update
147
+ DELETE /v1/jobs/:id # delete + unschedule
148
+ POST /v1/jobs/:id/pause # disable + unschedule
149
+ POST /v1/jobs/:id/resume # enable + reschedule
150
+ POST /v1/jobs/:id/trigger # fire once now
151
+ ```
152
+
153
+ ### Scheduler boot
154
+
155
+ `Jobs::Scheduler.instance.load_all!` is called once at server boot. It loads every `enabled: true` row from `cron_jobs` and registers a rufus cron handle for each.
156
+
157
+ ```ruby
158
+ scheduler = Rubino::Jobs::Scheduler.instance
159
+ scheduler.load_all! # at server boot
160
+ scheduler.schedule(job_row) # after POST /v1/jobs
161
+ scheduler.unschedule(job_id) # after DELETE
162
+ scheduler.trigger(job_id) # one-shot
163
+ scheduler.shutdown! # at server stop
164
+ ```
165
+
166
+ ### Webhook delivery
167
+
168
+ `Jobs::WebhookDelivery` is a thin Faraday + faraday-retry client. Best-effort: every failure path (no URL, non-2xx, transport error) is logged and counted, never raised.
169
+
170
+ | Setting | Value |
171
+ |---|---|
172
+ | URL | `RUBINO_WEBHOOK_URL` env (single URL for the whole process) |
173
+ | Timeout | 10s |
174
+ | Retry | 2 attempts, 0.5s initial interval, exponential backoff factor 2 |
175
+ | Retry triggers | `Faraday::TimeoutError`, `Faraday::ConnectionFailed` |
176
+ | Payload | `{ job_id, job_name, run_id, status, session_id }` |
177
+ | Metrics | `webhook_deliveries_total{outcome="ok"|"http_error"|"error"}` |
178
+
179
+ Per-job webhook URLs and signed payloads are planned.
180
+
181
+ ### Multi-process limitation
182
+
183
+ Because rufus lives in the Ruby heap, **every Puma worker** would run **every cron tick** if you scaled `rubino server` horizontally. The scheduler currently ships as single-instance only. The [Backend Adapter](#backend-adapter-planned-design) section sketches what cluster-safe scheduling looks like.
184
+
185
+ ---
186
+
187
+ ## Backend Adapter (planned design)
188
+
189
+ The internal queue today is hardwired to SQLite via Sequel. For real production deployments — multi-process Puma, k8s pods, heavy fanout — the natural move is to plug Sidekiq / SolidQueue / GoodJob / Resque underneath without rewriting any handler.
190
+
191
+ The contract below is **not implemented yet**. It is documented so that:
192
+
193
+ 1. Today's `Jobs::Queue` keeps a stable public method set (`enqueue / find / list / pending_count`) that can become the adapter facade later.
194
+ 2. Anyone writing a new handler today knows what *not* to depend on (DB queries on `:jobs`, transaction semantics, Sequel handles).
195
+ 3. When the adapter lands, the migration is a config flip, not a rewrite.
196
+
197
+ ### `Jobs::Backend` contract
198
+
199
+ ```ruby
200
+ module Rubino
201
+ module Jobs
202
+ # Single point of contact between Jobs::Queue (a thin facade once this
203
+ # ships) and whatever runs the work in production.
204
+ #
205
+ # Implementations MUST be thread-safe AND process-safe. The agent
206
+ # assumes nothing about how work is durably stored or dispatched —
207
+ # only that the four methods below behave per their contract.
208
+ module Backend
209
+ # Schedule a job for execution.
210
+ #
211
+ # @param type [String] handler type (matches Jobs::Registry key)
212
+ # @param payload [Hash] JSON-serialisable. No symbols on the wire,
213
+ # no Time objects, no Sequel rows.
214
+ # @param priority [Integer] lower runs first; semantics are best-effort.
215
+ # @param run_at [Time, nil] not-before timestamp; nil means ASAP.
216
+ # @return [String] backend-specific job id, round-trippable via #find.
217
+ def enqueue(type:, payload:, priority: 100, run_at: nil); end
218
+
219
+ # Look up a single job by the id returned from #enqueue.
220
+ # @return [Hash, nil] {id:, type:, status:, attempts:, last_error:, ...}
221
+ def find(id); end
222
+
223
+ # Snapshot for /v1/health and `rubino jobs list`.
224
+ # @return [Hash] e.g. {queued: 4, running: 1, dead: 0, completed: 132}
225
+ def stats; end
226
+
227
+ # Drain up to `limit` queued jobs in-process. Used by `inline` mode
228
+ # and tests. Adapters with no in-process executor (Sidekiq client-only
229
+ # mode) MAY raise NotImplementedError.
230
+ def drain!(limit: 100); end
231
+
232
+ # Optional. GC for completed/dead rows. Operators call this via
233
+ # `rubino jobs cleanup`. Implementations without a "completed"
234
+ # state (e.g. fire-and-forget transports) may no-op.
235
+ def purge_completed!(older_than:); end
236
+ end
237
+ end
238
+ end
239
+ ```
240
+
241
+ Wire-up:
242
+
243
+ ```ruby
244
+ Rubino.configure do |c|
245
+ c.jobs_backend = Rubino::Jobs::Backend::Sqlite.new # current default
246
+ # c.jobs_backend = MyApp::SidekiqAdapter.new # opt-in
247
+ end
248
+ ```
249
+
250
+ `Jobs::Queue.new` becomes a façade that delegates to `Rubino.configuration.jobs_backend`. No handler changes; no API changes; `inline` / `manual` / `worker` modes collapse into whatever the adapter exposes.
251
+
252
+ ### Sketch adapters (NOT shipped yet)
253
+
254
+ Each row below shows the **only file** an integrator would have to write. Handlers stay the same Ruby object with `#perform(payload)`.
255
+
256
+ | Adapter | Underlying lib | Transport | Retry / DLQ | Cron |
257
+ |---|---|---|---|---|
258
+ | `Backend::Sqlite` | Sequel | SQLite table polling | linear backoff, `dead` status | rufus (in-process) |
259
+ | `Backend::Sidekiq` | sidekiq | Redis | sidekiq retry + DLQ | sidekiq-cron |
260
+ | `Backend::SolidQueue` | solid_queue | ActiveRecord (no Redis) | SolidQueue retry | SolidQueue recurring jobs |
261
+ | `Backend::GoodJob` | good_job | Postgres LISTEN/NOTIFY | GoodJob retry | GoodJob cron |
262
+ | `Backend::ActiveJob` | activejob | host app's queue_adapter | adapter-dependent | adapter-dependent |
263
+
264
+ Indicative shape (informational only):
265
+
266
+ ```ruby
267
+ # This file would live in the host application — NOT in the gem.
268
+ class MyApp::Jobs::SidekiqAdapter
269
+ def enqueue(type:, payload:, priority: 100, run_at: nil)
270
+ handler = Rubino::Jobs::Registry.handler_for(type) or raise "unknown type: #{type}"
271
+ # Wrap the handler once so Sidekiq has a class with `perform`.
272
+ job_class = MyApp::Jobs::RubinoSidekiqWrapper
273
+ args = [type, JSON.generate(payload)]
274
+ if run_at
275
+ job_class.perform_at(run_at, *args)
276
+ else
277
+ job_class.perform_async(*args)
278
+ end
279
+ end
280
+ # find/stats/drain!/purge_completed! similarly
281
+ end
282
+ ```
283
+
284
+ The wrapper class on the Sidekiq side just dispatches:
285
+
286
+ ```ruby
287
+ class MyApp::Jobs::RubinoSidekiqWrapper
288
+ include Sidekiq::Job
289
+ def perform(type, payload_json)
290
+ payload = JSON.parse(payload_json, symbolize_names: true)
291
+ Rubino::Jobs::Registry.handler_for(type).new.perform(payload)
292
+ end
293
+ end
294
+ ```
295
+
296
+ ### Cron scheduler adapter (parallel design)
297
+
298
+ Cron has the same shape. Today rufus is a singleton wrapping in-process callbacks; tomorrow a `Scheduler::Backend` decides whether the tick happens here or somewhere else.
299
+
300
+ ```ruby
301
+ module Rubino::Jobs::Scheduler::Backend
302
+ def schedule(job); end # job row from cron_jobs
303
+ def unschedule(id); end
304
+ def trigger(id); end # one-shot now
305
+ def load_all!; end # boot
306
+ def shutdown!; end
307
+ end
308
+ ```
309
+
310
+ | Adapter | What ticks the cron |
311
+ |---|---|
312
+ | `Scheduler::Backend::Rufus` | rufus-scheduler in-process (current) |
313
+ | `Scheduler::Backend::SidekiqCron` | sidekiq-cron / sidekiq-scheduler |
314
+ | `Scheduler::Backend::SolidCron` | SolidQueue recurring jobs table |
315
+ | `Scheduler::Backend::Kubernetes` | host cluster's `CronJob` POSTs `/v1/jobs/:id/trigger` on schedule; rubino never schedules anything itself |
316
+
317
+ In every case the HTTP surface (`/v1/jobs`) does not change. Only **who actually fires the tick** changes.
318
+
319
+ ---
320
+
321
+ ## Non-goals
322
+
323
+ - **Pluggable backends are designed, not shipped.** The default and only backend is `Sqlite`. The `Jobs::Backend` module above does not exist in the code yet — `Jobs::Queue` is the SQLite implementation directly.
324
+ - **No cluster-safe cron.** The rufus scheduler is single-instance only. Running more than one `rubino server` in front of the same DB will multi-fire every cron job.
325
+ - **No per-job webhook URLs**, no payload signing, no signed JWT-style retry tokens. One URL per process via `RUBINO_WEBHOOK_URL`.
326
+ - **No queue web UI.** `rubino jobs list` and `/v1/jobs` are the only inspection surfaces.
327
+ - **No fan-out, no per-tenant queues, no priority classes.** Priority is a single integer column, best-effort.
328
+ - **No automatic dead-row GC.** Failed-past-max-attempts rows stay at `status=dead` until an operator removes them.
329
+
330
+ ## Why a contract, not just a config flag
331
+
332
+ The handler classes are the API. As long as `perform(payload)` is stable and idempotent, swapping the backend is a deploy concern, not a rewrite. Documenting the contract now (and keeping `Jobs::Queue`'s public method surface aligned with `Backend`) is what keeps "let's move to Sidekiq" a one-day task instead of a one-month refactor.
data/docs/mcp.md ADDED
@@ -0,0 +1,128 @@
1
+ # MCP Integration
2
+
3
+ > **Status: EXPERIMENTAL.** stdio servers are wired end-to-end (connect at chat boot, tools registered, `doctor`/`tools`/in-chat `/mcp` surfaces). `sse`/`streamable` configs are forwarded to [ruby_llm-mcp](https://github.com/patvice/ruby_llm-mcp) but less battle-tested, and OAuth is **not implemented** on the rubino side (see below). Don't depend on it in production yet.
4
+
5
+ rubino supports the [Model Context Protocol](https://modelcontextprotocol.io/) via [ruby_llm-mcp](https://github.com/patvice/ruby_llm-mcp).
6
+
7
+ ## Configuration
8
+
9
+ Configuring at least one server under `mcp.servers` in `config.yml` **is the opt-in** — there is no separate feature flag to flip. Set `mcp.enabled: false` to switch MCP off without deleting the server definitions.
10
+
11
+ ```yaml
12
+ mcp:
13
+ # enabled: false # optional kill switch; defaults to true when servers exist
14
+ servers:
15
+ # Local server via stdio
16
+ filesystem:
17
+ transport: stdio
18
+ command: "npx"
19
+ args: ["@modelcontextprotocol/server-filesystem", "/path/to/project"]
20
+ env:
21
+ DEBUG: "1"
22
+
23
+ # Remote server via SSE
24
+ remote_api:
25
+ transport: sse
26
+ url: "https://mcp.example.com/sse"
27
+ headers:
28
+ Authorization: "Bearer {env:MCP_TOKEN}"
29
+
30
+ # Remote server via streamable HTTP
31
+ streaming_api:
32
+ transport: streamable
33
+ url: "https://mcp.example.com/api"
34
+ timeout: 15000
35
+ ```
36
+
37
+ ## Transport Types
38
+
39
+ | Transport | Use Case | Config |
40
+ |-----------|----------|--------|
41
+ | `stdio` | Local MCP servers, CLI tools | `command`, `args`, `env` |
42
+ | `sse` | Web-based servers with Server-Sent Events | `url`, `headers` |
43
+ | `streamable` | HTTP servers with streaming support | `url`, `headers`, `oauth` |
44
+
45
+ ## How It Works
46
+
47
+ 1. At chat boot (and in `rubino tools`), `MCP::Manager` connects to all configured servers — best-effort: a server that fails to start prints a warning and is skipped, it never blocks the session
48
+ 2. Each server's tools are wrapped in `MCPToolWrapper` (adapts to `Tools::Base` interface), forwarding the server-declared input schema so the model calls them with the right argument names
49
+ 3. Wrapped tools are registered in `Tools::Registry` with a prefix (`servername_toolname`)
50
+ 4. The agent can use MCP tools like any built-in tool; a failed MCP call (including a server-side argument rejection) surfaces as an `Error: …` tool result and renders ✗ like any failed built-in tool
51
+ 5. `ruby_llm-mcp`'s own log lines (including everything a stdio server prints on its stderr) go to `<home>/logs/mcp.log`, never to stdout — one-shot `rubino prompt` output stays machine-readable
52
+
53
+ MCP tools are dynamic — they come from whatever servers you configure — so they are not part of the drift-checked built-in tool list in [tools.md](tools.md) and have no `tools.<key>` config gate; disable a server (`/mcp <server> off` for the session, or set `mcp.enabled: false`) to remove its tools.
54
+
55
+ ## Per-Agent Scoping
56
+
57
+ Control which MCP servers each agent can access in `config.yml`:
58
+
59
+ ```yaml
60
+ agents:
61
+ explore:
62
+ mcp_servers: ["filesystem"] # Only filesystem MCP
63
+ build:
64
+ mcp_servers: all # All MCP servers (default)
65
+ plan:
66
+ mcp_servers: [] # No MCP tools
67
+ ```
68
+
69
+ An agent with no `mcp_servers` key sees every server. The YAML string `all` is normalized to `:all`. The scoping is enforced in `Agent::Definition#resolved_tools` — the single seam every consumer of an agent's tool set (chat lifecycle, prompt assembler) goes through — so a scoped agent's model request simply does not contain the out-of-scope servers' tool definitions.
70
+
71
+ In code (an explicit value here wins over config):
72
+
73
+ ```ruby
74
+ Rubino::Agent::Definition.new(
75
+ name: "secure_agent",
76
+ mcp_servers: ["internal_api"] # Only this server's tools
77
+ )
78
+ ```
79
+
80
+ ## Authentication
81
+
82
+ Remote-server credentials are passed through config: use `headers` (e.g. `Authorization: "Bearer {env:MCP_TOKEN}"`) or the server process `env` for stdio servers.
83
+
84
+ An `oauth` hash on a `streamable` server is forwarded verbatim to `ruby_llm-mcp` — rubino itself implements **no** OAuth flow: there is no PKCE/browser handshake and no rubino-side token storage (no `~/.rubino/oauth_tokens.json`). Whatever OAuth behavior you get is whatever your installed `ruby_llm-mcp` version provides; treat it as not yet supported.
85
+
86
+ ## Managing from Chat
87
+
88
+ `/mcp` is the in-chat management surface ([commands.md](commands.md#mcp-servers-mcp)):
89
+
90
+ ```
91
+ /mcp # server list: name, transport, reachability, tool count
92
+ /mcp <server> # drill-in: transport + command/url, health, registered tools, last start error
93
+ /mcp <server> off # stop the client and deregister its tools for this session
94
+ /mcp <server> on # (re)start the client and register its tools
95
+ /mcp reload # re-read config.yml and reconnect every server (no chat restart needed)
96
+ ```
97
+
98
+ `off`/`on` are session-scoped — config is untouched. `/mcp reload` is how a server added to `config.yml` mid-session becomes usable. When servers are configured, `/status` includes an `mcp` line (`2 servers · 1 reachable · 14 tools`).
99
+
100
+ ## Manual Management
101
+
102
+ ```ruby
103
+ # Start all servers
104
+ manager = Rubino::MCP::Manager.new
105
+ manager.start_all!
106
+
107
+ # Get tools for a specific agent (mcp_servers scoping applied)
108
+ tools = agent_definition.resolved_tools
109
+
110
+ # Health check
111
+ manager.health_check
112
+ # => [{ name: "filesystem", alive: true }, { name: "api", alive: false }]
113
+
114
+ # Stop a server
115
+ manager.stop_server("filesystem")
116
+
117
+ # Stop all
118
+ manager.stop_all!
119
+ ```
120
+
121
+ ## CLI
122
+
123
+ ```bash
124
+ rubino doctor # "Optional (MCP servers, experimental)" section: per-server reachability.
125
+ # Informational only — an unreachable MCP server never fails doctor.
126
+ rubino tools # "MCP Tools (experimental)" section: prefixed servername_toolname rows
127
+ # per server, after the built-in table.
128
+ ```