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,404 @@
1
+ # Security
2
+
3
+ Secure Ruby/Rails for an AI agent. Each vuln class shows the unsafe pattern next to the safe one. Targets Ruby 3.2–3.4, Rails 7.1–8.x. For app structure see references/rails.md; for the `send` injection note see references/metaprogramming.md; for regex performance (separate from ReDoS) see references/performance.md.
4
+
5
+ ## SQL injection
6
+
7
+ Never interpolate user input into SQL fragments. Use parameterized queries — hash conditions, placeholders, or `sanitize_sql`.
8
+
9
+ ```ruby
10
+ # WRONG — string interpolation, trivially injectable
11
+ User.where("name = '#{params[:name]}'")
12
+ User.where("age > #{params[:age]}")
13
+ Order.order(params[:sort]) # order() is also injectable
14
+
15
+ # RIGHT — hash conditions (auto-parameterized + quoted)
16
+ User.where(name: params[:name])
17
+ User.where("age > ?", params[:age]) # positional placeholder
18
+ User.where("age > :age", age: params[:age]) # named placeholder
19
+ ```
20
+
21
+ Column names and SQL keywords can't be parameterized — allowlist them.
22
+
23
+ ```ruby
24
+ # WRONG — user controls the column/direction
25
+ Order.order("#{params[:col]} #{params[:dir]}")
26
+
27
+ # RIGHT — allowlist, never pass raw input as identifiers
28
+ SORTS = { "name" => "name", "date" => "created_at" }.freeze
29
+ col = SORTS.fetch(params[:col], "created_at")
30
+ dir = params[:dir] == "desc" ? "desc" : "asc"
31
+ Order.order(Arel.sql("#{col} #{dir}")) # Arel.sql asserts you vetted it
32
+ ```
33
+
34
+ `Arel.sql` silences Rails' "dangerous raw SQL" deprecation — only wrap values you have already proven safe. Other injectable methods that take raw SQL: `select`, `group`, `having`, `joins`, `pluck`, `lock`, `from`. The same hash/placeholder rules apply. `find_by_sql`/`execute` need `sanitize_sql_array`:
35
+
36
+ ```ruby
37
+ sql = User.sanitize_sql_array(["SELECT * FROM users WHERE name = ?", params[:name]])
38
+ User.find_by_sql(sql)
39
+ ```
40
+
41
+ ## Command injection
42
+
43
+ A string command goes through the shell, so metacharacters (`;`, `|`, `` ` ``, `$()`) inject. The array form bypasses the shell entirely.
44
+
45
+ ```ruby
46
+ # WRONG — shell interpolation
47
+ system("convert #{params[:file]} out.png")
48
+ `git log #{ref}`
49
+ %x{ping #{host}}
50
+ exec("rm -rf #{dir}")
51
+
52
+ # RIGHT — array/multi-arg form: no shell, args passed literally
53
+ system("convert", params[:file], "out.png")
54
+ out = IO.popen(["git", "log", ref], &:read)
55
+ system("ping", "-c", "1", host)
56
+ ```
57
+
58
+ `open`, `Open3.capture2/capture3`, `IO.popen`, `spawn`, `Process.spawn`, `Kernel#exec/system` all take the array form. Prefer `Open3` for capturing output safely:
59
+
60
+ ```ruby
61
+ require "open3"
62
+ stdout, stderr, status = Open3.capture3("git", "log", "--oneline", ref)
63
+ ```
64
+
65
+ Never pass untrusted input to `eval`, `instance_eval`, `class_eval`, `Kernel#open` with a `"|cmd"` string, or `send`/`public_send` with a user-supplied method name (see references/metaprogramming.md).
66
+
67
+ ## Mass assignment
68
+
69
+ Use strong parameters; permit explicitly. Never `permit!` user input.
70
+
71
+ ```ruby
72
+ # WRONG — user can set admin: true, role:, etc.
73
+ User.create(params[:user])
74
+ User.update(params.require(:user).permit!)
75
+
76
+ # RIGHT — explicit allowlist; sensitive attrs never permitted
77
+ def user_params
78
+ params.require(:user).permit(:name, :email, :bio)
79
+ end
80
+ User.create(user_params)
81
+ ```
82
+
83
+ Defense in depth for truly sensitive columns — block assignment at the model:
84
+
85
+ ```ruby
86
+ class User < ApplicationRecord
87
+ attr_readonly :account_id # set once, never via update
88
+ # or expose a guarded setter and keep role= private
89
+ end
90
+ ```
91
+
92
+ Nested/array params must be declared:
93
+
94
+ ```ruby
95
+ params.require(:order).permit(:note, line_items: [:sku, :qty], tag_ids: [])
96
+ ```
97
+
98
+ ## Unsafe deserialization
99
+
100
+ `Marshal.load`, `YAML.load` (pre-3.1 behavior), and `Oj` in object mode can instantiate arbitrary classes and trigger RCE. Never feed them untrusted bytes.
101
+
102
+ ```ruby
103
+ # WRONG — RCE on attacker-controlled input
104
+ Marshal.load(request.body.read)
105
+ YAML.load(params[:config])
106
+
107
+ # RIGHT — safe_load with an explicit class allowlist
108
+ YAML.safe_load(params[:config]) # only basic types
109
+ YAML.safe_load(file, permitted_classes: [Date, Symbol], aliases: false)
110
+
111
+ # Prefer JSON for untrusted data
112
+ JSON.parse(request.body.read) # objects only, no code
113
+ ```
114
+
115
+ On Ruby 3.1+/Psych 4, `YAML.load` is already an alias for `safe_load`; use `YAML.unsafe_load` only for files you fully control (and even then, prefer not to). Avoid `Marshal` for any cross-trust boundary — it has no safe mode.
116
+
117
+ ## XSS in Rails
118
+
119
+ ERB auto-escapes by default. The danger is anything that opts out: `html_safe`, `raw`, `<%==`, and `sanitize` misuse.
120
+
121
+ ```erb
122
+ <%# RIGHT — auto-escaped, safe %>
123
+ <%= @user.bio %>
124
+
125
+ <%# WRONG — renders raw HTML from user input %>
126
+ <%= raw @user.bio %>
127
+ <%= @user.bio.html_safe %>
128
+ <%== @user.bio %>
129
+ ```
130
+
131
+ When you must allow some HTML, use `sanitize` with an allowlist — never `html_safe`:
132
+
133
+ ```erb
134
+ <%= sanitize @post.body, tags: %w[p br strong em a], attributes: %w[href] %>
135
+ ```
136
+
137
+ Other XSS sinks:
138
+
139
+ ```ruby
140
+ # WRONG — user data into a script/JS context
141
+ "<script>var u = '#{params[:name]}';</script>".html_safe
142
+ # RIGHT
143
+ content_tag(:script, "var u = #{params[:name].to_json};".html_safe) # to_json escapes
144
+
145
+ link_to "site", params[:url] # WRONG: javascript: URLs execute
146
+ # RIGHT — validate scheme
147
+ url = params[:url].to_s
148
+ link_to "site", (url.start_with?("http://", "https://") ? url : "#")
149
+ ```
150
+
151
+ Set a Content-Security-Policy (`config/initializers/content_security_policy.rb`) as a backstop. `html_safe` does not sanitize — it only marks a string as already-safe; calling it on user input is the bug.
152
+
153
+ ## CSRF
154
+
155
+ Rails enables CSRF protection by default. Keep it on.
156
+
157
+ ```ruby
158
+ class ApplicationController < ActionController::Base
159
+ protect_from_forgery with: :exception # default on new apps; don't remove
160
+ end
161
+ ```
162
+
163
+ Don't `skip_forgery_protection` or `skip_before_action :verify_authenticity_token` on state-changing actions. For JSON APIs authenticated by token/header (not cookies), use `ActionController::API` (no cookie session → CSRF N/A) rather than disabling the check on a cookie-session controller. Never globally disable it to "fix" a failing form.
164
+
165
+ ## Open redirects
166
+
167
+ `redirect_to` with user input lets attackers bounce victims to phishing sites.
168
+
169
+ ```ruby
170
+ # WRONG — open redirect
171
+ redirect_to params[:return_to]
172
+
173
+ # RIGHT — Rails 7+ blocks off-host redirects by default; be explicit
174
+ redirect_to params[:return_to], allow_other_host: false # default
175
+ # Or allowlist paths only
176
+ safe = params[:return_to].to_s
177
+ redirect_to(safe.start_with?("/") && !safe.start_with?("//") ? safe : root_path)
178
+ ```
179
+
180
+ Rails 7 made `allow_other_host: false` the default and raises on cross-host targets — do not set `allow_other_host: true` with user input.
181
+
182
+ ## Authorization / IDOR
183
+
184
+ Insecure Direct Object Reference: a user passes an `id` for a record they don't own. Scope every lookup to the current user, and enforce a policy layer.
185
+
186
+ ```ruby
187
+ # WRONG — any user can read any invoice
188
+ @invoice = Invoice.find(params[:id])
189
+
190
+ # RIGHT — scope to ownership; 404s on someone else's record
191
+ @invoice = current_user.invoices.find(params[:id])
192
+ ```
193
+
194
+ Use a policy library and verify on every action:
195
+
196
+ ```ruby
197
+ # Pundit
198
+ class InvoicePolicy < ApplicationPolicy
199
+ def show? = record.user_id == user.id
200
+ end
201
+
202
+ def show
203
+ @invoice = Invoice.find(params[:id])
204
+ authorize @invoice # raises Pundit::NotAuthorizedError if denied
205
+ end
206
+ # enforce it globally
207
+ after_action :verify_authorized, except: :index
208
+ ```
209
+
210
+ CanCanCan equivalent uses `authorize! :show, @invoice` against an `Ability`. Don't rely on hidden form fields or "the UI doesn't show it" — always check server-side. Don't trust `params[:user_id]` for the actor; derive identity from the session/token.
211
+
212
+ ## Secrets management
213
+
214
+ Never commit secrets. Use Rails encrypted credentials or ENV; keep the master key out of git.
215
+
216
+ ```bash
217
+ EDITOR="code --wait" bin/rails credentials:edit # edits config/credentials.yml.enc
218
+ # config/master.key (and *.key) MUST be gitignored; ship the key via ENV in prod
219
+ ```
220
+
221
+ ```ruby
222
+ Rails.application.credentials.dig(:stripe, :secret_key) # decrypted at runtime
223
+ ENV.fetch("DATABASE_URL") # fetch → fails loudly if unset
224
+ ```
225
+
226
+ ```ruby
227
+ # WRONG — secret hardcoded / committed
228
+ STRIPE_KEY = "sk_live_abc123"
229
+ ENV["SECRET"] || "fallback-secret" # fallback leaks into source
230
+ ```
231
+
232
+ Use the `dotenv-rails` gem for local dev ENV (gitignore `.env`, commit `.env.example` with blank values). Per-environment credentials: `config/credentials/production.yml.enc` + `config/credentials/production.key`. If a key leaks, rotate it — don't just remove it from the latest commit (it stays in history).
233
+
234
+ ## Dependency security
235
+
236
+ Audit gems for known CVEs and keep them patched.
237
+
238
+ ```bash
239
+ gem install bundler-audit
240
+ bundle audit check --update # checks Gemfile.lock against ruby-advisory-db
241
+ ```
242
+
243
+ Run `bundle audit` in CI and fail the build on findings. Enable Dependabot (`.github/dependabot.yml`) or Renovate for automated PRs. Pin sources and avoid arbitrary git/path gems from untrusted origins:
244
+
245
+ ```ruby
246
+ source "https://rubygems.org" # use HTTPS; don't add random sources
247
+ gem "rails", "~> 7.2.0" # pessimistic constraint, see tooling.md
248
+ gem "nokogiri", "~> 1.16"
249
+ ```
250
+
251
+ Commit `Gemfile.lock` (apps) so CI/prod resolve identical versions. Review transitive deps; prefer well-maintained gems. For high-assurance setups, verify gem signatures (`gem cert` / `--trust-policy`), though most of the ecosystem is unsigned — rely primarily on the advisory DB + lockfile pinning. See references/tooling.md for Bundler details and references/gem-authoring.md for publishing.
252
+
253
+ ## Static analysis — Brakeman
254
+
255
+ Brakeman is a Rails-specific security scanner; run it in CI.
256
+
257
+ ```bash
258
+ gem install brakeman
259
+ brakeman --no-pager # scan
260
+ brakeman -w2 -z # warning level 2+, exit non-zero on findings (CI)
261
+ brakeman -I # interactively build the ignore file
262
+ ```
263
+
264
+ Triage findings into `config/brakeman.ignore` (with a note/justification per entry) for vetted false positives; never blanket-ignore. Pair with RuboCop's security cops. Brakeman catches SQLi, command injection, mass assignment, unsafe redirects, and `html_safe`/`raw` XSS sinks statically — but it is not a substitute for the safe patterns above.
265
+
266
+ ## ReDoS and Timeout
267
+
268
+ Catastrophic backtracking lets a short input hang a regex (and your request thread). The risk is nested/overlapping quantifiers on user input.
269
+
270
+ ```ruby
271
+ # WRONG — exponential backtracking on "aaaaaa!"
272
+ /^(a+)+$/ =~ user_input
273
+ /(\w+\s*)+$/ =~ user_input
274
+
275
+ # RIGHT — avoid nested quantifiers; anchor with \A \z (not ^ $, which match per-line)
276
+ /\A\w+\z/ =~ user_input
277
+ ```
278
+
279
+ Ruby 3.2+ ships a regexp timeout — set a global cap so no single match can hang:
280
+
281
+ ```ruby
282
+ Regexp.timeout = 1.0 # seconds, global (Ruby 3.2+)
283
+ /\A(a+)+\z/.match?(input, timeout: 0.5) # per-regexp override
284
+ ```
285
+
286
+ Validate input length before matching, and prefer non-regex parsing where possible. (Regex *performance/backtracking* internals: references/performance.md.) For other untrusted-duration work, `Timeout.timeout` exists but is unsafe — it can raise at arbitrary points and corrupt state; prefer the native regexp timeout or IO/socket-level timeouts instead.
287
+
288
+ ## Random tokens and constant-time comparison
289
+
290
+ Use `SecureRandom` (CSPRNG) for anything secret — never `rand`, `Random`, or `SecureRandom.random_number` mod small ranges for tokens.
291
+
292
+ ```ruby
293
+ # WRONG — predictable
294
+ token = rand(10**10).to_s
295
+ token = Time.now.to_i.to_s(36)
296
+
297
+ # RIGHT
298
+ SecureRandom.hex(32) # 64 hex chars
299
+ SecureRandom.urlsafe_base64(32)
300
+ SecureRandom.uuid # for ids, not secrets
301
+ ```
302
+
303
+ Compare secrets/tokens in constant time to avoid timing attacks — `==` short-circuits and leaks length/prefix info.
304
+
305
+ ```ruby
306
+ # WRONG — early-exit comparison leaks timing
307
+ provided == stored_token
308
+
309
+ # RIGHT — constant-time
310
+ ActiveSupport::SecurityUtils.secure_compare(provided, stored_token) # equal-length
311
+ ActiveSupport::SecurityUtils.fixed_length_secure_compare(a, b)
312
+ # plain Ruby:
313
+ OpenSSL.fixed_length_secure_compare(a, b) # raises unless same length; hash first if not
314
+ ```
315
+
316
+ Store password hashes with `has_secure_password` (bcrypt) — never plain or fast hashes (MD5/SHA) for passwords.
317
+
318
+ ## TLS / certificate verification
319
+
320
+ Never disable certificate verification. It silently enables MITM.
321
+
322
+ ```ruby
323
+ # WRONG — accepts any cert
324
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
325
+ Net::HTTP.start(host, 443, use_ssl: true, verify_mode: OpenSSL::SSL::VERIFY_NONE)
326
+ OpenSSL::SSL::VERIFY_NONE # anywhere with untrusted peers
327
+
328
+ # RIGHT — verify (the default); use https URIs
329
+ uri = URI("https://api.example.com")
330
+ Net::HTTP.get(uri) # verifies by default
331
+ http = Net::HTTP.new(uri.host, uri.port)
332
+ http.use_ssl = true # verify_mode defaults to VERIFY_PEER
333
+ ```
334
+
335
+ If you hit cert errors, fix the trust store (`ssl_ca_cert`/`SSL_CERT_FILE`) — don't disable verification. Same rule for HTTP client gems (Faraday/HTTParty/Excon): never set `verify: false`/`ssl_verify: false` in production.
336
+
337
+ ## SSRF
338
+
339
+ Server-Side Request Forgery: user-controlled URLs let attackers reach internal services (cloud metadata `169.254.169.254`, `localhost`, private ranges).
340
+
341
+ ```ruby
342
+ # WRONG — fetches whatever the user points at
343
+ Net::HTTP.get(URI(params[:url]))
344
+
345
+ # RIGHT — allowlist scheme + host, then resolve and block private IPs
346
+ require "resolv"; require "ipaddr"
347
+
348
+ def safe_fetch(raw)
349
+ uri = URI.parse(raw)
350
+ raise "scheme" unless %w[http https].include?(uri.scheme)
351
+ raise "host" unless ALLOWED_HOSTS.include?(uri.host) # allowlist is strongest
352
+ ip = IPAddr.new(Resolv.getaddress(uri.host))
353
+ raise "private" if ip.private? || ip.loopback? || ip.link_local?
354
+ Net::HTTP.get(uri)
355
+ end
356
+ ```
357
+
358
+ Prefer an explicit host allowlist over a denylist. Beware DNS-rebinding (resolve-then-connect on the same IP) and redirects (disable auto-follow or re-validate each hop). Block link-local (`169.254.0.0/16`) to protect cloud metadata endpoints.
359
+
360
+ ## File uploads and path traversal
361
+
362
+ `../` in a filename can escape your directory. Never build paths from raw user input.
363
+
364
+ ```ruby
365
+ # WRONG — path traversal: "../../etc/passwd"
366
+ File.read(File.join("uploads", params[:file]))
367
+ File.read("uploads/#{params[:name]}")
368
+
369
+ # RIGHT — basename strips directories; verify the result stays inside the root
370
+ name = File.basename(params[:file]) # drops any path components
371
+ path = File.expand_path(File.join(UPLOAD_DIR, name))
372
+ raise "traversal" unless path.start_with?(UPLOAD_DIR + File::SEPARATOR)
373
+ File.read(path)
374
+ ```
375
+
376
+ For uploads: allowlist extensions and validate content type, cap size, store outside the web root (or use Active Storage), and generate your own filename (`SecureRandom`) rather than trusting the client's.
377
+
378
+ ```ruby
379
+ ALLOWED_EXT = %w[.png .jpg .jpeg .pdf].freeze
380
+ ext = File.extname(uploaded.original_filename).downcase
381
+ raise "type" unless ALLOWED_EXT.include?(ext)
382
+ stored = "#{SecureRandom.uuid}#{ext}"
383
+ ```
384
+
385
+ Validate by sniffing real content (e.g. `Marcel`/magic bytes), not just the extension or the client-supplied MIME type. Never `send_file`/`render file:` with a user-controlled path without the basename+root check above.
386
+
387
+ ## Quick checklist
388
+
389
+ - SQLi: `where(col: val)` / `where("x = ?", val)`; never interpolate; allowlist column/order identifiers; `Arel.sql` only on vetted strings.
390
+ - Command: array form `system("cmd", arg)` / `Open3.capture3`; never string commands, backticks, or `eval` with input.
391
+ - Mass assignment: strong params with explicit `permit`; never `permit!` user data; guard sensitive columns at the model.
392
+ - Deserialization: `YAML.safe_load` / `JSON.parse` on untrusted input; never `Marshal.load` or `YAML.unsafe_load` across trust boundaries.
393
+ - XSS: rely on auto-escaping; `sanitize` with allowlist, never `raw`/`html_safe` on user input; `to_json` for JS contexts; set CSP.
394
+ - CSRF: keep `protect_from_forgery`; use `ActionController::API` for token APIs instead of disabling it.
395
+ - Redirects: keep `allow_other_host: false`; allowlist or require leading `/`.
396
+ - AuthZ/IDOR: scope finds via `current_user.things.find`; enforce Pundit/CanCanCan on every action; verify server-side.
397
+ - Secrets: encrypted credentials or `ENV.fetch`; gitignore `*.key`/`.env`; rotate on leak; no hardcoded fallbacks.
398
+ - Deps: `bundle audit` in CI; Dependabot; commit `Gemfile.lock`; HTTPS source; pessimistic version pins.
399
+ - Scan: Brakeman `-w2 -z` in CI; triage into `brakeman.ignore` with justifications.
400
+ - ReDoS: avoid nested quantifiers; anchor with `\A\z`; set `Regexp.timeout`.
401
+ - Tokens: `SecureRandom`; compare with `SecurityUtils.secure_compare`; `has_secure_password` for passwords.
402
+ - TLS: never `VERIFY_NONE` / `verify: false`; fix the CA store instead.
403
+ - SSRF: allowlist scheme+host, resolve and block private/loopback/link-local IPs, re-validate redirects.
404
+ - Files: `File.basename` + `expand_path` + root prefix check; allowlist extension and sniff content; generate the stored filename.