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,590 @@
1
+ # Configuration Reference
2
+
3
+ All values below are checked against `lib/rubino/config/defaults.rb` (`MODULE_DEFAULTS`) — the single source of truth. Only keys that ship a default are shown with one; everything else is opt-in.
4
+
5
+ ## File Locations
6
+
7
+ - **User config:** `~/.rubino/config.yml` (created by `rubino setup`)
8
+ - **Project config:** `.rubino/config.yml` (overrides user config)
9
+ - **Secrets:** `~/.rubino/.env`
10
+ - **Database:** `~/.rubino/rubino.sqlite3`
11
+
12
+ > **`RUBINO_HOME` relocates everything.** When set, the home directory, `config.yml`, `.env`, and the database all follow it (the `database.path` default is a sentinel resolved at read time against the resolved home — issue #96). The CLI and the API server share one resolver, so they never disagree about where state lives.
13
+
14
+ ## Precedence (highest to lowest)
15
+
16
+ 1. Environment variables (`RUBINO_*`)
17
+ 2. Project-local `.rubino/config.yml`
18
+ 3. User global `~/.rubino/config.yml`
19
+ 4. Built-in defaults
20
+
21
+ ## Substitutions
22
+
23
+ Use in any string value:
24
+ - `{env:VAR_NAME}` or `${VAR_NAME}` — inserts an environment variable
25
+ - `{file:path/to/file}` — inserts file contents
26
+
27
+ ---
28
+
29
+ ## Full Config Reference
30
+
31
+ ### model
32
+
33
+ ```yaml
34
+ model:
35
+ default: "openai/gpt-4.1" # Model identifier (NOTE: resolves to OpenRouter — see models-and-keys.md)
36
+ provider: "auto" # auto | openai | anthropic | bedrock | gemini | minimax | gateway
37
+ context_length: null # Override context window (null = use model default)
38
+ temperature: 0.3 # Generation temperature
39
+ max_tokens: null # Max output tokens (anthropic-family path); null = adapter default (16384)
40
+ thinking_budget: null # LEGACY — superseded by thinking.effort (below); null = adapter default (8000), 0 disables
41
+ max_tokens_text_headroom: 4096 # Visible-output headroom reserved on top of the thinking budget
42
+ supports_vision: null # null = auto-detect from model id; true/false to override
43
+ ```
44
+
45
+ > The shipped default `openai/gpt-4.1` resolves to OpenRouter in ruby_llm's registry. See [models-and-keys.md](models-and-keys.md) for the per-provider blocks and the fail-fast behavior.
46
+
47
+ ### providers
48
+
49
+ ```yaml
50
+ providers:
51
+ openai:
52
+ base_url: null # Custom endpoint (Azure, proxy)
53
+ request_timeout_seconds: 600 # Per-read socket inactivity timeout (resets per chunk)
54
+ stale_timeout_seconds: 300 # Stale connection timeout
55
+ anthropic:
56
+ base_url: null
57
+ request_timeout_seconds: 600
58
+ bedrock:
59
+ region: "us-east-1"
60
+ request_timeout_seconds: 600
61
+ gemini:
62
+ request_timeout_seconds: 600
63
+ gateway: # OpenAI-compatible gateway
64
+ openai_compatible: true
65
+ assume_model_exists: true
66
+ base_url: null
67
+ request_timeout_seconds: 600
68
+ ```
69
+
70
+ Per-provider you may also set `api_key`, and for custom gateways `anthropic_compatible: true` (MiniMax) or `openai_compatible: true`. See [models-and-keys.md](models-and-keys.md).
71
+
72
+ Per-provider `supports_thinking: true | false` declares whether the backend handles an Anthropic-style thinking budget correctly; `false` means no budget is ever sent to it, regardless of `thinking.effort`. Unset, MiniMax-family model ids default to `false`, everything else to `true` — see [reasoning & thinking](#reasoning--thinking).
73
+
74
+ ### auxiliary
75
+
76
+ ```yaml
77
+ auxiliary:
78
+ compression:
79
+ provider: "main" # "main" uses default model
80
+ model: "" # Specific model for compression
81
+ base_url: null
82
+ timeout: 120
83
+ approval:
84
+ provider: "main"
85
+ model: ""
86
+ base_url: null
87
+ timeout: 30
88
+ vision: # `vision` tool delegates here so a text-only primary can "see"
89
+ provider: "main"
90
+ model: "" # "auto-vision" lets an OpenAI-compatible gateway pick
91
+ base_url: null
92
+ timeout: 120
93
+ summarize: # `summarize_file` tool map-reduces big files out-of-context here
94
+ provider: "main"
95
+ model: ""
96
+ base_url: null
97
+ timeout: 300
98
+ ```
99
+
100
+ ### agent
101
+
102
+ ```yaml
103
+ agent:
104
+ max_turns: 90 # Max turns per session
105
+ max_tool_iterations: 8 # Max consecutive tool calls
106
+ max_turn_seconds: 120 # Timeout per turn
107
+ api_max_retries: 5 # LLM API retry count (exp backoff)
108
+ api_retry_backoff_cap_seconds: 16 # Max per-retry backoff draw
109
+ api_retry_backoff_overload_cap_seconds: 60 # Higher cap used only for overload (529/503)
110
+ empty_response_max_retries: 2 # In-turn retries for a 200-OK-but-empty response
111
+ fallback_models: [] # Ordered provider/model fallback chain (empty = none)
112
+ disabled_toolsets: [] # Tool names to disable
113
+ tool_use_enforcement: "auto"
114
+ ```
115
+
116
+ ### run
117
+
118
+ ```yaml
119
+ run:
120
+ idle_event_timeout: 300 # SSE watchdog: mark a stalled run failed after N idle seconds (null = off)
121
+ ```
122
+
123
+ ### database
124
+
125
+ ```yaml
126
+ database:
127
+ path: "<RUBINO_HOME>/rubino.sqlite3" # sentinel; resolved against the home at read time
128
+ ```
129
+
130
+ An explicit `path` in `config.yml` is used verbatim and overrides the sentinel.
131
+
132
+ ### paths
133
+
134
+ ```yaml
135
+ paths:
136
+ home: "~/.rubino"
137
+ memory: "~/.rubino/memories"
138
+ skills: "~/.rubino/skills"
139
+ cron: "~/.rubino/cron"
140
+ sessions: "~/.rubino/sessions"
141
+ logs: "~/.rubino/logs"
142
+ ```
143
+
144
+ ### ui
145
+
146
+ ```yaml
147
+ ui:
148
+ adapter: "cli" # cli | api | null
149
+ theme: "default" # default | dark | light | monokai
150
+ verbose: false
151
+ ```
152
+
153
+ ### notifications
154
+
155
+ Attention signals for the moments the agent needs human eyes: a long turn finishing, an approval prompt parking the run on a decision, or a background subagent escalating an `ask_parent` to you (the ⛔ banner).
156
+
157
+ ```yaml
158
+ notifications:
159
+ enabled: true # master switch for all attention signals
160
+ bell: true # terminal bell (BEL) per event; on iTerm2 an OSC 9 escape is also sent (native macOS notification)
161
+ command: null # optional shell command spawned non-blocking per event, e.g. "osascript -e \"display notification ...\""
162
+ min_turn_seconds: 10 # a turn must run at least this long before its completion notifies; quick turns stay silent
163
+ ```
164
+
165
+ - **Events**: `turn_finished` (only when the turn ran ≥ `min_turn_seconds`), `needs_approval` (the main agent's approval card or a background child flipping to `needs approval`), `blocked` (a background child escalated `ask_parent` to the human).
166
+ - **Bell hygiene**: the BEL byte is only ever written to a real terminal — never into a pipe — and is routed to the real terminal IO even while the bottom composer owns the screen (BEL doesn't move the cursor).
167
+ - **`command` hook**: runs detached and best-effort (stdio nulled, errors swallowed to the log) with `RUBINO_EVENT` (`turn_finished` | `needs_approval` | `blocked`) and `RUBINO_MESSAGE` in its environment — the seam for `osascript` (macOS), `notify-send` (Linux), or any custom notifier.
168
+ - **Spam control**: events within ~1s of the last emitted one coalesce into a single signal.
169
+
170
+ ### reasoning & thinking
171
+
172
+ Two orthogonal first-class knobs — these are what `/reasoning` and `/think` write:
173
+
174
+ ```yaml
175
+ display:
176
+ reasoning: collapsed # hidden | collapsed | full — how reasoning is RENDERED
177
+
178
+ thinking:
179
+ effort: "off" # "off" | low | medium | high — how hard the model thinks
180
+ ```
181
+
182
+ - `display.reasoning` controls rendering: `hidden` (nothing shown; Ctrl+O can still reveal the last thought), `collapsed` (default — a dim "✻ thought for Ns · ctrl-o to show" cue), `full` (the whole reasoning as a dim `┊` aside).
183
+ - `thinking.effort` maps to an Anthropic-style thinking-token budget (`off`→0, `low`→4000, `medium`→8000, `high`→16000) on the anthropic-family path. Unset (`null`) falls back to the `thinking_budget` chain, whose default is 8000 — i.e. the effective default effort is `medium`.
184
+ - **Quote `"off"`**: bare YAML `off` parses as the boolean `false`. The reader coerces `false` back to `off`, but quoting keeps `config get thinking.effort` honest.
185
+ - **Provider caveat**: some anthropic-compatible backends reject thinking budgets. The adapter detects the rejection, retries the turn once without the budget, and prints `provider doesn't support thinking — effort off` — set `effort: "off"` to skip the first-turn retry entirely.
186
+ - **Provider capability gate**: other backends *accept* the budget but, lacking a separate reasoning channel, dump the model's chain-of-thought as plain content — the reasoning appears inside the assistant message. `providers.<name>.supports_thinking: false` stops the budget from ever being sent to that backend, regardless of `thinking.effort`. Unset, MiniMax-family model ids (`MiniMax*`/`abab*`) default to `false` (they return no thinking blocks and leak reasoning when sent a budget); set `supports_thinking: true` explicitly to re-enable. For a model outside ruby_llm's registry (`assume_model_exists`, e.g. MiniMax on the anthropic-compatible path) the explicit opt-in sends the Anthropic-style `thinking` block via raw request params — ruby_llm's `with_thinking` only renders for registry models that declare a budget reasoning option and would otherwise raise client-side, silently killing the opt-in.
187
+
188
+ The legacy `display.show_reasoning` boolean maps in only when `display.reasoning` is unset (`true`→full, `false`→hidden); `model.thinking_budget` is likewise superseded by `thinking.effort`.
189
+
190
+ ### streaming
191
+
192
+ ```yaml
193
+ display:
194
+ streaming: true
195
+ reasoning: collapsed # see "reasoning & thinking" above
196
+ show_reasoning: true # LEGACY — superseded by display.reasoning
197
+ language: "en"
198
+ runtime_footer: { enabled: false }
199
+ interim_assistant_messages: false
200
+ statusbar: true # the model + context bar under the chat input
201
+ tool_output_preview_lines: 3 # head lines of tool output shown in the transcript (0 = full dump)
202
+ input_max_rows: 8 # chat input grows up to this many rows, then scrolls
203
+
204
+ paste:
205
+ collapse_lines: 5 # pastes longer than this collapse to a placeholder
206
+ file_threshold_tokens: 8000 # bigger pastes overflow to a session paste_N.txt
207
+
208
+ streaming:
209
+ enabled: true
210
+ transport: "off"
211
+ edit_interval: 0.3
212
+ buffer_threshold: 40
213
+ cursor: " ▉"
214
+
215
+ context:
216
+ engine: "compressor"
217
+ max_tokens: null
218
+ ```
219
+
220
+ - `display.statusbar` (default `true`) pins a dim one-line bar UNDER the chat input — the session mode first (plus the branch/skill tokens when set), then the resolved model id and context saturation, e.g. `default · MiniMax-M3 · ctx ~8.4k/64k (13%)` (the percentage is omitted below 1%). The mode token is the live mode indicator (the prompt itself is a constant `▍❯ `): dim `default`, yellow `plan`, red `yolo`. Saturation uses the REAL usage the provider reported for the last response when available (the full assembled prompt, recorded by the agent loop), else the same chars/4 estimate compaction runs on (`Context::TokenBudget`); the window comes from `model.context_length` / `context.max_tokens`. It refreshes at turn boundaries (after each turn footer, and on session resume), never per stream delta. The percentage turns yellow at 70% and red at 90%; with no usable window only the token count shows. The bar is omitted off a TTY or on terminals narrower than 40 columns.
221
+ - `display.tool_output_preview_lines` (default `3`) caps how many head lines of each tool's output the transcript shows before a dim `… +N lines (full output → context)` marker. DISPLAY-ONLY: the model always receives the full output (subject to the `tool_output` truncation caps) — only the scrollback rendering collapses. Set `0` to restore the old full dump.
222
+ - `display.input_max_rows` (default `8`) caps how many visual rows the chat input grows to as a long or multi-line prompt wraps; past the cap the input scrolls vertically, keeping the caret row in view.
223
+ - `paste.collapse_lines` (default `5`) — the file-backed paste pipeline's first tier. Pasting MORE than this many lines into the chat input inserts a single cyan `[Pasted text #N +M lines]` placeholder instead of flooding the composer; the placeholder is one editable token (backspace deletes it whole, you can type around it, it survives ↑ draft recall and Alt+Enter queueing) and expands to the full pasted body when the message is sent — the model sees everything, while the transcript echo keeps the compact placeholder. Pastes at or under the threshold inline as real rows, exactly as before.
224
+ - `paste.file_threshold_tokens` (default `8000`) — the second tier. A paste estimated above this many tokens (chars/4, the same rule compaction uses) is written to `<RUBINO_HOME>/sessions/<session-id>/paste_N.txt` instead of being held inline, and the sent message carries `[Pasted text #N saved to <path> — too large to inline; read it with the read tool]` so the model reads just the parts it needs. The files persist for the session; `/clear-images` does not touch them (it only drops staged image attachments).
225
+
226
+ ### compression
227
+
228
+ ```yaml
229
+ compression:
230
+ enabled: true
231
+ threshold: 0.50 # Trigger at 50% of context window
232
+ gateway_threshold: 0.85 # Critical threshold
233
+ target_ratio: 0.20 # Compress to 20% of window
234
+ protect_first_n: 3 # Keep first N messages
235
+ protect_last_n: 20 # Keep last N messages
236
+ max_summary_tokens: 12000
237
+ preserve_tool_pairs: true
238
+ ```
239
+
240
+ ### memory
241
+
242
+ ```yaml
243
+ memory:
244
+ enabled: true
245
+ backend: "sqlite" # tiny-Zep FTS5/BM25 + graph-lite recall (default). "default" = legacy non-ranked store
246
+ auto_extract: true
247
+ auto_save: true
248
+ user_profile_enabled: true
249
+ project_context_enabled: true
250
+ memory_char_limit: 2200 # injection budget at RETRIEVAL time
251
+ user_char_limit: 1375
252
+ ingest_char_limit: null # cap on the live set at STORE time (null = unbounded)
253
+ sqlite:
254
+ vector: false # opt-in sqlite-vec/embedding KNN on top of FTS5 (needs RubyLLM.embed)
255
+ graph: true # graph-lite 1-hop entity/edge blend
256
+ ```
257
+
258
+ See [memory.md](memory.md) for the backend internals.
259
+
260
+ ### jobs
261
+
262
+ ```yaml
263
+ jobs:
264
+ mode: "inline" # inline | manual | worker
265
+ poll_interval: 2 # Worker poll interval (seconds)
266
+ max_attempts: 3
267
+ retry_backoff_seconds: 30
268
+ ```
269
+
270
+ ### tasks
271
+
272
+ Caps on the nested-subagent (`task` delegation) tree, all enforced in one place (`Tools::BackgroundTasks#reserve`). See [agents.md](agents.md#nesting-and-caps) for the model.
273
+
274
+ ```yaml
275
+ tasks:
276
+ max_depth: 2 # max nesting depth (human → child → grandchild)
277
+ max_children_per_node: 3 # max LIVE direct children per node
278
+ max_concurrent_total: 8 # hard ceiling on total LIVE subagents across the tree
279
+ max_live_probes_per_child: 5 # per-child budget for billed live probes (probe(live: true))
280
+ ask_parent_timeout: 900 # seconds a blocking ask_parent waits before the child self-heals
281
+ ```
282
+
283
+ ### tools
284
+
285
+ ```yaml
286
+ tools:
287
+ workspace_strict: true # Sandbox write/edit/delete to workspace_root; false = any reachable path
288
+ git: true
289
+ shell: true # ON by default (the agent ships to run inside an isolated VM);
290
+ # every command is still gated by security.require_confirmation_for_shell
291
+ ruby: true
292
+ web: false # Gates BOTH the webfetch and websearch tools
293
+ memory: true
294
+ ```
295
+
296
+ Each tool declares its own `tools.<key>` gate (`Tools::Base#config_key`). A key
297
+ absent from config means the tool is enabled (opt-out model); only an explicit
298
+ `false` disables it. So the keys above are the ones that ship a default — file
299
+ tools (`read`/`write`/`edit`/`multi_edit`/`grep`/`glob`/`apply_patch`),
300
+ `github`, and the rest are on by default and don't need a config entry. Note
301
+ both web tools share a single gate: `tools.web` controls `webfetch` **and**
302
+ `websearch` (there is no `tools.webfetch` / `tools.websearch`).
303
+
304
+ ### tool_output
305
+
306
+ ```yaml
307
+ tool_output:
308
+ max_bytes: 50000
309
+ max_lines: 2000
310
+ max_line_length: 2000
311
+
312
+ file_read:
313
+ max_chars: 100000
314
+ ```
315
+
316
+ ### terminal
317
+
318
+ ```yaml
319
+ terminal:
320
+ backend: "local"
321
+ cwd: null # workspace root override; null = Dir.pwd
322
+ file_sync_enabled: false
323
+ file_sync_max_mb: 100
324
+ ```
325
+
326
+ ### approvals
327
+
328
+ ```yaml
329
+ approvals:
330
+ mode: "manual" # manual | auto | skip
331
+ auto_allow_readonly: true # auto-allow provably read-only shell commands (ls, grep, git log, ...) without a prompt
332
+ readonly_commands: [] # extra command names / leading-token prefixes (e.g. "docker ps") merged into the built-in read-only set
333
+ wait_timeout_seconds: 900 # how long a run waits on a human decision before auto-DENYing (null = forever)
334
+ ```
335
+
336
+ See [security.md](security.md#auto-allowed-read-only-commands) for the read-only parse rules (the hardline floor and `permissions: deny` always run first).
337
+
338
+ ### permissions
339
+
340
+ Pattern-based rules (wildcard support):
341
+
342
+ ```yaml
343
+ permissions:
344
+ "git *": "allow"
345
+ "shell rm -rf *": "deny"
346
+ "shell bundle *": "allow"
347
+ "write ~/.env": "deny"
348
+ "read *": "allow"
349
+ ```
350
+
351
+ Actions: `allow`, `ask`, `deny`
352
+
353
+ ### attachments
354
+
355
+ SSRF guard + secure-by-default file-attachment policy. See [security.md](security.md).
356
+
357
+ The policy is enforced on **every** attachment surface: API/server run attachments and CLI image attachments (`-i`/`--image`, `@image` tokens, dropped paths, `/paste`) all pass the same classification (magic bytes win over extension) and `max_file_bytes` cap **before** anything is sent to a provider. A rejected CLI attachment is a clean one-line error, never a provider call.
358
+
359
+ ```yaml
360
+ attachments:
361
+ allowed_hosts: [] # hosts allowed for URL attachments (loopback always allowed; ALLOWED_FILE_URL_HOSTS env merged in)
362
+ policy:
363
+ max_file_bytes: 26214400 # 25 MB hard cap (checked before reading)
364
+ inline_text_budget_bytes: 100000
365
+ allow_kinds: [image, text, document, archive, binary]
366
+ auto_extract_documents: false
367
+ aux_vision_egress: true
368
+ archive: { max_entries: 2000, max_uncompressed_bytes: 268435456, max_entry_ratio: 100, max_total_ratio: 50, max_nesting_depth: 1 }
369
+ ```
370
+
371
+ ### security
372
+
373
+ ```yaml
374
+ security:
375
+ # confirm_policy: "confirm_all" # confirm_all (default) | dangerous_only; derived from the alias below when absent
376
+ require_confirmation_for_shell: true # legacy alias for confirm_policy; true => confirm_all
377
+ command_allowlist: # prefix-matched commands pre-approved (empty = approve nothing)
378
+ - "git status"
379
+ - "git diff"
380
+ - "bundle exec rspec"
381
+ website_blocklist:
382
+ enabled: false
383
+ domains: []
384
+ shared_files: []
385
+ ```
386
+
387
+ The hardline floor (catastrophic commands) and `permissions: deny` rules always run **before** any allow path, including `yolo`. See [security.md](security.md).
388
+
389
+ ### mcp
390
+
391
+ ```yaml
392
+ mcp:
393
+ servers:
394
+ filesystem:
395
+ transport: stdio
396
+ command: "npx"
397
+ args: ["@modelcontextprotocol/server-filesystem", "."]
398
+ env:
399
+ DEBUG: "1"
400
+ remote_api:
401
+ transport: streamable
402
+ url: "https://mcp.example.com/api"
403
+ headers:
404
+ Authorization: "Bearer {env:MCP_TOKEN}"
405
+ oauth:
406
+ client_id: "{env:MCP_CLIENT_ID}"
407
+ scope: "mcp:read mcp:write"
408
+ timeout: 15000
409
+ ```
410
+
411
+ Experimental. Configuring servers is the opt-in; `mcp.enabled: false` switches MCP off. The `oauth` hash is forwarded verbatim to `ruby_llm-mcp` — rubino implements no OAuth flow itself. See [mcp.md](mcp.md).
412
+
413
+ ### skills
414
+
415
+ ```yaml
416
+ skills:
417
+ enabled: true
418
+ auto_distill: true # post-turn skill distillation (DistillSkillJob); separate from `enabled`
419
+ include_builtin: true # also scan the gem-bundled skills/ catalogue (e.g. ruby-expert)
420
+ paths:
421
+ - ".rubino/skills"
422
+ - "~/.rubino/skills"
423
+ ```
424
+
425
+ The agent loads a skill's instructions on demand (`tools.skill` gates the loading
426
+ tool). With `skills.enabled` (default true) the agent also creates skills: the
427
+ deterministic post-turn `DistillSkillJob` distils complex, repeatable runs into a
428
+ new skill (gated on a tool-count threshold, default 5 — set
429
+ `RA_DISTILL_TOOL_THRESHOLD` to tune), and the agent can author one on demand via
430
+ `skill(action: "create", ...)`. Setting `skills.enabled: false` turns off both
431
+ the distillation cost and the create affordance.
432
+
433
+ Skill activity is exported on `GET /v1/metrics` as two Prometheus counters:
434
+
435
+ - `skills_loaded_total` — number of times a skill body was successfully loaded via
436
+ the `skill` tool (usage/adoption).
437
+ - `skills_created_total` — number of new skills created (the on-demand create tool
438
+ and the registry's re-scan disk-diff both feed this).
439
+
440
+ A successful load emits the `SKILL_LOADED` event (`skill.loaded`); a creation emits
441
+ `SKILL_CREATED`. See **[docs/skills.md](skills.md)** for the full skill system,
442
+ including creation and the 3-level disclosure model.
443
+
444
+ ### commands
445
+
446
+ ```yaml
447
+ commands:
448
+ paths:
449
+ - ".rubino/commands"
450
+ - "~/.rubino/commands"
451
+ shell_injection_enabled: false # true = allow !`shell` interpolation in command templates
452
+ ```
453
+
454
+ ### formatters
455
+
456
+ ```yaml
457
+ formatters:
458
+ "*.rb": "rubocop -A --fail-level=fatal"
459
+ "*.js": "prettier --write"
460
+ "*.ts": "prettier --write"
461
+ "*.py": "black"
462
+ ```
463
+
464
+ ### agents
465
+
466
+ Custom agent definitions:
467
+
468
+ ```yaml
469
+ agents:
470
+ security:
471
+ type: subagent
472
+ model: "anthropic/claude-sonnet-4-20250514"
473
+ description: "Security-focused code review"
474
+ tools: [read, grep, glob]
475
+ mcp_servers: []
476
+ ```
477
+
478
+ ### prompts
479
+
480
+ System-prompt layering. The defaults ship the built-in role prompts.
481
+
482
+ ```yaml
483
+ prompts:
484
+ preamble: null # block prepended after the role identity (customer context)
485
+ environment:
486
+ enabled: true # inject an [Environment] block (date/OS/cwd/git/runtimes/PATH utilities)
487
+ extra_utilities: [] # extra binaries to probe beyond the defaults
488
+ overrides: {} # prompts.overrides.<role> fully replaces a built-in role prompt
489
+ ```
490
+
491
+ ### clarify / worktree / privacy / quick_commands
492
+
493
+ ```yaml
494
+ clarify:
495
+ timeout: 120 # seconds to wait for a clarification answer
496
+
497
+ worktree:
498
+ enabled: false # run in a git worktree
499
+
500
+ privacy:
501
+ redact_pii: false
502
+
503
+ quick_commands: {} # named one-line shortcuts
504
+ ```
505
+
506
+ ### formatters
507
+
508
+ ```yaml
509
+ formatters:
510
+ "*.rb": "rubocop -A --fail-level=fatal"
511
+ "*.js": "prettier --write"
512
+ "*.ts": "prettier --write"
513
+ "*.py": "black"
514
+ ```
515
+
516
+ ### agents (planned)
517
+
518
+ Custom agent definitions (multi-agent routing is not fully wired yet — see [agents.md](agents.md)):
519
+
520
+ ```yaml
521
+ agents:
522
+ security:
523
+ type: subagent
524
+ model: "anthropic/claude-sonnet-4-20250514"
525
+ description: "Security-focused code review"
526
+ tools: [read, grep, glob]
527
+ mcp_servers: []
528
+ ```
529
+
530
+ ### server / api
531
+
532
+ ```yaml
533
+ server:
534
+ port: 4820
535
+ auth: false
536
+
537
+ api:
538
+ max_body_bytes: 5242880 # 5 MB cap on JSON request bodies (413 past this)
539
+ max_upload_bytes: 52428800 # 50 MB cap on multipart uploads
540
+ rate_limit_enabled: true
541
+ rate_limit_unauth_per_minute: 60
542
+ rate_limit_auth_per_minute: 600
543
+ ```
544
+
545
+ ---
546
+
547
+ ## Environment Variables
548
+
549
+ ### Provider keys
550
+
551
+ | Variable | Purpose |
552
+ |----------|---------|
553
+ | `MINIMAX_API_KEY` | MiniMax API key |
554
+ | `OPENAI_API_KEY` | OpenAI key (also the fallback for `openai_compatible` gateways) |
555
+ | `ANTHROPIC_API_KEY` | Anthropic key (also the fallback for `anthropic_compatible` gateways) |
556
+ | `GEMINI_API_KEY` / `GOOGLE_API_KEY` | Google Gemini key |
557
+ | `BEDROCK_API_KEY` | AWS Bedrock bearer key |
558
+
559
+ ### Agent runtime
560
+
561
+ | Variable | Purpose |
562
+ |----------|---------|
563
+ | `RUBINO_HOME` | Relocate the home dir; config, `.env`, and the database all follow it |
564
+ | `RUBINO_ALLOW_FAKE` | `1` to allow the fake provider in `chat`/`server` (dev only) |
565
+ | `RUBINO_HYPERLINKS` | Toggle terminal hyperlink output |
566
+ | `RUBINO_LOG_LEVEL` / `RUBINO_LOG_FORMAT` | Logging verbosity / format |
567
+
568
+ ### HTTP API server
569
+
570
+ | Variable | Purpose |
571
+ |----------|---------|
572
+ | `RUBINO_API_KEY` | Bearer token required on every API request |
573
+ | `RUBINO_API_HOST` / `RUBINO_API_PORT` | Bind interface / port |
574
+ | `RUBINO_ENCRYPTION_KEY` | Required to encrypt OAuth tokens at rest |
575
+ | `RUBINO_TLS` | `1` to serve the API over a self-signed, client-pinned cert |
576
+ | `RUBINO_WEBHOOK_URL` / `RUBINO_WEBHOOK_SECRET` | Outbound webhook target + signing secret |
577
+
578
+ ### Tools & network
579
+
580
+ | Variable | Purpose |
581
+ |----------|---------|
582
+ | `GITHUB_TOKEN` | GitHub access token for the `github` tool |
583
+ | `TAVILY_API_KEY` | Tavily search key for `websearch` |
584
+ | `SEARXNG_URL` | SearXNG instance URL for `websearch` |
585
+ | `ALLOWED_FILE_URL_HOSTS` | Comma-separated extra hosts for URL attachments (merged with `attachments.allowed_hosts`) |
586
+ | `SUDO_PASSWORD` | When set, relaxes the `sudo -S` hardline guard |
587
+ | `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY` | Standard network proxy (full HTTP/HTTPS/SOCKS support) |
588
+ | `SSL_CERT_FILE` | Custom CA certificate bundle |
589
+
590
+ > There is no `RUBINO_PROXY_URL`; the agent uses the standard `HTTP_PROXY`/`HTTPS_PROXY`/`NO_PROXY` variables (and SOCKS) for outbound network proxying.