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,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+ require "digest"
5
+ require "base64"
6
+ require "oauth2"
7
+
8
+ module Rubino
9
+ module OAuth
10
+ # Abstract OAuth 2.0 provider. Subclasses declare endpoints + default scopes
11
+ # and implement #fetch_account_info to populate account_id/account_email
12
+ # after a successful token exchange.
13
+ #
14
+ # Configured per-provider with client_id, client_secret, scopes from
15
+ # rubino.yml. PKCE (S256) is enabled by default for the auth_code flow.
16
+ # The agent is stateless across the redirect: the client persists the
17
+ # returned +state+ and +code_verifier+ between connect and callback.
18
+ class Provider
19
+ attr_reader :client_id, :client_secret, :scopes, :metadata
20
+
21
+ def self.id
22
+ raise NotImplementedError
23
+ end
24
+
25
+ def self.display_name
26
+ id.to_s.capitalize
27
+ end
28
+
29
+ def self.site
30
+ raise NotImplementedError
31
+ end
32
+
33
+ def self.authorize_path
34
+ raise NotImplementedError
35
+ end
36
+
37
+ def self.token_path
38
+ raise NotImplementedError
39
+ end
40
+
41
+ def self.default_scopes
42
+ []
43
+ end
44
+
45
+ def initialize(client_id:, client_secret:, scopes: nil, metadata: {})
46
+ @client_id = client_id
47
+ @client_secret = client_secret
48
+ @scopes = (scopes || self.class.default_scopes).map(&:to_s)
49
+ @metadata = metadata
50
+ end
51
+
52
+ def id
53
+ self.class.id
54
+ end
55
+
56
+ # Build the authorize URL the client must redirect the user to.
57
+ #
58
+ # The returned +state+ and +code_verifier+ MUST be persisted by the
59
+ # caller and replayed on the callback — rubino keeps no per-flow
60
+ # session.
61
+ #
62
+ # @param redirect_uri [String] absolute callback URL registered with the provider
63
+ # @param scopes [Array<String>, nil] overrides the instance default scopes when present
64
+ # @param extra [Hash] additional query parameters appended to the authorize URL
65
+ # @return [Hash] with keys +:authorize_url+ (String), +:state+ (String,
66
+ # urlsafe base64), +:code_verifier+ (String, PKCE verifier)
67
+ def build_authorize_request(redirect_uri:, scopes: nil, extra: {})
68
+ state = SecureRandom.urlsafe_base64(32)
69
+ code_verifier = SecureRandom.urlsafe_base64(64)
70
+ code_challenge = pkce_challenge(code_verifier)
71
+
72
+ url = oauth2_client.auth_code.authorize_url(
73
+ redirect_uri: redirect_uri,
74
+ scope: Array(scopes || @scopes).join(scope_separator),
75
+ state: state,
76
+ code_challenge: code_challenge,
77
+ code_challenge_method: "S256",
78
+ **extra
79
+ )
80
+
81
+ { authorize_url: url, state: state, code_verifier: code_verifier }
82
+ end
83
+
84
+ # Exchange the authorization code for tokens.
85
+ #
86
+ # @param code [String] authorization code returned by the provider
87
+ # @param redirect_uri [String] same redirect_uri used in {#build_authorize_request}
88
+ # @param code_verifier [String] PKCE verifier paired with the original challenge
89
+ # @return [Hash] with keys +:access_token+ (String), +:refresh_token+
90
+ # (String, nil), +:expires_at+ (String ISO8601 UTC, nil), +:scopes+
91
+ # (Array<String>)
92
+ def exchange_code(code:, redirect_uri:, code_verifier:)
93
+ token = oauth2_client.auth_code.get_token(
94
+ code,
95
+ redirect_uri: redirect_uri,
96
+ code_verifier: code_verifier
97
+ )
98
+ normalize(token)
99
+ end
100
+
101
+ def refresh(refresh_token)
102
+ token = OAuth2::AccessToken.new(oauth2_client, "", refresh_token: refresh_token)
103
+ normalize(token.refresh!)
104
+ end
105
+
106
+ # Provider-specific call to /userinfo (or equivalent) using the access
107
+ # token.
108
+ #
109
+ # @param _access_token [String]
110
+ # @return [Hash] with keys +:account_id+ (String), +:account_email+
111
+ # (String, nil), +:metadata+ (Hash)
112
+ def fetch_account_info(_access_token)
113
+ raise NotImplementedError
114
+ end
115
+
116
+ private
117
+
118
+ def oauth2_client
119
+ @oauth2_client ||= OAuth2::Client.new(
120
+ @client_id,
121
+ @client_secret,
122
+ site: self.class.site,
123
+ authorize_url: self.class.authorize_path,
124
+ token_url: self.class.token_path
125
+ )
126
+ end
127
+
128
+ def scope_separator
129
+ " "
130
+ end
131
+
132
+ def normalize(token)
133
+ expires_at = token.expires_at ? Time.at(token.expires_at).utc.iso8601 : nil
134
+ {
135
+ access_token: token.token,
136
+ refresh_token: token.refresh_token,
137
+ expires_at: expires_at,
138
+ scopes: (token.params["scope"] || @scopes.join(" ")).to_s.split(/[\s,]+/).reject(&:empty?)
139
+ }
140
+ end
141
+
142
+ # PKCE S256 challenge: SHA-256 of the verifier, base64url-encoded with
143
+ # padding stripped (RFC 7636 §4.2 — providers reject "=" padding).
144
+ def pkce_challenge(verifier)
145
+ Base64.urlsafe_encode64(Digest::SHA256.digest(verifier), padding: false)
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubino
4
+ module OAuth
5
+ # Process-wide registry of configured OAuth providers. Mutex-protected so
6
+ # +register+ / +reset!+ are safe under concurrent boot or reload.
7
+ #
8
+ # Hydrated from +oauth.providers.*+ in Rubino.configuration; only ids
9
+ # listed in {BUILTINS} are considered, and any section missing both
10
+ # +client_id+ and +client_secret+ is silently skipped so a partial config
11
+ # never raises at boot (it just hides the provider from
12
+ # +/v1/oauth/providers+).
13
+ module Registry
14
+ BUILTINS = {
15
+ github: "Rubino::OAuth::Provider::Github",
16
+ google: "Rubino::OAuth::Provider::Google"
17
+ }.freeze
18
+
19
+ class << self
20
+ def register(id, instance)
21
+ mutex.synchronize { providers[id.to_sym] = instance }
22
+ instance
23
+ end
24
+
25
+ # @param id [String, Symbol]
26
+ # @return [Provider]
27
+ # @raise [Rubino::NotFoundError] when no provider is registered for +id+
28
+ def fetch(id)
29
+ providers[id.to_sym] or raise NotFoundError.new("oauth_provider", id)
30
+ end
31
+
32
+ def fetch_or_nil(id)
33
+ providers[id.to_sym]
34
+ end
35
+
36
+ def all
37
+ providers.values
38
+ end
39
+
40
+ def ids
41
+ providers.keys
42
+ end
43
+
44
+ def reset!
45
+ mutex.synchronize { providers.clear }
46
+ end
47
+
48
+ # Hydrate from the loaded Rubino configuration. Reads oauth.providers.*
49
+ # sections; for each id matching a BUILTIN, instantiates and registers
50
+ # using its declared client_id/client_secret/scopes. Replaces any
51
+ # previously registered providers ({#reset!} runs first).
52
+ #
53
+ # @param configuration [#dig] anything responding to +dig("oauth", "providers")+
54
+ # @return [Array<Provider>] providers registered by this call
55
+ def load_from_config!(configuration = Rubino.configuration)
56
+ reset!
57
+ oauth_cfg = configuration.dig("oauth", "providers") || {}
58
+ oauth_cfg.each do |id, cfg|
59
+ klass_name = BUILTINS[id.to_sym]
60
+ next unless klass_name
61
+ next unless cfg["client_id"] && cfg["client_secret"]
62
+
63
+ klass = Object.const_get(klass_name)
64
+ register(id, klass.new(
65
+ client_id: cfg["client_id"],
66
+ client_secret: cfg["client_secret"],
67
+ scopes: cfg["scopes"],
68
+ metadata: cfg.reject { |k, _| %w[client_id client_secret scopes].include?(k) }
69
+ ))
70
+ end
71
+ all
72
+ end
73
+
74
+ private
75
+
76
+ def providers
77
+ @providers ||= {}
78
+ end
79
+
80
+ def mutex
81
+ @mutex ||= Mutex.new
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+ require "base64"
5
+
6
+ module Rubino
7
+ module OAuth
8
+ # AES-256-GCM symmetric encryption for OAuth tokens at rest.
9
+ #
10
+ # Key supplied via RUBINO_ENCRYPTION_KEY env (32 raw bytes encoded as
11
+ # standard base64). Generate one with:
12
+ # ruby -rsecurerandom -rbase64 -e 'puts Base64.strict_encode64(SecureRandom.random_bytes(32))'
13
+ #
14
+ # Wire format is Base64(IV || ciphertext || tag) with a 12-byte IV and a
15
+ # 16-byte GCM auth tag. {.from_env} raises {KeyMissingError} when the env
16
+ # var is missing or not a 32-byte key; {#decrypt} raises
17
+ # {InvalidCiphertextError} on tampered or truncated payloads.
18
+ class TokenEncryptor
19
+ CIPHER = "aes-256-gcm"
20
+ IV_LEN = 12
21
+ TAG_LEN = 16
22
+
23
+ class KeyMissingError < Rubino::Error
24
+ end
25
+
26
+ class InvalidCiphertextError < Rubino::Error
27
+ end
28
+
29
+ # Build an encryptor using the key in RUBINO_ENCRYPTION_KEY.
30
+ #
31
+ # @return [TokenEncryptor]
32
+ # @raise [KeyMissingError] if the env var is unset, empty, or does not
33
+ # decode to exactly 32 bytes
34
+ def self.from_env
35
+ raw = ENV.fetch("RUBINO_ENCRYPTION_KEY", nil)
36
+ raise KeyMissingError, "RUBINO_ENCRYPTION_KEY not set" if raw.nil? || raw.empty?
37
+
38
+ key = Base64.strict_decode64(raw)
39
+ raise KeyMissingError, "RUBINO_ENCRYPTION_KEY must decode to 32 bytes" unless key.bytesize == 32
40
+
41
+ new(key)
42
+ end
43
+
44
+ def initialize(key)
45
+ raise ArgumentError, "key must be 32 bytes" unless key.bytesize == 32
46
+
47
+ @key = key
48
+ end
49
+
50
+ # @param plaintext [String, nil]
51
+ # @return [String, nil] Base64(IV || ciphertext || tag), or nil when
52
+ # plaintext is nil (so nullable token columns round-trip unchanged)
53
+ def encrypt(plaintext)
54
+ return nil if plaintext.nil?
55
+
56
+ cipher = OpenSSL::Cipher.new(CIPHER).encrypt
57
+ cipher.key = @key
58
+ iv = cipher.random_iv
59
+ ciphertext = cipher.update(plaintext.to_s) + cipher.final
60
+ Base64.strict_encode64(iv + ciphertext + cipher.auth_tag)
61
+ end
62
+
63
+ # @param payload [String, nil] a value previously returned by {#encrypt}
64
+ # @return [String, nil] the original plaintext, or nil when payload is nil
65
+ # @raise [InvalidCiphertextError] if the payload is too short or the GCM
66
+ # auth tag does not verify (tampering, wrong key, truncation)
67
+ def decrypt(payload)
68
+ return nil if payload.nil?
69
+
70
+ bytes = Base64.strict_decode64(payload)
71
+ raise InvalidCiphertextError, "payload too short" if bytes.bytesize <= IV_LEN + TAG_LEN
72
+
73
+ iv = bytes.byteslice(0, IV_LEN)
74
+ tag = bytes.byteslice(-TAG_LEN, TAG_LEN)
75
+ ciphertext = bytes.byteslice(IV_LEN, bytes.bytesize - IV_LEN - TAG_LEN)
76
+
77
+ cipher = OpenSSL::Cipher.new(CIPHER).decrypt
78
+ cipher.key = @key
79
+ cipher.iv = iv
80
+ cipher.auth_tag = tag
81
+ cipher.update(ciphertext) + cipher.final
82
+ rescue OpenSSL::Cipher::CipherError => e
83
+ raise InvalidCiphertextError, "decryption failed: #{e.message}"
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubino
4
+ module Plugins
5
+ # Central registry for plugins and their hooks.
6
+ class Registry
7
+ def initialize
8
+ @hooks = Hash.new { |h, k| h[k] = [] }
9
+ @plugins = []
10
+ end
11
+
12
+ # Registers a hook handler
13
+ def on(event, &block)
14
+ raise Error, "Unknown hook: #{event}. Valid: #{HOOKS.join(", ")}" unless HOOKS.include?(event.to_sym)
15
+
16
+ @hooks[event.to_sym] << block
17
+ end
18
+
19
+ # Executes all handlers for a hook, passing context through each
20
+ def run_hook(event, context = {})
21
+ @hooks[event.to_sym].each do |handler|
22
+ result = handler.call(context)
23
+ # If handler returns a hash, merge it into context
24
+ context = context.merge(result) if result.is_a?(Hash)
25
+ end
26
+ context
27
+ end
28
+
29
+ # Returns true if any handlers are registered for this hook
30
+ def has_hook?(event)
31
+ @hooks[event.to_sym].any?
32
+ end
33
+
34
+ # Loads a plugin from a file
35
+ def load_plugin(path)
36
+ load(path)
37
+ @plugins << path
38
+ rescue StandardError => e
39
+ Rubino.ui.warning("Failed to load plugin #{path}: #{e.message}")
40
+ end
41
+
42
+ # Loads all plugins from configured paths
43
+ def load_all!
44
+ plugin_paths.each do |dir|
45
+ expanded = File.expand_path(dir)
46
+ next unless File.directory?(expanded)
47
+
48
+ Dir.glob(File.join(expanded, "*.rb")).each do |path|
49
+ load_plugin(path)
50
+ end
51
+ end
52
+ end
53
+
54
+ # Returns count of loaded plugins
55
+ def plugin_count
56
+ @plugins.size
57
+ end
58
+
59
+ # Clears all hooks and plugins (for testing)
60
+ def reset!
61
+ @hooks.clear
62
+ @plugins.clear
63
+ end
64
+
65
+ private
66
+
67
+ def plugin_paths
68
+ [
69
+ ".rubino/plugins",
70
+ "~/.rubino/plugins"
71
+ ]
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubino
4
+ # Plugin system with event hooks.
5
+ # Plugins can subscribe to events and modify behavior.
6
+ #
7
+ # Usage:
8
+ # Rubino.plugin do
9
+ # on(:tool_execute_before) do |context|
10
+ # # Modify or inspect tool call before execution
11
+ # end
12
+ #
13
+ # on(:tool_execute_after) do |context|
14
+ # # React to tool results
15
+ # end
16
+ #
17
+ # on(:session_start) do |context|
18
+ # # Do something when a session starts
19
+ # end
20
+ # end
21
+ #
22
+ module Plugins
23
+ # All supported hook points (30+)
24
+ HOOKS = %i[
25
+ tool_execute_before
26
+ tool_execute_after
27
+ tool_approval_before
28
+ tool_approval_after
29
+ tool_result_transform
30
+
31
+ shell_env
32
+ shell_execute_before
33
+ shell_execute_after
34
+
35
+ file_read_before
36
+ file_read_after
37
+ file_write_before
38
+ file_write_after
39
+
40
+ compaction_before
41
+ compaction_after
42
+ compaction_context_inject
43
+
44
+ session_start
45
+ session_end
46
+ session_fork
47
+ session_persist
48
+
49
+ message_before
50
+ message_after
51
+ message_stream_chunk
52
+
53
+ memory_extract
54
+ memory_save_before
55
+ memory_retrieve_after
56
+
57
+ job_before
58
+ job_after
59
+ job_failed
60
+
61
+ model_call_before
62
+ model_call_after
63
+ model_response_transform
64
+
65
+ prompt_assemble_before
66
+ prompt_assemble_after
67
+
68
+ agent_switch
69
+ agent_route
70
+
71
+ config_reload
72
+ startup
73
+ shutdown
74
+ ].freeze
75
+
76
+ class << self
77
+ def registry
78
+ @registry ||= Registry.new
79
+ end
80
+
81
+ def reset!
82
+ @registry = nil
83
+ end
84
+ end
85
+ end
86
+ end