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,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubino
4
+ module API
5
+ module Operations
6
+ module Memory
7
+ # GET /v1/memory/stats
8
+ # Returns the active backend's name and total fact count — the data the
9
+ # CLI /status line and the web dashboard's memory card need without
10
+ # paging the whole store.
11
+ class StatsOperation
12
+ def self.call(request)
13
+ new.call(request)
14
+ end
15
+
16
+ # Accepts an alternate backend for tests.
17
+ def initialize(backend: nil)
18
+ @backend = backend || ::Rubino::Memory::Backends.build
19
+ end
20
+
21
+ def call(_request)
22
+ [200, { backend: @backend.class.backend_name, count: @backend.count }]
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubino
4
+ module API
5
+ module Operations
6
+ # GET /v1/metrics — Prometheus text exposition format (text/plain v0.0.4).
7
+ # No auth required (allowlisted in Middleware::Auth::SKIP_PATHS).
8
+ #
9
+ # @return [[Integer, Hash, Array<String>]] raw Rack triple with the rendered registry.
10
+ class MetricsOperation
11
+ def self.call(_request)
12
+ body = ::Rubino::Metrics.render
13
+ [200, { "content-type" => "text/plain; version=0.0.4" }, [body]]
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubino
4
+ module API
5
+ module Operations
6
+ module Mode
7
+ # GET /v1/mode
8
+ # Returns the active mode and the list of valid modes so the web
9
+ # client can render a picker without hardcoding the set.
10
+ class ShowOperation
11
+ def self.call(request)
12
+ new.call(request)
13
+ end
14
+
15
+ def call(_request)
16
+ current = Rubino::Modes.current
17
+ [200, {
18
+ mode: current,
19
+ description: Rubino::Modes.description(current),
20
+ available: Rubino::Modes::ALL.map do |m|
21
+ { mode: m, description: Rubino::Modes.description(m) }
22
+ end
23
+ }]
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubino
4
+ module API
5
+ module Operations
6
+ module Mode
7
+ # PUT /v1/mode
8
+ # Body: { "mode": "default" | "plan" | "yolo" }
9
+ #
10
+ # Switches the active mode and emits the same `mode_changed` UI event
11
+ # the CLI fires on `/mode plan`, so any in-flight SSE stream notices.
12
+ #
13
+ # @raise [Rubino::ValidationError] on missing/typo'd mode
14
+ class UpdateOperation
15
+ def self.call(request)
16
+ new.call(request)
17
+ end
18
+
19
+ def call(request)
20
+ attrs = request.validate!(Schemas::UpdateMode)
21
+ previous = Rubino::Modes.current
22
+
23
+ begin
24
+ Rubino::Modes.set(attrs[:mode])
25
+ rescue ArgumentError => e
26
+ # Modes.set already rejects unknowns; surface as a 422 the same
27
+ # way validation errors do. The dry-schema enum below normally
28
+ # catches this first; this is just defence in depth for an
29
+ # alternate caller.
30
+ raise ValidationError.new(e.message)
31
+ end
32
+
33
+ current = Rubino::Modes.current
34
+ Rubino.ui&.mode_changed(current, previous: previous)
35
+
36
+ [200, { mode: current, previous: previous, description: Rubino::Modes.description(current) }]
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby_llm"
4
+
5
+ module Rubino
6
+ module API
7
+ module Operations
8
+ module Models
9
+ # GET /v1/models — returns the model catalog from ruby_llm.
10
+ # The source defaults to RubyLLM.models.all but accepts any callable
11
+ # returning an enumerable of model objects/hashes for tests.
12
+ class ListOperation
13
+ def self.call(request)
14
+ new.call(request)
15
+ end
16
+
17
+ # Accepts an alternate model source (callable) for tests.
18
+ def initialize(model_source: nil)
19
+ @model_source = model_source || -> { RubyLLM.models.all }
20
+ end
21
+
22
+ def call(_request)
23
+ models = @model_source.call.map do |m|
24
+ {
25
+ id: model_id(m),
26
+ provider: m.respond_to?(:provider) ? m.provider.to_s : nil,
27
+ context_window: m.respond_to?(:context_window) ? m.context_window : nil
28
+ }
29
+ end
30
+ [200, models]
31
+ end
32
+
33
+ private
34
+
35
+ def model_id(model)
36
+ return model.id if model.respond_to?(:id)
37
+ return model[:id] if model.is_a?(Hash)
38
+
39
+ model.to_s
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubino
4
+ module API
5
+ module Operations
6
+ module OAuth
7
+ module Connections
8
+ # DELETE /v1/oauth/connections/:id
9
+ # Removes a stored OAuth connection (encrypted tokens included) and
10
+ # asks the provider to revoke the underlying token, so a DB dump +
11
+ # encryption-key compromise no longer yields indefinite provider-side
12
+ # access.
13
+ #
14
+ # Provider revoke is best-effort: a failure (network, 4xx) is logged
15
+ # and the local row is still destroyed — leaving a stale local row is
16
+ # strictly worse than missing the revoke, since the user thinks the
17
+ # connection is gone and we'd keep using the encrypted tokens.
18
+ #
19
+ # @return [[Integer, nil]] 204 No Content.
20
+ # @raise [Rubino::NotFoundError] when the connection does not exist.
21
+ class DisconnectOperation
22
+ def self.call(request)
23
+ new.call(request)
24
+ end
25
+
26
+ # Accepts an alternate repository / registry / logger for tests.
27
+ def initialize(repository: nil, registry: ::Rubino::OAuth::Registry, logger: nil)
28
+ @repository = repository
29
+ @registry = registry
30
+ @logger = logger
31
+ end
32
+
33
+ def call(request)
34
+ id = request.params.fetch("id")
35
+ connection = repository.find(id)
36
+ raise NotFoundError.new("oauth_connection", id) unless connection
37
+
38
+ revoke_remote(connection)
39
+ repository.destroy!(id)
40
+ [204, nil]
41
+ end
42
+
43
+ private
44
+
45
+ def revoke_remote(connection)
46
+ provider = @registry.fetch_or_nil(connection[:provider])
47
+ return unless provider&.respond_to?(:revoke)
48
+
49
+ # Prefer refresh_token (Google revokes the whole grant) and fall
50
+ # back to access_token (GitHub only knows about user tokens).
51
+ token = connection[:refresh_token] || connection[:access_token]
52
+ return unless token && !token.empty?
53
+
54
+ provider.revoke(token)
55
+ rescue StandardError => e
56
+ logger.warn(
57
+ event: "oauth.disconnect.revoke_failed",
58
+ provider: connection[:provider],
59
+ connection_id: connection[:id],
60
+ error_class: e.class.name,
61
+ error_message: e.message
62
+ )
63
+ end
64
+
65
+ def repository
66
+ @repository ||= ::Rubino::OAuth::ConnectionRepository.new
67
+ end
68
+
69
+ def logger
70
+ @logger ||= ::Rubino.logger
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubino
4
+ module API
5
+ module Operations
6
+ module OAuth
7
+ module Connections
8
+ # GET /v1/oauth/connections
9
+ # Lists stored OAuth connections through Serializer, which strips
10
+ # tokens and other secret fields before they leave the API.
11
+ class ListOperation
12
+ def self.call(request)
13
+ new.call(request)
14
+ end
15
+
16
+ # Accepts an alternate connection repository for tests.
17
+ def initialize(repository: nil)
18
+ @repository = repository
19
+ end
20
+
21
+ def call(_request)
22
+ connections = repository.list.map { |c| Serializer.call(c) }
23
+ [200, connections]
24
+ end
25
+
26
+ private
27
+
28
+ def repository
29
+ @repository ||= ::Rubino::OAuth::ConnectionRepository.new
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubino
4
+ module API
5
+ module Operations
6
+ module OAuth
7
+ module Providers
8
+ # POST /v1/oauth/providers/:id/callback
9
+ #
10
+ # Client posts back code + state + code_verifier (plus expected_state
11
+ # it kept from connect). We constant-time-compare state, exchange the
12
+ # code, fetch account info, upsert by (provider, account_id) with
13
+ # encrypted tokens, and return the connection. Exchange outcomes
14
+ # bump oauth_token_exchanges_total {ok|error}.
15
+ #
16
+ # @return [[Integer, Hash]] 201 + serialized connection (tokens stripped).
17
+ # @raise [Rubino::NotFoundError] when no provider is registered for +:id+.
18
+ # @raise [Rubino::ValidationError] when the body fails Schemas::CallbackProvider or state mismatches.
19
+ # @raise [Rubino::UpstreamError] when the provider's token exchange raises.
20
+ class CallbackOperation
21
+ def self.call(request)
22
+ new.call(request)
23
+ end
24
+
25
+ # Accepts an alternate provider registry and connection repository for tests.
26
+ def initialize(registry: ::Rubino::OAuth::Registry, repository: nil)
27
+ @registry = registry
28
+ @repository = repository
29
+ end
30
+
31
+ def call(request)
32
+ id = request.params.fetch("id")
33
+ provider = @registry.fetch(id)
34
+ attrs = request.validate!(Schemas::CallbackProvider)
35
+
36
+ unless Rack::Utils.secure_compare(attrs[:state], attrs[:expected_state])
37
+ raise ValidationError, "state mismatch"
38
+ end
39
+
40
+ token =
41
+ begin
42
+ provider.exchange_code(
43
+ code: attrs[:code],
44
+ redirect_uri: attrs[:redirect_uri],
45
+ code_verifier: attrs[:code_verifier]
46
+ )
47
+ rescue StandardError => e
48
+ ::Rubino::Metrics.counter(:oauth_token_exchanges_total,
49
+ provider: provider.id, outcome: "error").increment
50
+ raise UpstreamError.new("token exchange failed: #{e.class.name}", service: provider.id)
51
+ end
52
+
53
+ ::Rubino::Metrics.counter(:oauth_token_exchanges_total,
54
+ provider: provider.id, outcome: "ok").increment
55
+
56
+ info = provider.fetch_account_info(token[:access_token])
57
+
58
+ connection = repository.upsert(
59
+ provider: provider.id,
60
+ account_id: info[:account_id],
61
+ account_email: info[:account_email],
62
+ access_token: token[:access_token],
63
+ refresh_token: token[:refresh_token],
64
+ expires_at: token[:expires_at],
65
+ scopes: token[:scopes],
66
+ metadata: info[:metadata] || {}
67
+ )
68
+
69
+ [201, Serializer.call(connection)]
70
+ end
71
+
72
+ private
73
+
74
+ def repository
75
+ @repository ||= ::Rubino::OAuth::ConnectionRepository.new
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubino
4
+ module API
5
+ module Operations
6
+ module OAuth
7
+ module Providers
8
+ # POST /v1/oauth/providers/:id/connect
9
+ #
10
+ # Builds a PKCE authorize request for the provider and returns
11
+ # { authorize_url, state, code_verifier, provider }. The client
12
+ # persists state + code_verifier between connect and callback;
13
+ # rubino stays stateless on the OAuth flow itself.
14
+ #
15
+ # @raise [Rubino::NotFoundError] when no provider is registered for +:id+.
16
+ # @raise [Rubino::ValidationError] when the body fails Schemas::ConnectProvider.
17
+ class ConnectOperation
18
+ def self.call(request)
19
+ new.call(request)
20
+ end
21
+
22
+ # Accepts an alternate provider registry for tests.
23
+ def initialize(registry: ::Rubino::OAuth::Registry)
24
+ @registry = registry
25
+ end
26
+
27
+ def call(request)
28
+ id = request.params.fetch("id")
29
+ provider = @registry.fetch(id)
30
+ attrs = request.validate!(Schemas::ConnectProvider)
31
+
32
+ flow = provider.build_authorize_request(
33
+ redirect_uri: attrs[:redirect_uri],
34
+ scopes: attrs[:scopes]
35
+ )
36
+
37
+ [200, flow.merge(provider: provider.id)]
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubino
4
+ module API
5
+ module Operations
6
+ module OAuth
7
+ module Providers
8
+ # GET /v1/oauth/providers
9
+ # Lists OAuth providers registered at boot, with their default scopes.
10
+ class ListOperation
11
+ def self.call(request)
12
+ new.call(request)
13
+ end
14
+
15
+ # Accepts an alternate provider registry for tests.
16
+ def initialize(registry: ::Rubino::OAuth::Registry)
17
+ @registry = registry
18
+ end
19
+
20
+ def call(_request)
21
+ providers = @registry.all.map do |p|
22
+ {
23
+ id: p.id,
24
+ display_name: p.class.display_name,
25
+ scopes: p.scopes
26
+ }
27
+ end
28
+ [200, providers]
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubino
4
+ module API
5
+ module Operations
6
+ module OAuth
7
+ # Strips secret fields from connection rows before they leave the API.
8
+ # Tokens never go on the wire — clients use them implicitly through
9
+ # tools, not directly.
10
+ module Serializer
11
+ PUBLIC_FIELDS = %i[id provider account_id account_email expires_at scopes metadata created_at
12
+ updated_at].freeze
13
+
14
+ def self.call(connection)
15
+ connection.slice(*PUBLIC_FIELDS)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubino
4
+ module API
5
+ module Operations
6
+ module Runs
7
+ # POST /v1/sessions/:id/runs
8
+ # Persists a new run for the session and hands it to the executor.
9
+ # The run is reported as "running" immediately; clients tail
10
+ # /v1/runs/:id/events for state transitions.
11
+ #
12
+ # @return [[Integer, Hash]] 201 + run payload.
13
+ # @raise [Rubino::NotFoundError] when the parent session does not exist.
14
+ # @raise [Rubino::ValidationError] when the body fails Schemas::CreateRun.
15
+ class CreateOperation
16
+ def self.call(request)
17
+ new.call(request)
18
+ end
19
+
20
+ # Accepts alternate collaborators (session repo, run repo, executor) for tests.
21
+ def initialize(session_repository: nil, run_repository: nil, executor: nil)
22
+ @session_repo = session_repository || ::Rubino::Session::Repository.new
23
+ @run_repo = run_repository || ::Rubino::Run::Repository.new
24
+ @executor = executor || ::Rubino::Run::Executor.new
25
+ end
26
+
27
+ def call(request)
28
+ session_id = request.params.fetch("id")
29
+ raise NotFoundError.new("session", session_id) unless @session_repo.find(session_id)
30
+
31
+ attrs = request.validate!(Schemas::CreateRun)
32
+ ensure_input_or_attachments!(attrs)
33
+ run = @run_repo.create(
34
+ session_id: session_id,
35
+ input_text: attrs[:input],
36
+ attachments: attrs[:attachments] || [],
37
+ skills: attrs[:skills] || [],
38
+ model: attrs[:model],
39
+ provider: attrs[:provider]
40
+ )
41
+
42
+ @executor.start(run)
43
+
44
+ [201, serialize(run)]
45
+ end
46
+
47
+ private
48
+
49
+ # A run needs SOMETHING to act on: either text or at least one
50
+ # attachment (image-only runs are valid — the executor fills in a
51
+ # default prompt). The schema allows a blank/absent `input`, so this
52
+ # is where the "input present OR attachments present" rule lives.
53
+ # Reproduces the prior schema's 422 shape so an empty, attachment-less
54
+ # body still fails as `input: ["must be filled"]`.
55
+ def ensure_input_or_attachments!(attrs)
56
+ return if attrs[:input].to_s.strip != ""
57
+ return if Array(attrs[:attachments]).any?
58
+
59
+ raise ValidationError.new(
60
+ "invalid request body",
61
+ details: { errors: { input: ["must be filled"] } }
62
+ )
63
+ end
64
+
65
+ def serialize(run)
66
+ {
67
+ id: run[:id],
68
+ session_id: run[:session_id],
69
+ status: "running",
70
+ created_at: run[:created_at]
71
+ }
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end