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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +115 -0
- data/.rubocop_todo.yml +955 -0
- data/.ruby-version +1 -0
- data/AGENTS.md +97 -0
- data/CHANGELOG.md +344 -0
- data/CONTRIBUTING.md +69 -0
- data/LICENSE +21 -0
- data/README.md +200 -0
- data/Rakefile +8 -0
- data/docs/agents.md +190 -0
- data/docs/api/v1.md +414 -0
- data/docs/architecture.md +177 -0
- data/docs/commands.md +375 -0
- data/docs/configuration.md +590 -0
- data/docs/getting-started.md +143 -0
- data/docs/jobs.md +332 -0
- data/docs/mcp.md +128 -0
- data/docs/memory.md +98 -0
- data/docs/models-and-keys.md +173 -0
- data/docs/oauth-providers.md +145 -0
- data/docs/plugins.md +195 -0
- data/docs/security.md +145 -0
- data/docs/skills.md +322 -0
- data/docs/tools.md +395 -0
- data/docs/troubleshooting.md +73 -0
- data/exe/rubino +9 -0
- data/install.sh +275 -0
- data/lib/rubino/active_skill.rb +50 -0
- data/lib/rubino/agent/agent_registry.rb +120 -0
- data/lib/rubino/agent/backoff_policy.rb +116 -0
- data/lib/rubino/agent/definition.rb +128 -0
- data/lib/rubino/agent/degenerate_recovery.rb +271 -0
- data/lib/rubino/agent/fallback_chain.rb +194 -0
- data/lib/rubino/agent/iteration_budget.rb +50 -0
- data/lib/rubino/agent/loop.rb +617 -0
- data/lib/rubino/agent/model_call_runner.rb +383 -0
- data/lib/rubino/agent/prompts/build.txt +69 -0
- data/lib/rubino/agent/prompts/compaction.txt +20 -0
- data/lib/rubino/agent/prompts/explore.txt +19 -0
- data/lib/rubino/agent/prompts/general.txt +20 -0
- data/lib/rubino/agent/prompts/plan.txt +31 -0
- data/lib/rubino/agent/response_validator.rb +70 -0
- data/lib/rubino/agent/router.rb +65 -0
- data/lib/rubino/agent/runner.rb +195 -0
- data/lib/rubino/agent/tool_executor.rb +402 -0
- data/lib/rubino/agent/truncation_continuation.rb +137 -0
- data/lib/rubino/api/middleware/auth.rb +43 -0
- data/lib/rubino/api/middleware/error_handler.rb +65 -0
- data/lib/rubino/api/middleware/json_parser.rb +100 -0
- data/lib/rubino/api/middleware/observability.rb +59 -0
- data/lib/rubino/api/middleware/rate_limit.rb +136 -0
- data/lib/rubino/api/operations/approvals/decide_operation.rb +49 -0
- data/lib/rubino/api/operations/clarifications/decide_operation.rb +44 -0
- data/lib/rubino/api/operations/cron_jobs/create_operation.rb +46 -0
- data/lib/rubino/api/operations/cron_jobs/delete_operation.rb +36 -0
- data/lib/rubino/api/operations/cron_jobs/list_operation.rb +55 -0
- data/lib/rubino/api/operations/cron_jobs/pause_operation.rb +34 -0
- data/lib/rubino/api/operations/cron_jobs/resume_operation.rb +34 -0
- data/lib/rubino/api/operations/cron_jobs/schedule_validation.rb +30 -0
- data/lib/rubino/api/operations/cron_jobs/show_operation.rb +32 -0
- data/lib/rubino/api/operations/cron_jobs/trigger_operation.rb +38 -0
- data/lib/rubino/api/operations/cron_jobs/update_operation.rb +42 -0
- data/lib/rubino/api/operations/files/read_operation.rb +40 -0
- data/lib/rubino/api/operations/files/upload_operation.rb +175 -0
- data/lib/rubino/api/operations/health_operation.rb +46 -0
- data/lib/rubino/api/operations/memory/delete_operation.rb +32 -0
- data/lib/rubino/api/operations/memory/index_operation.rb +80 -0
- data/lib/rubino/api/operations/memory/stats_operation.rb +28 -0
- data/lib/rubino/api/operations/metrics_operation.rb +18 -0
- data/lib/rubino/api/operations/mode/show_operation.rb +29 -0
- data/lib/rubino/api/operations/mode/update_operation.rb +42 -0
- data/lib/rubino/api/operations/models/list_operation.rb +45 -0
- data/lib/rubino/api/operations/oauth/connections/disconnect_operation.rb +77 -0
- data/lib/rubino/api/operations/oauth/connections/list_operation.rb +36 -0
- data/lib/rubino/api/operations/oauth/providers/callback_operation.rb +82 -0
- data/lib/rubino/api/operations/oauth/providers/connect_operation.rb +44 -0
- data/lib/rubino/api/operations/oauth/providers/list_operation.rb +35 -0
- data/lib/rubino/api/operations/oauth/serializer.rb +21 -0
- data/lib/rubino/api/operations/runs/create_operation.rb +77 -0
- data/lib/rubino/api/operations/runs/events_operation.rb +195 -0
- data/lib/rubino/api/operations/runs/stop_operation.rb +34 -0
- data/lib/rubino/api/operations/sessions/create_operation.rb +46 -0
- data/lib/rubino/api/operations/sessions/delete_operation.rb +33 -0
- data/lib/rubino/api/operations/sessions/index_operation.rb +82 -0
- data/lib/rubino/api/operations/sessions/retry_operation.rb +45 -0
- data/lib/rubino/api/operations/sessions/show_operation.rb +59 -0
- data/lib/rubino/api/operations/sessions/undo_operation.rb +38 -0
- data/lib/rubino/api/operations/skills/list_operation.rb +34 -0
- data/lib/rubino/api/operations/skills/toggle_operation.rb +40 -0
- data/lib/rubino/api/operations/tasks/index_operation.rb +30 -0
- data/lib/rubino/api/operations/tasks/serializer.rb +60 -0
- data/lib/rubino/api/operations/tasks/show_operation.rb +33 -0
- data/lib/rubino/api/operations/tasks/stop_operation.rb +47 -0
- data/lib/rubino/api/request.rb +54 -0
- data/lib/rubino/api/responses.rb +64 -0
- data/lib/rubino/api/router.rb +72 -0
- data/lib/rubino/api/schemas.rb +103 -0
- data/lib/rubino/api/server.rb +102 -0
- data/lib/rubino/api/tls.rb +108 -0
- data/lib/rubino/attachments/classification.rb +16 -0
- data/lib/rubino/attachments/classify.rb +171 -0
- data/lib/rubino/attachments/defang.rb +47 -0
- data/lib/rubino/attachments/policy.rb +36 -0
- data/lib/rubino/attachments/preamble.rb +120 -0
- data/lib/rubino/boot/encryption_key.rb +32 -0
- data/lib/rubino/cli/chat/bang_shell.rb +257 -0
- data/lib/rubino/cli/chat/completion_builder.rb +290 -0
- data/lib/rubino/cli/chat/idle_card_host.rb +69 -0
- data/lib/rubino/cli/chat/image_inbox.rb +168 -0
- data/lib/rubino/cli/chat/session_resolver.rb +176 -0
- data/lib/rubino/cli/chat_command.rb +1674 -0
- data/lib/rubino/cli/commands.rb +250 -0
- data/lib/rubino/cli/config_command.rb +96 -0
- data/lib/rubino/cli/doctor_command.rb +251 -0
- data/lib/rubino/cli/jobs_command.rb +60 -0
- data/lib/rubino/cli/memory_command.rb +135 -0
- data/lib/rubino/cli/onboarding_wizard.rb +207 -0
- data/lib/rubino/cli/server_command.rb +139 -0
- data/lib/rubino/cli/session_command.rb +125 -0
- data/lib/rubino/cli/setup_command.rb +107 -0
- data/lib/rubino/cli/skills_command.rb +85 -0
- data/lib/rubino/cli/tools_command.rb +81 -0
- data/lib/rubino/cli/trust_gate.rb +71 -0
- data/lib/rubino/commands/built_ins.rb +46 -0
- data/lib/rubino/commands/command.rb +116 -0
- data/lib/rubino/commands/executor.rb +550 -0
- data/lib/rubino/commands/handlers/agents.rb +510 -0
- data/lib/rubino/commands/handlers/config.rb +88 -0
- data/lib/rubino/commands/handlers/help.rb +148 -0
- data/lib/rubino/commands/handlers/jobs.rb +71 -0
- data/lib/rubino/commands/handlers/mcp.rb +229 -0
- data/lib/rubino/commands/handlers/memory.rb +200 -0
- data/lib/rubino/commands/handlers/sessions.rb +207 -0
- data/lib/rubino/commands/handlers/skills.rb +195 -0
- data/lib/rubino/commands/handlers/status.rb +211 -0
- data/lib/rubino/commands/loader.rb +90 -0
- data/lib/rubino/config/configuration.rb +455 -0
- data/lib/rubino/config/defaults.rb +569 -0
- data/lib/rubino/config/loader.rb +115 -0
- data/lib/rubino/config/reasoning_prefs.rb +67 -0
- data/lib/rubino/config/writer.rb +72 -0
- data/lib/rubino/context/compressor.rb +149 -0
- data/lib/rubino/context/environment_inspector.rb +176 -0
- data/lib/rubino/context/file_discovery.rb +45 -0
- data/lib/rubino/context/message_boundary.rb +39 -0
- data/lib/rubino/context/prompt_assembler.rb +382 -0
- data/lib/rubino/context/summary_builder.rb +159 -0
- data/lib/rubino/context/token_budget.rb +68 -0
- data/lib/rubino/context/tool_pair_sanitizer.rb +70 -0
- data/lib/rubino/database/connection.rb +77 -0
- data/lib/rubino/database/migrations/001_create_initial_schema.rb +156 -0
- data/lib/rubino/database/migrations/002_create_runs.rb +45 -0
- data/lib/rubino/database/migrations/003_create_skill_states.rb +15 -0
- data/lib/rubino/database/migrations/004_create_cron_jobs.rb +36 -0
- data/lib/rubino/database/migrations/005_create_oauth_connections.rb +27 -0
- data/lib/rubino/database/migrations/006_create_webhook_deliveries.rb +34 -0
- data/lib/rubino/database/migrations/007_create_messages_fts.rb +59 -0
- data/lib/rubino/database/migrations/008_create_memory_facts.rb +75 -0
- data/lib/rubino/database/migrations/009_create_memory_graph.rb +55 -0
- data/lib/rubino/database/migrations/010_add_owner_pid_to_sessions.rb +20 -0
- data/lib/rubino/database/migrator.rb +48 -0
- data/lib/rubino/documents/converters/csv.rb +79 -0
- data/lib/rubino/documents/converters/docx.rb +129 -0
- data/lib/rubino/documents/converters/html.rb +28 -0
- data/lib/rubino/documents/converters/json.rb +35 -0
- data/lib/rubino/documents/converters/pdf.rb +59 -0
- data/lib/rubino/documents/converters/plain.rb +68 -0
- data/lib/rubino/documents/converters/pptx.rb +64 -0
- data/lib/rubino/documents/converters/xlsx.rb +62 -0
- data/lib/rubino/documents/converters/xml.rb +45 -0
- data/lib/rubino/documents/html.rb +71 -0
- data/lib/rubino/documents/registry.rb +68 -0
- data/lib/rubino/documents/table.rb +63 -0
- data/lib/rubino/documents.rb +50 -0
- data/lib/rubino/errors.rb +119 -0
- data/lib/rubino/files/workspace.rb +93 -0
- data/lib/rubino/interaction/cancel_token.rb +43 -0
- data/lib/rubino/interaction/clipboard_image.rb +84 -0
- data/lib/rubino/interaction/event_bus.rb +48 -0
- data/lib/rubino/interaction/events.rb +101 -0
- data/lib/rubino/interaction/image_input.rb +127 -0
- data/lib/rubino/interaction/input_queue.rb +117 -0
- data/lib/rubino/interaction/lifecycle.rb +299 -0
- data/lib/rubino/interaction/probe.rb +65 -0
- data/lib/rubino/interaction/state.rb +56 -0
- data/lib/rubino/jobs/cron_job_repository.rb +75 -0
- data/lib/rubino/jobs/handlers/cleanup_sessions_job.rb +32 -0
- data/lib/rubino/jobs/handlers/compact_session_job.rb +21 -0
- data/lib/rubino/jobs/handlers/distill_skill_job.rb +186 -0
- data/lib/rubino/jobs/handlers/extract_memory_job.rb +37 -0
- data/lib/rubino/jobs/handlers/summarize_session_job.rb +21 -0
- data/lib/rubino/jobs/queue.rb +184 -0
- data/lib/rubino/jobs/registry.rb +45 -0
- data/lib/rubino/jobs/runner.rb +79 -0
- data/lib/rubino/jobs/scheduler.rb +138 -0
- data/lib/rubino/jobs/webhook_delivery.rb +225 -0
- data/lib/rubino/jobs/worker.rb +59 -0
- data/lib/rubino/llm/adapter_factory.rb +47 -0
- data/lib/rubino/llm/adapter_response.rb +65 -0
- data/lib/rubino/llm/auxiliary_client.rb +61 -0
- data/lib/rubino/llm/bedrock_bearer_client.rb +235 -0
- data/lib/rubino/llm/content_builder.rb +55 -0
- data/lib/rubino/llm/credential_check.rb +93 -0
- data/lib/rubino/llm/error_classifier.rb +364 -0
- data/lib/rubino/llm/fake_provider.rb +292 -0
- data/lib/rubino/llm/inline_think_filter.rb +58 -0
- data/lib/rubino/llm/model_catalog.rb +29 -0
- data/lib/rubino/llm/provider_resolver.rb +48 -0
- data/lib/rubino/llm/reasoning_manager.rb +100 -0
- data/lib/rubino/llm/request.rb +56 -0
- data/lib/rubino/llm/ruby_llm_adapter.rb +794 -0
- data/lib/rubino/llm/scenario_loader.rb +68 -0
- data/lib/rubino/llm/scenario_selector.rb +80 -0
- data/lib/rubino/llm/scenarios/agent-creates-cron-failure.yml +29 -0
- data/lib/rubino/llm/scenarios/agent-creates-cron.yml +36 -0
- data/lib/rubino/llm/scenarios/analysis.yml +501 -0
- data/lib/rubino/llm/scenarios/complex-analysis.yml +598 -0
- data/lib/rubino/llm/scenarios/failure.yml +65 -0
- data/lib/rubino/llm/scenarios/happy-path.yml +24 -0
- data/lib/rubino/llm/scenarios/provider-quota-completed.yml +14 -0
- data/lib/rubino/llm/scenarios/wide-table.yml +121 -0
- data/lib/rubino/llm/scenarios/with-approvals.yml +50 -0
- data/lib/rubino/llm/scenarios/with-artifacts.yml +98 -0
- data/lib/rubino/llm/scenarios/with-clarify.yml +32 -0
- data/lib/rubino/llm/scenarios/with-reasoning.yml +175 -0
- data/lib/rubino/llm/scenarios/with-uploads.yml +104 -0
- data/lib/rubino/llm/thinking_support.rb +84 -0
- data/lib/rubino/llm/tool_bridge.rb +89 -0
- data/lib/rubino/logger.rb +99 -0
- data/lib/rubino/mcp/manager.rb +180 -0
- data/lib/rubino/mcp/mcp_tool_wrapper.rb +69 -0
- data/lib/rubino/mcp.rb +57 -0
- data/lib/rubino/memory/backend.rb +104 -0
- data/lib/rubino/memory/backends/default.rb +101 -0
- data/lib/rubino/memory/backends/sqlite.rb +653 -0
- data/lib/rubino/memory/backends.rb +53 -0
- data/lib/rubino/memory/deduplicator.rb +74 -0
- data/lib/rubino/memory/extractor.rb +85 -0
- data/lib/rubino/memory/flusher.rb +31 -0
- data/lib/rubino/memory/retriever.rb +50 -0
- data/lib/rubino/memory/sqlite_extraction_prompt.rb +70 -0
- data/lib/rubino/memory/sqlite_graph.rb +154 -0
- data/lib/rubino/memory/store.rb +228 -0
- data/lib/rubino/memory/threat_scanner.rb +68 -0
- data/lib/rubino/metrics.rb +175 -0
- data/lib/rubino/modes.rb +93 -0
- data/lib/rubino/oauth/connection_repository.rb +95 -0
- data/lib/rubino/oauth/provider/github.rb +75 -0
- data/lib/rubino/oauth/provider/google.rb +59 -0
- data/lib/rubino/oauth/provider.rb +149 -0
- data/lib/rubino/oauth/registry.rb +86 -0
- data/lib/rubino/oauth/token_encryptor.rb +87 -0
- data/lib/rubino/plugins/registry.rb +75 -0
- data/lib/rubino/plugins.rb +86 -0
- data/lib/rubino/run/approval_gate.rb +243 -0
- data/lib/rubino/run/attachment_downloader.rb +166 -0
- data/lib/rubino/run/event_store.rb +74 -0
- data/lib/rubino/run/executor.rb +383 -0
- data/lib/rubino/run/gate_registry.rb +39 -0
- data/lib/rubino/run/recorder.rb +69 -0
- data/lib/rubino/run/repository.rb +118 -0
- data/lib/rubino/run/session_approval_cache.rb +118 -0
- data/lib/rubino/security/allowlist_persister.rb +55 -0
- data/lib/rubino/security/approval_policy.rb +227 -0
- data/lib/rubino/security/command_allowlist.rb +24 -0
- data/lib/rubino/security/dangerous_patterns.rb +118 -0
- data/lib/rubino/security/deny_persister.rb +73 -0
- data/lib/rubino/security/doom_loop_detector.rb +43 -0
- data/lib/rubino/security/hardline_guard.rb +105 -0
- data/lib/rubino/security/pattern_matcher.rb +62 -0
- data/lib/rubino/security/prefix_deriver.rb +124 -0
- data/lib/rubino/security/readonly_commands.rb +211 -0
- data/lib/rubino/session/exporter.rb +101 -0
- data/lib/rubino/session/message.rb +77 -0
- data/lib/rubino/session/repository.rb +295 -0
- data/lib/rubino/session/store.rb +198 -0
- data/lib/rubino/session/summary_store.rb +65 -0
- data/lib/rubino/skills/prompt_index.rb +85 -0
- data/lib/rubino/skills/registry.rb +208 -0
- data/lib/rubino/skills/skill.rb +176 -0
- data/lib/rubino/skills/skill_tool.rb +215 -0
- data/lib/rubino/skills/state_repository.rb +37 -0
- data/lib/rubino/skills/toggle.rb +26 -0
- data/lib/rubino/tools/answer_child_tool.rb +83 -0
- data/lib/rubino/tools/ask_parent_tool.rb +232 -0
- data/lib/rubino/tools/attach_file_tool.rb +120 -0
- data/lib/rubino/tools/background_tasks.rb +520 -0
- data/lib/rubino/tools/base.rb +222 -0
- data/lib/rubino/tools/custom_tool_loader.rb +119 -0
- data/lib/rubino/tools/edit_tool.rb +122 -0
- data/lib/rubino/tools/git_tool.rb +71 -0
- data/lib/rubino/tools/github_tool.rb +233 -0
- data/lib/rubino/tools/glob_tool.rb +69 -0
- data/lib/rubino/tools/grep_tool.rb +206 -0
- data/lib/rubino/tools/memory_tool.rb +184 -0
- data/lib/rubino/tools/multi_edit_tool.rb +110 -0
- data/lib/rubino/tools/patch_tool.rb +260 -0
- data/lib/rubino/tools/probe_tool.rb +175 -0
- data/lib/rubino/tools/question_tool.rb +128 -0
- data/lib/rubino/tools/read_attachment_tool.rb +180 -0
- data/lib/rubino/tools/read_tool.rb +212 -0
- data/lib/rubino/tools/read_tracker.rb +98 -0
- data/lib/rubino/tools/registry.rb +166 -0
- data/lib/rubino/tools/result.rb +113 -0
- data/lib/rubino/tools/ruby_tool.rb +0 -0
- data/lib/rubino/tools/session_search_tool.rb +103 -0
- data/lib/rubino/tools/shell_input_tool.rb +96 -0
- data/lib/rubino/tools/shell_kill_tool.rb +76 -0
- data/lib/rubino/tools/shell_output_tool.rb +72 -0
- data/lib/rubino/tools/shell_registry.rb +158 -0
- data/lib/rubino/tools/shell_tail_tool.rb +118 -0
- data/lib/rubino/tools/shell_tool.rb +330 -0
- data/lib/rubino/tools/steer_tool.rb +118 -0
- data/lib/rubino/tools/subagent_probe.rb +89 -0
- data/lib/rubino/tools/summarize_file_tool.rb +182 -0
- data/lib/rubino/tools/task_result_tool.rb +90 -0
- data/lib/rubino/tools/task_stop_tool.rb +80 -0
- data/lib/rubino/tools/task_tool.rb +622 -0
- data/lib/rubino/tools/test_tool.rb +454 -0
- data/lib/rubino/tools/todo_tool.rb +93 -0
- data/lib/rubino/tools/tool_call_repository.rb +33 -0
- data/lib/rubino/tools/vision_tool.rb +85 -0
- data/lib/rubino/tools/webfetch_tool.rb +153 -0
- data/lib/rubino/tools/websearch_tool.rb +179 -0
- data/lib/rubino/tools/write_tool.rb +61 -0
- data/lib/rubino/trust.rb +88 -0
- data/lib/rubino/ui/api.rb +296 -0
- data/lib/rubino/ui/base.rb +252 -0
- data/lib/rubino/ui/bottom_composer.rb +1599 -0
- data/lib/rubino/ui/cli.rb +1987 -0
- data/lib/rubino/ui/completion_menu.rb +321 -0
- data/lib/rubino/ui/completion_source.rb +284 -0
- data/lib/rubino/ui/escape_reader.rb +169 -0
- data/lib/rubino/ui/indented_io.rb +88 -0
- data/lib/rubino/ui/input_history.rb +108 -0
- data/lib/rubino/ui/live_region.rb +183 -0
- data/lib/rubino/ui/markdown_renderer.rb +506 -0
- data/lib/rubino/ui/notifier.rb +163 -0
- data/lib/rubino/ui/null.rb +195 -0
- data/lib/rubino/ui/paste_store.rb +176 -0
- data/lib/rubino/ui/printer_base.rb +79 -0
- data/lib/rubino/ui/probe_wait_indicator.rb +75 -0
- data/lib/rubino/ui/queued_indicators.rb +66 -0
- data/lib/rubino/ui/status_bar.rb +100 -0
- data/lib/rubino/ui/stdout_proxy.rb +161 -0
- data/lib/rubino/ui/streaming_markdown.rb +186 -0
- data/lib/rubino/ui/subagent_cards.rb +134 -0
- data/lib/rubino/ui/subagent_view.rb +255 -0
- data/lib/rubino/ui.rb +21 -0
- data/lib/rubino/update_check.rb +187 -0
- data/lib/rubino/util/duration.rb +23 -0
- data/lib/rubino/util/hyperlink.rb +105 -0
- data/lib/rubino/util/output.rb +145 -0
- data/lib/rubino/util/secrets_mask.rb +83 -0
- data/lib/rubino/version.rb +5 -0
- data/lib/rubino/workspace.rb +85 -0
- data/lib/rubino-agent.rb +5 -0
- data/lib/rubino.rb +318 -0
- data/mise.toml +2 -0
- data/rubino-agent.gemspec +103 -0
- data/skills/ruby-expert/SKILL.md +67 -0
- data/skills/ruby-expert/references/concurrency.md +357 -0
- data/skills/ruby-expert/references/datetime-and-encoding.md +363 -0
- data/skills/ruby-expert/references/errors-and-types.md +460 -0
- data/skills/ruby-expert/references/gem-authoring.md +459 -0
- data/skills/ruby-expert/references/language-idioms.md +465 -0
- data/skills/ruby-expert/references/metaprogramming.md +339 -0
- data/skills/ruby-expert/references/oo-design.md +553 -0
- data/skills/ruby-expert/references/performance.md +383 -0
- data/skills/ruby-expert/references/rails.md +424 -0
- data/skills/ruby-expert/references/security.md +404 -0
- data/skills/ruby-expert/references/testing.md +473 -0
- data/skills/ruby-expert/references/tooling.md +466 -0
- metadata +856 -0
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubino
|
|
4
|
+
module Config
|
|
5
|
+
# Default configuration values for the entire system.
|
|
6
|
+
# These mirror the Rich config structure adapted for Ruby.
|
|
7
|
+
module Defaults
|
|
8
|
+
# Sentinel for the default database path. When config still carries this
|
|
9
|
+
# value, Configuration#database_path resolves it against the resolved
|
|
10
|
+
# home (RUBINO_HOME) instead of a literal ~/.rubino (issue #96).
|
|
11
|
+
DEFAULT_DATABASE_PATH = "<RUBINO_HOME>/rubino.sqlite3"
|
|
12
|
+
|
|
13
|
+
# Sentinel for the user-home commands directory. Resolved at read time
|
|
14
|
+
# (Commands::Loader/Executor) against the resolved home (RUBINO_HOME)
|
|
15
|
+
# instead of a literal ~/.rubino so commands in a custom home are
|
|
16
|
+
# actually discovered (issue #38).
|
|
17
|
+
HOME_COMMANDS_PATH = "<RUBINO_HOME>/commands"
|
|
18
|
+
|
|
19
|
+
MODULE_DEFAULTS = {
|
|
20
|
+
"model" => {
|
|
21
|
+
"default" => "openai/gpt-4.1",
|
|
22
|
+
"provider" => "auto",
|
|
23
|
+
"context_length" => nil,
|
|
24
|
+
"temperature" => 0.3,
|
|
25
|
+
# Max output tokens for the anthropic-family path (anthropic_compatible
|
|
26
|
+
# MiniMax, native anthropic, bedrock). ruby_llm defaults the Anthropic
|
|
27
|
+
# max_tokens to 4096, which a reasoning model can exhaust on thinking
|
|
28
|
+
# tokens alone → empty visible text. nil = use the adapter default
|
|
29
|
+
# (16384). providers.<name>.max_tokens overrides per-backend.
|
|
30
|
+
"max_tokens" => nil,
|
|
31
|
+
# Thinking/reasoning token budget for the anthropic-family path. nil =
|
|
32
|
+
# adapter default (8000, the reference "medium"). 0 disables thinking.
|
|
33
|
+
# providers.<name>.thinking_budget overrides per-backend.
|
|
34
|
+
"thinking_budget" => nil,
|
|
35
|
+
# Visible-output headroom (tokens) reserved on top of the thinking
|
|
36
|
+
# budget so the model can think AND answer. Mirrors the reference +4096.
|
|
37
|
+
"max_tokens_text_headroom" => 4096,
|
|
38
|
+
# nil = auto-detect from model_id via LLM::ContentBuilder.supports_vision?.
|
|
39
|
+
# Set to true/false to override (e.g. when running behind a gateway that
|
|
40
|
+
# hides the real upstream model name, like the gateway provider's `auto`).
|
|
41
|
+
"supports_vision" => nil
|
|
42
|
+
},
|
|
43
|
+
"providers" => {
|
|
44
|
+
"openai" => {
|
|
45
|
+
"base_url" => nil,
|
|
46
|
+
# Per-READ socket inactivity (resets on every streamed chunk), NOT a
|
|
47
|
+
# total — this is the agent's first-token + inter-token idle bound,
|
|
48
|
+
# same as the OpenAI/Anthropic SDK default. A silent socket fails
|
|
49
|
+
# within this window and is retried pre-first-token. Raise it for a
|
|
50
|
+
# large local Ollama that cold-loads for minutes before token #1.
|
|
51
|
+
"request_timeout_seconds" => 600,
|
|
52
|
+
"stale_timeout_seconds" => 300
|
|
53
|
+
},
|
|
54
|
+
"anthropic" => {
|
|
55
|
+
"base_url" => nil,
|
|
56
|
+
"request_timeout_seconds" => 600
|
|
57
|
+
},
|
|
58
|
+
"bedrock" => {
|
|
59
|
+
"region" => "us-east-1",
|
|
60
|
+
"request_timeout_seconds" => 600
|
|
61
|
+
},
|
|
62
|
+
"gemini" => {
|
|
63
|
+
"request_timeout_seconds" => 600
|
|
64
|
+
},
|
|
65
|
+
# Opt-in provider for an OpenAI-compatible gateway. Point it at any
|
|
66
|
+
# gateway that exposes an OpenAI-style /v1/* API: set base_url and
|
|
67
|
+
# api_key and the agent routes everything here regardless of model id.
|
|
68
|
+
# The gateway decides which upstream (OpenAI/Anthropic/…) and model
|
|
69
|
+
# to call. Set model.provider: "gateway" to enable.
|
|
70
|
+
"gateway" => {
|
|
71
|
+
"openai_compatible" => true,
|
|
72
|
+
"assume_model_exists" => true,
|
|
73
|
+
"base_url" => nil,
|
|
74
|
+
"request_timeout_seconds" => 600
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
"auxiliary" => {
|
|
78
|
+
"compression" => {
|
|
79
|
+
"provider" => "main",
|
|
80
|
+
"model" => "",
|
|
81
|
+
"base_url" => nil,
|
|
82
|
+
"timeout" => 120
|
|
83
|
+
},
|
|
84
|
+
"approval" => {
|
|
85
|
+
"provider" => "main",
|
|
86
|
+
"model" => "",
|
|
87
|
+
"base_url" => nil,
|
|
88
|
+
"timeout" => 30
|
|
89
|
+
},
|
|
90
|
+
# Multimodal aux. When set, the `vision` tool delegates here so a
|
|
91
|
+
# text-only primary can still "see" an image. `provider: "main"`
|
|
92
|
+
# reuses the primary's provider/base_url; otherwise both can be
|
|
93
|
+
# overridden. Set `model: "auto-vision"` to let the gateway proxy
|
|
94
|
+
# pick a vision model from the model catalog.
|
|
95
|
+
"vision" => {
|
|
96
|
+
"provider" => "main",
|
|
97
|
+
"model" => "",
|
|
98
|
+
"base_url" => nil,
|
|
99
|
+
"timeout" => 120
|
|
100
|
+
},
|
|
101
|
+
# Document summarization. The `summarize_file` tool delegates here so
|
|
102
|
+
# the raw bytes of a huge file are map-reduced in these aux calls and
|
|
103
|
+
# never enter the main agent context (only the final summary returns).
|
|
104
|
+
# `provider: "main"` reuses the primary's provider/model.
|
|
105
|
+
"summarize" => {
|
|
106
|
+
"provider" => "main",
|
|
107
|
+
"model" => "",
|
|
108
|
+
"base_url" => nil,
|
|
109
|
+
"timeout" => 300
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
"agent" => {
|
|
113
|
+
"max_turns" => 90,
|
|
114
|
+
"max_tool_iterations" => 8,
|
|
115
|
+
"max_turn_seconds" => 120,
|
|
116
|
+
# 5 retries with exponential backoff = 1+2+4+8+16 = 31s total wait.
|
|
117
|
+
# Sized to absorb common provider blips (MiniMax intl in particular
|
|
118
|
+
# has been observed returning "API server error - please try again"
|
|
119
|
+
# for ~15-25 seconds before recovering) without timing out the user.
|
|
120
|
+
"api_max_retries" => 5,
|
|
121
|
+
# Hard ceiling (seconds) on a single full-jitter backoff draw between
|
|
122
|
+
# retries: sleep = max(0.2, rand * min(2^(n-1), cap)). Caps worst-case
|
|
123
|
+
# per-retry wait so a flapping backend can't stall a turn for minutes.
|
|
124
|
+
"api_retry_backoff_cap_seconds" => 16,
|
|
125
|
+
# Higher ceiling used ONLY for overload (529/503) and MiniMax "unknown
|
|
126
|
+
# error" blips: those backends stay overloaded for tens of seconds, so
|
|
127
|
+
# the 16s cap retries too eagerly back into a still-hot endpoint. 60s
|
|
128
|
+
# lets the backoff ride out the overload window (the reference uses 120s).
|
|
129
|
+
"api_retry_backoff_overload_cap_seconds" => 60,
|
|
130
|
+
# In-turn retries for a 200-OK-but-EMPTY model response (no text, no
|
|
131
|
+
# tool calls). After this many re-issues of the same turn the Loop
|
|
132
|
+
# raises EmptyModelResponseError → run marked failed (never a silent
|
|
133
|
+
# "completed but empty"). Mirrors the reference treating an empty/invalid
|
|
134
|
+
# response as retryable-then-terminal.
|
|
135
|
+
"empty_response_max_retries" => 2,
|
|
136
|
+
# Provider/model fallback chain (Slice 7 — Agent::FallbackChain). An
|
|
137
|
+
# ORDERED list of backends to rotate to when the primary keeps failing
|
|
138
|
+
# (invalid/empty responses, rate-limit, overload, exhausted retries).
|
|
139
|
+
# The primary is implicit (index 0); these are the fallbacks tried in
|
|
140
|
+
# order. EMPTY by default → no fallback, behaviour byte-identical to a
|
|
141
|
+
# single-provider setup. Each entry:
|
|
142
|
+
# { "provider" => "anthropic", "model" => "claude-...",
|
|
143
|
+
# "base_url" => nil, "api_key" => nil }
|
|
144
|
+
# provider + model are required; base_url/api_key override the
|
|
145
|
+
# providers.<name> config for that entry (custom endpoints). An entry
|
|
146
|
+
# that resolves to the current provider/model/base_url is skipped
|
|
147
|
+
# (dedup) so we never fall back to the backend that just failed.
|
|
148
|
+
"fallback_models" => [],
|
|
149
|
+
"disabled_toolsets" => [],
|
|
150
|
+
"tool_use_enforcement" => "auto"
|
|
151
|
+
},
|
|
152
|
+
"run" => {
|
|
153
|
+
# SSE watchdog: when a run is "running" but no new event has been
|
|
154
|
+
# written for this many seconds, EventsOperation marks it failed and
|
|
155
|
+
# emits a synthetic run.failed frame. Covers cases the executor's
|
|
156
|
+
# rescue can't (model in infinite tool loop, provider stream hung,
|
|
157
|
+
# OS-level thread death). Set to nil to disable.
|
|
158
|
+
"idle_event_timeout" => 300
|
|
159
|
+
},
|
|
160
|
+
"database" => {
|
|
161
|
+
# Sentinel: resolved at read time (Configuration#database_path) to
|
|
162
|
+
# "<resolved home>/rubino.sqlite3" so the DB follows
|
|
163
|
+
# RUBINO_HOME like config/.env/skills do. An explicit override
|
|
164
|
+
# in config.yml replaces this and is used verbatim (issue #96).
|
|
165
|
+
"path" => DEFAULT_DATABASE_PATH
|
|
166
|
+
},
|
|
167
|
+
"paths" => {
|
|
168
|
+
"home" => "~/.rubino",
|
|
169
|
+
"memory" => "~/.rubino/memories",
|
|
170
|
+
"skills" => "~/.rubino/skills",
|
|
171
|
+
"cron" => "~/.rubino/cron",
|
|
172
|
+
"sessions" => "~/.rubino/sessions",
|
|
173
|
+
"logs" => "~/.rubino/logs"
|
|
174
|
+
},
|
|
175
|
+
"ui" => {
|
|
176
|
+
"adapter" => "cli",
|
|
177
|
+
"theme" => "default",
|
|
178
|
+
"verbose" => false
|
|
179
|
+
},
|
|
180
|
+
"display" => {
|
|
181
|
+
"streaming" => true,
|
|
182
|
+
# Tri-state reasoning render (display.reasoning): "hidden" suppresses
|
|
183
|
+
# thinking entirely, "collapsed" buffers it and commits a one-liner cue
|
|
184
|
+
# ("thought for Ns"), "full" renders the whole reasoning as a dim aside
|
|
185
|
+
# above the answer. Deliberately NOT seeded here (#132): defaults
|
|
186
|
+
# injecting it made the documented legacy display.show_reasoning
|
|
187
|
+
# mapping (true→full, false→hidden, applied only when
|
|
188
|
+
# display.reasoning is unset) unreachable for every config loaded
|
|
189
|
+
# normally. Config::ReasoningPrefs supplies the "collapsed" default
|
|
190
|
+
# when neither key is set.
|
|
191
|
+
"language" => "en",
|
|
192
|
+
"runtime_footer" => { "enabled" => false },
|
|
193
|
+
"interim_assistant_messages" => false,
|
|
194
|
+
# The dim status bar pinned UNDER the chat input (model id + context
|
|
195
|
+
# saturation), refreshed at turn boundaries. Omitted automatically
|
|
196
|
+
# off a TTY or on terminals narrower than 40 columns.
|
|
197
|
+
"statusbar" => true,
|
|
198
|
+
# Head lines of each tool's output shown in the transcript before a
|
|
199
|
+
# dim "… +N lines (full output → context)" marker. DISPLAY-ONLY —
|
|
200
|
+
# the model always receives the full (truncation-capped) output.
|
|
201
|
+
# 0 disables the collapse (old full dump).
|
|
202
|
+
"tool_output_preview_lines" => 3,
|
|
203
|
+
# Cap on the chat input's visual rows: a long/multi-line prompt
|
|
204
|
+
# wraps and grows the input downward up to this many rows, then
|
|
205
|
+
# scrolls vertically (caret kept in view).
|
|
206
|
+
"input_max_rows" => 8
|
|
207
|
+
},
|
|
208
|
+
"paste" => {
|
|
209
|
+
# File-backed paste pipeline (UI::PasteStore). A paste with MORE
|
|
210
|
+
# than collapse_lines lines collapses to a "[Pasted text #N +M
|
|
211
|
+
# lines]" placeholder in the chat input, expanded to the full body
|
|
212
|
+
# when the message is sent (the transcript echo keeps the
|
|
213
|
+
# placeholder). A paste estimated above file_threshold_tokens
|
|
214
|
+
# (chars/4) is written to <home>/sessions/<id>/paste_N.txt instead
|
|
215
|
+
# and the sent message carries a read-tool pointer to it.
|
|
216
|
+
"collapse_lines" => 5,
|
|
217
|
+
"file_threshold_tokens" => 8000
|
|
218
|
+
},
|
|
219
|
+
"notifications" => {
|
|
220
|
+
# Attention signals (UI::Notifier) for the moments the agent needs
|
|
221
|
+
# human eyes: a long turn finishing, an approval prompt, a blocked
|
|
222
|
+
# subagent. CLI-only; never emitted into a pipe.
|
|
223
|
+
"enabled" => true,
|
|
224
|
+
# Ring the terminal bell (BEL). On iTerm2 an OSC 9 escape is also
|
|
225
|
+
# sent so it surfaces as a native macOS notification.
|
|
226
|
+
"bell" => true,
|
|
227
|
+
# Optional shell command spawned non-blocking per event with
|
|
228
|
+
# RUBINO_EVENT (turn_finished|needs_approval|blocked) and
|
|
229
|
+
# RUBINO_MESSAGE in its env — e.g. osascript / notify-send.
|
|
230
|
+
"command" => nil,
|
|
231
|
+
# A turn must run at least this many seconds before its completion
|
|
232
|
+
# notifies; quick turns stay silent.
|
|
233
|
+
"min_turn_seconds" => 10
|
|
234
|
+
},
|
|
235
|
+
"thinking" => {
|
|
236
|
+
# Reasoning effort: off | low | medium | high. Mapped to an Anthropic
|
|
237
|
+
# thinking-token budget (off→0, low→4000, medium→8000, high→16000) on
|
|
238
|
+
# the anthropic-family path. "off" disables thinking. When SET it wins
|
|
239
|
+
# over the model/provider thinking_budget chain; left nil (the default)
|
|
240
|
+
# the budget falls through that chain, whose own default is 8000 — i.e.
|
|
241
|
+
# the effective default effort is already "medium". /think reports
|
|
242
|
+
# "medium" for the nil case.
|
|
243
|
+
"effort" => nil
|
|
244
|
+
},
|
|
245
|
+
"streaming" => {
|
|
246
|
+
"enabled" => true,
|
|
247
|
+
"transport" => "off",
|
|
248
|
+
"edit_interval" => 0.3,
|
|
249
|
+
"buffer_threshold" => 40,
|
|
250
|
+
"cursor" => " \u2589"
|
|
251
|
+
},
|
|
252
|
+
"context" => {
|
|
253
|
+
"engine" => "compressor",
|
|
254
|
+
"max_tokens" => nil
|
|
255
|
+
},
|
|
256
|
+
"compression" => {
|
|
257
|
+
"enabled" => true,
|
|
258
|
+
"threshold" => 0.50,
|
|
259
|
+
"gateway_threshold" => 0.85,
|
|
260
|
+
"target_ratio" => 0.20,
|
|
261
|
+
"protect_first_n" => 3,
|
|
262
|
+
"protect_last_n" => 20,
|
|
263
|
+
"max_summary_tokens" => 12_000,
|
|
264
|
+
"preserve_tool_pairs" => true
|
|
265
|
+
},
|
|
266
|
+
"memory" => {
|
|
267
|
+
"enabled" => true,
|
|
268
|
+
"backend" => "sqlite",
|
|
269
|
+
"auto_extract" => true,
|
|
270
|
+
"auto_save" => true,
|
|
271
|
+
"user_profile_enabled" => true,
|
|
272
|
+
"project_context_enabled" => true,
|
|
273
|
+
"memory_char_limit" => 2200,
|
|
274
|
+
"user_char_limit" => 1375,
|
|
275
|
+
# Ingest/store cap for the live memory set, kept SEPARATE from the
|
|
276
|
+
# injection budget above. `memory_char_limit` only bounds what gets
|
|
277
|
+
# packed into the prompt at RETRIEVAL time; storing facts must not be
|
|
278
|
+
# throttled by it or long multi-session conversations stall once the
|
|
279
|
+
# injection budget fills. `nil` = unbounded ingest (the default).
|
|
280
|
+
"ingest_char_limit" => nil,
|
|
281
|
+
# tiny-Zep SQLite backend tuning. `vector` enables best-effort
|
|
282
|
+
# sqlite-vec/RubyLLM.embed KNN on top of the always-on FTS5 hybrid;
|
|
283
|
+
# off by default so the stock install needs no extra deps. `graph`
|
|
284
|
+
# is the graph-lite 1-hop entity/edge blend (on by default).
|
|
285
|
+
"sqlite" => {
|
|
286
|
+
"vector" => false,
|
|
287
|
+
"graph" => true
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
"jobs" => {
|
|
291
|
+
"mode" => "inline",
|
|
292
|
+
"poll_interval" => 2,
|
|
293
|
+
"max_attempts" => 3,
|
|
294
|
+
"retry_backoff_seconds" => 30
|
|
295
|
+
},
|
|
296
|
+
# Nested-subagent (the `task` delegation tool) caps. A subagent CAN now
|
|
297
|
+
# spawn its own subagents; these three caps bound the tree so depth ×
|
|
298
|
+
# fan-out cannot blow past the process's thread/cost budget. All three are
|
|
299
|
+
# enforced in ONE place — Tools::BackgroundTasks#reserve — which refuses a
|
|
300
|
+
# spawn (the tool then surfaces a clear at-capacity / max-depth message).
|
|
301
|
+
"tasks" => {
|
|
302
|
+
# Max nesting depth. depth 0 = a human/top-level-spawned child; the cap
|
|
303
|
+
# bounds chains of subagents-spawning-subagents. 2 ⇒ human→child→grandchild
|
|
304
|
+
# (no deeper).
|
|
305
|
+
"max_depth" => 2,
|
|
306
|
+
# Max LIVE direct children one node (human/top-level or a single
|
|
307
|
+
# subagent) may have at once.
|
|
308
|
+
"max_children_per_node" => 3,
|
|
309
|
+
# Hard global ceiling on total LIVE subagents across the whole tree.
|
|
310
|
+
"max_concurrent_total" => 8,
|
|
311
|
+
# Per-child budget for BILLED live probes (`probe(live:true)`): how many
|
|
312
|
+
# times an owner may run a one-shot model peek over a single child's
|
|
313
|
+
# transcript. Over budget → the model is told to use the FREE
|
|
314
|
+
# live:false snapshot instead. Free snapshots are unlimited.
|
|
315
|
+
"max_live_probes_per_child" => 5,
|
|
316
|
+
# Bound (seconds) a BLOCKING ask_parent waits before the child
|
|
317
|
+
# self-heals and proceeds with its best judgement (S5a). Matches the
|
|
318
|
+
# approvals wait-timeout default — never "forever".
|
|
319
|
+
"ask_parent_timeout" => 900
|
|
320
|
+
},
|
|
321
|
+
"tools" => {
|
|
322
|
+
# Sandbox write/edit/delete tools to workspace_root (terminal.cwd
|
|
323
|
+
# or Dir.pwd). Set to false to let the model touch any path the
|
|
324
|
+
# process can reach — only do this if you trust the model + the
|
|
325
|
+
# approval flow alone.
|
|
326
|
+
"workspace_strict" => true,
|
|
327
|
+
"git" => true,
|
|
328
|
+
# Default ON: the agent ships to run inside an isolated per-customer
|
|
329
|
+
# VM where running shell commands is the whole point. The blast radius
|
|
330
|
+
# is the VM, and security.require_confirmation_for_shell (default true)
|
|
331
|
+
# still gates every command behind an approval prompt.
|
|
332
|
+
"shell" => true,
|
|
333
|
+
"ruby" => true,
|
|
334
|
+
|
|
335
|
+
"web" => false,
|
|
336
|
+
"memory" => true
|
|
337
|
+
},
|
|
338
|
+
"tool_output" => {
|
|
339
|
+
"max_bytes" => 50_000,
|
|
340
|
+
"max_lines" => 2000,
|
|
341
|
+
"max_line_length" => 2000
|
|
342
|
+
},
|
|
343
|
+
"file_read" => {
|
|
344
|
+
"max_chars" => 100_000
|
|
345
|
+
},
|
|
346
|
+
"terminal" => {
|
|
347
|
+
"backend" => "local",
|
|
348
|
+
"cwd" => nil,
|
|
349
|
+
"file_sync_enabled" => false,
|
|
350
|
+
"file_sync_max_mb" => 100
|
|
351
|
+
},
|
|
352
|
+
"approvals" => {
|
|
353
|
+
"mode" => "manual",
|
|
354
|
+
# Auto-allow provably READ-ONLY shell commands (ls, pwd, cat, grep,
|
|
355
|
+
# git log, ...) without an approval prompt. The whole line must
|
|
356
|
+
# parse as safe (Security::ReadonlyCommands): no redirection or
|
|
357
|
+
# command/process substitution, every pipe/&&/; segment from the
|
|
358
|
+
# read-only set, no mutating flags (find -exec/-delete, ...).
|
|
359
|
+
# Anything ambiguous still prompts. The hardline floor and
|
|
360
|
+
# permissions:deny always run first, so this never weakens them.
|
|
361
|
+
"auto_allow_readonly" => true,
|
|
362
|
+
# Extra command names (or leading-token prefixes, e.g. "docker ps")
|
|
363
|
+
# merged into the built-in read-only set. The same parse validation
|
|
364
|
+
# applies to every segment.
|
|
365
|
+
"readonly_commands" => [],
|
|
366
|
+
# How long (seconds) a run waits on a human approval/clarification
|
|
367
|
+
# before giving up. On expiry the gate AUTO-DENIES (never approves)
|
|
368
|
+
# and frees the worker thread — an abandoned approval (closed tab, no
|
|
369
|
+
# answer) must not park a server worker indefinitely (W1). A sane
|
|
370
|
+
# bound (15 min), not the old 24h that effectively never released.
|
|
371
|
+
# Set to nil for a truly unbounded wait (interruptible only by an
|
|
372
|
+
# explicit run stop; discouraged on shared servers). While a decision
|
|
373
|
+
# is pending the SSE idle watchdog is suspended for that run
|
|
374
|
+
# (EventsOperation), so the run is never reaped mid-wait.
|
|
375
|
+
"wait_timeout_seconds" => 900
|
|
376
|
+
},
|
|
377
|
+
|
|
378
|
+
# SSRF guard for Run::AttachmentDownloader. Only URLs whose host is in
|
|
379
|
+
# this list (case-insensitive) are fetched into the run workspace; the
|
|
380
|
+
# downloader refuses everything else. ENV["ALLOWED_FILE_URL_HOSTS"]
|
|
381
|
+
# (comma-separated) is merged in too, so a downstream consumer can keep
|
|
382
|
+
# using its existing env knob. Loopback hosts (localhost, 127.0.0.1, ::1) are
|
|
383
|
+
# ALWAYS allowed on top of this list, since an HTTP client co-located on the
|
|
384
|
+
# same host produces loopback attachment URLs.
|
|
385
|
+
# Empty list + empty env = only loopback is fetchable.
|
|
386
|
+
"attachments" => {
|
|
387
|
+
"allowed_hosts" => [],
|
|
388
|
+
# Secure-by-default policy for the universal file-attachment handler
|
|
389
|
+
# (Attachments::Classify / Preamble). Every default is on the secure
|
|
390
|
+
# branch; explicit user config wins (Configuration merges over these).
|
|
391
|
+
# Fail closed: oversize / unsafe / disallowed-kind => warn + skip.
|
|
392
|
+
"policy" => {
|
|
393
|
+
# Hard cap on accepted file size, enforced via lstat BEFORE reading.
|
|
394
|
+
"max_file_bytes" => 26_214_400, # 25 MB
|
|
395
|
+
# Inline budget for text files; over budget => head + read-rest note.
|
|
396
|
+
"inline_text_budget_bytes" => 100_000, # ~25k tokens
|
|
397
|
+
# Kinds the handler will process. Deny one by removing it.
|
|
398
|
+
"allow_kinds" => %w[image text document archive binary],
|
|
399
|
+
# Documents are hint-only by default (cost / injection blast radius);
|
|
400
|
+
# the flag is reserved for a future in-process extract path.
|
|
401
|
+
"auto_extract_documents" => false,
|
|
402
|
+
# Routing an image to an EXTERNAL aux model is data egress; on by
|
|
403
|
+
# default to preserve the existing aux-vision behaviour.
|
|
404
|
+
"aux_vision_egress" => true,
|
|
405
|
+
# Caps for any in-process archive listing (hint-only today, so
|
|
406
|
+
# unused unless listing is enabled).
|
|
407
|
+
"archive" => {
|
|
408
|
+
"max_entries" => 2000,
|
|
409
|
+
"max_uncompressed_bytes" => 268_435_456,
|
|
410
|
+
"max_entry_ratio" => 100,
|
|
411
|
+
"max_total_ratio" => 50,
|
|
412
|
+
"max_nesting_depth" => 1
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
},
|
|
416
|
+
"security" => {
|
|
417
|
+
# Prompt policy for shell commands not otherwise allowed/denied:
|
|
418
|
+
# confirm_all (DEFAULT) every such command prompts for approval.
|
|
419
|
+
# dangerous_only (reference-faithful) safe commands run unprompted;
|
|
420
|
+
# only DangerousPatterns matches prompt.
|
|
421
|
+
# Intentionally NOT defaulted here: when the key is absent the
|
|
422
|
+
# accessor derives it from require_confirmation_for_shell below
|
|
423
|
+
# (true -> confirm_all, false -> dangerous_only). Setting the key
|
|
424
|
+
# explicitly makes confirm_policy win over the legacy alias. The
|
|
425
|
+
# hardline floor and permissions:deny always precede this regardless
|
|
426
|
+
# of policy, so dangerous_only never weakens the non-bypassable floor.
|
|
427
|
+
#
|
|
428
|
+
# "confirm_policy" => "confirm_all",
|
|
429
|
+
#
|
|
430
|
+
# Legacy alias for confirm_policy (see above). Kept working for any
|
|
431
|
+
# existing readers. When true, every `shell` command goes through the
|
|
432
|
+
# approval prompt regardless of the tool's own risk level. Default ON.
|
|
433
|
+
"require_confirmation_for_shell" => true,
|
|
434
|
+
"command_allowlist" => [
|
|
435
|
+
"git status",
|
|
436
|
+
"git diff",
|
|
437
|
+
"bundle exec rspec"
|
|
438
|
+
],
|
|
439
|
+
|
|
440
|
+
"website_blocklist" => {
|
|
441
|
+
"enabled" => false,
|
|
442
|
+
"domains" => [],
|
|
443
|
+
"shared_files" => []
|
|
444
|
+
}
|
|
445
|
+
},
|
|
446
|
+
"privacy" => {
|
|
447
|
+
"redact_pii" => false
|
|
448
|
+
},
|
|
449
|
+
"clarify" => {
|
|
450
|
+
"timeout" => 120
|
|
451
|
+
},
|
|
452
|
+
"worktree" => {
|
|
453
|
+
"enabled" => false
|
|
454
|
+
},
|
|
455
|
+
# System-prompt layering. Defaults ship the built-in role prompts
|
|
456
|
+
# from lib/rubino/agent/prompts/*.txt. Customers customise via
|
|
457
|
+
# config.yml:
|
|
458
|
+
# prompts.preamble — single block prepended after the role
|
|
459
|
+
# identity; the natural place for "You are running inside
|
|
460
|
+
# <product>" customer context.
|
|
461
|
+
# prompts.environment.enabled — when true (default) the assembler
|
|
462
|
+
# injects an [Environment] block with date/OS/cwd/git/runtimes
|
|
463
|
+
# and the list of CLI utilities found on PATH. Cached per
|
|
464
|
+
# process — re-probed every boot, not every turn.
|
|
465
|
+
# prompts.environment.extra_utilities — additional binaries to
|
|
466
|
+
# probe beyond EnvironmentInspector::DEFAULT_UTILITIES.
|
|
467
|
+
# prompts.overrides.<role> — full replacement of the built-in
|
|
468
|
+
# role prompt (escape hatch; prefer preamble for incremental
|
|
469
|
+
# tweaks).
|
|
470
|
+
"prompts" => {
|
|
471
|
+
"preamble" => nil,
|
|
472
|
+
"environment" => {
|
|
473
|
+
"enabled" => true,
|
|
474
|
+
"extra_utilities" => []
|
|
475
|
+
},
|
|
476
|
+
"overrides" => {}
|
|
477
|
+
},
|
|
478
|
+
"quick_commands" => {},
|
|
479
|
+
"mcp" => {
|
|
480
|
+
"servers" => {}
|
|
481
|
+
},
|
|
482
|
+
"skills" => {
|
|
483
|
+
"enabled" => true,
|
|
484
|
+
# Post-turn skill distillation (Variant B). When true, a successful,
|
|
485
|
+
# tool-heavy turn enqueues DistillSkillJob, which spends ONE auxiliary
|
|
486
|
+
# model call to distil a reusable SKILL.md. Mirrors memory.auto_extract:
|
|
487
|
+
# a separate toggle from `enabled` (which only controls whether skills
|
|
488
|
+
# are loaded/usable) so a deployment — or a test that scripts a fixed
|
|
489
|
+
# number of LLM turns — can keep skills usable while turning off the
|
|
490
|
+
# extra background aux call.
|
|
491
|
+
"auto_distill" => true,
|
|
492
|
+
# Discover the skills shipped *inside the gem* (skills/<name>/SKILL.md),
|
|
493
|
+
# so every install gets the built-in catalogue (e.g. ruby-expert) with
|
|
494
|
+
# no copy step, on top of the user paths below. Built-ins are scanned
|
|
495
|
+
# first, so a same-named user skill still overrides them. Set false to
|
|
496
|
+
# run with only your own skills.
|
|
497
|
+
"include_builtin" => true,
|
|
498
|
+
"paths" => [
|
|
499
|
+
".rubino/skills",
|
|
500
|
+
"~/.rubino/skills"
|
|
501
|
+
]
|
|
502
|
+
},
|
|
503
|
+
"commands" => {
|
|
504
|
+
"paths" => [
|
|
505
|
+
".rubino/commands",
|
|
506
|
+
HOME_COMMANDS_PATH
|
|
507
|
+
],
|
|
508
|
+
# When false (default), !`shell` interpolation in command templates is
|
|
509
|
+
# disabled. Set to true only in trusted environments where you explicitly
|
|
510
|
+
# want command templates to execute shell commands.
|
|
511
|
+
"shell_injection_enabled" => false
|
|
512
|
+
},
|
|
513
|
+
"permissions" => {},
|
|
514
|
+
"formatters" => {},
|
|
515
|
+
"agents" => {},
|
|
516
|
+
"server" => {
|
|
517
|
+
"port" => 4820,
|
|
518
|
+
"auth" => false
|
|
519
|
+
},
|
|
520
|
+
"api" => {
|
|
521
|
+
# Hard cap on JSON request bodies. Anything past this (whether
|
|
522
|
+
# advertised by Content-Length or revealed mid-read) is rejected
|
|
523
|
+
# with 413 before the parser allocates the full payload — keeps a
|
|
524
|
+
# multi-GB POST from OOM-killing the process.
|
|
525
|
+
"max_body_bytes" => 5 * 1024 * 1024,
|
|
526
|
+
# Hard cap on multipart upload payload (POST /v1/files). Checked
|
|
527
|
+
# against Content-Length first, then enforced mid-stream so a
|
|
528
|
+
# truncated/missing Content-Length cannot saturate the disk.
|
|
529
|
+
"max_upload_bytes" => 50 * 1024 * 1024,
|
|
530
|
+
# Token-bucket rate limiter. Unauth bucket (per remote IP) protects
|
|
531
|
+
# /v1/health and /v1/metrics from public floods; auth bucket (per
|
|
532
|
+
# bearer token) caps authenticated callers. Storage is in-memory,
|
|
533
|
+
# so multi-process deployments need a shared backend before this
|
|
534
|
+
# gives meaningful protection across workers.
|
|
535
|
+
"rate_limit_enabled" => true,
|
|
536
|
+
"rate_limit_unauth_per_minute" => 60,
|
|
537
|
+
"rate_limit_auth_per_minute" => 600
|
|
538
|
+
}
|
|
539
|
+
}.freeze
|
|
540
|
+
|
|
541
|
+
class << self
|
|
542
|
+
# Deep copy so a Configuration#set on a never-overridden nested section
|
|
543
|
+
# (e.g. display.reasoning) mutates the per-config hash, NOT the shared
|
|
544
|
+
# MODULE_DEFAULTS constant. A shallow .dup left nested section hashes
|
|
545
|
+
# aliased to the constant, so the first /reasoning or /think write
|
|
546
|
+
# poisoned the process-wide default.
|
|
547
|
+
def to_hash
|
|
548
|
+
deep_dup(MODULE_DEFAULTS)
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
def deep_dup(obj)
|
|
552
|
+
case obj
|
|
553
|
+
when Hash then obj.each_with_object({}) { |(k, v), h| h[k] = deep_dup(v) }
|
|
554
|
+
when Array then obj.map { |v| deep_dup(v) }
|
|
555
|
+
else obj
|
|
556
|
+
end
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
def to_yaml
|
|
560
|
+
MODULE_DEFAULTS.to_yaml
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
def dig(*keys)
|
|
564
|
+
MODULE_DEFAULTS.dig(*keys)
|
|
565
|
+
end
|
|
566
|
+
end
|
|
567
|
+
end
|
|
568
|
+
end
|
|
569
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module Rubino
|
|
7
|
+
module Config
|
|
8
|
+
class ConfigError < StandardError; end
|
|
9
|
+
|
|
10
|
+
# Responsible for loading configuration from YAML files and environment.
|
|
11
|
+
# Searches in order: project-local, user home, defaults.
|
|
12
|
+
class Loader
|
|
13
|
+
CONFIG_FILENAME = "config.yml"
|
|
14
|
+
ENV_FILENAME = ".env"
|
|
15
|
+
ENV_VAR_PATTERN = /\$\{([A-Z_][A-Z0-9_]*)\}/
|
|
16
|
+
|
|
17
|
+
attr_reader :home_path, :config_path, :env_path
|
|
18
|
+
|
|
19
|
+
# Single source of truth for the home directory: RUBINO_HOME when
|
|
20
|
+
# set, else ~/.rubino. Rubino.home_path delegates here so the
|
|
21
|
+
# server (which loads config via the Loader) and the CLI commands
|
|
22
|
+
# (config/setup/doctor) resolve the SAME directory — previously the
|
|
23
|
+
# server honoured $RUBINO_HOME while the CLI recomputed
|
|
24
|
+
# File.join(Rubino.home_path, "config.yml") off the YAML
|
|
25
|
+
# `paths.home` default (~/.rubino), a split brain at first boot.
|
|
26
|
+
def self.default_home_path
|
|
27
|
+
env = ENV["RUBINO_HOME"].to_s.strip
|
|
28
|
+
env.empty? ? File.expand_path("~/.rubino") : File.expand_path(env)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def initialize(home_path: nil)
|
|
32
|
+
@home_path = home_path || self.class.default_home_path
|
|
33
|
+
@config_path = File.join(@home_path, CONFIG_FILENAME)
|
|
34
|
+
@env_path = File.join(@home_path, ENV_FILENAME)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Loads configuration from file, merging with defaults
|
|
38
|
+
def load
|
|
39
|
+
raw =
|
|
40
|
+
if File.exist?(@config_path)
|
|
41
|
+
begin
|
|
42
|
+
YAML.safe_load_file(@config_path, permitted_classes: [Symbol]) || {}
|
|
43
|
+
rescue Psych::SyntaxError => e
|
|
44
|
+
raise ConfigError,
|
|
45
|
+
"Invalid YAML in #{@config_path} at line #{e.line}, column #{e.column}: #{e.problem}"
|
|
46
|
+
end
|
|
47
|
+
else
|
|
48
|
+
{}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
load_env_file if File.exist?(@env_path)
|
|
52
|
+
|
|
53
|
+
deep_merge(Defaults.to_hash, expand_env_vars(raw))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Returns true if a config file exists
|
|
57
|
+
def config_exists?
|
|
58
|
+
File.exist?(@config_path)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Creates the initial config file with defaults
|
|
62
|
+
def create_default_config!
|
|
63
|
+
FileUtils.mkdir_p(@home_path)
|
|
64
|
+
File.write(@config_path, Defaults.to_yaml)
|
|
65
|
+
@config_path
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def load_env_file
|
|
71
|
+
File.readlines(@env_path).each do |line|
|
|
72
|
+
line = line.strip
|
|
73
|
+
next if line.empty? || line.start_with?("#")
|
|
74
|
+
|
|
75
|
+
key, value = line.split("=", 2)
|
|
76
|
+
next unless key && value
|
|
77
|
+
|
|
78
|
+
ENV[key.strip] = strip_env_quotes(value.strip)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Strips matched surrounding single or double quotes (a common .env
|
|
83
|
+
# convention: FOO="bar baz" → bar baz). Unbalanced quotes are preserved
|
|
84
|
+
# verbatim so they aren't silently mangled.
|
|
85
|
+
def strip_env_quotes(value)
|
|
86
|
+
return value if value.length < 2
|
|
87
|
+
|
|
88
|
+
first = value[0]
|
|
89
|
+
last = value[-1]
|
|
90
|
+
return value[1..-2] if ['"', "'"].include?(first) && first == last
|
|
91
|
+
|
|
92
|
+
value
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def expand_env_vars(node)
|
|
96
|
+
case node
|
|
97
|
+
when Hash then node.transform_values { |v| expand_env_vars(v) }
|
|
98
|
+
when Array then node.map { |v| expand_env_vars(v) }
|
|
99
|
+
when String then node.gsub(ENV_VAR_PATTERN) { ENV[Regexp.last_match(1)] || "" }
|
|
100
|
+
else node
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def deep_merge(base, override)
|
|
105
|
+
base.merge(override) do |_key, old_val, new_val|
|
|
106
|
+
if old_val.is_a?(Hash) && new_val.is_a?(Hash)
|
|
107
|
+
deep_merge(old_val, new_val)
|
|
108
|
+
else
|
|
109
|
+
new_val
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|