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
data/docs/memory.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Memory
|
|
2
|
+
|
|
3
|
+
rubino remembers facts about you and the project across sessions. The default backend is a small SQLite "tiny-Zep" store — Zep/Graphiti-inspired, minus the graph database, the server, and the multi-call pipeline.
|
|
4
|
+
|
|
5
|
+
## Backends
|
|
6
|
+
|
|
7
|
+
Memory backends are pluggable (registered like tools). Two ship:
|
|
8
|
+
|
|
9
|
+
| `memory.backend` | What it is |
|
|
10
|
+
|---|---|
|
|
11
|
+
| `sqlite` (**default**) | tiny-Zep: LLM-extracted atomic facts, bi-temporal supersession, hybrid FTS5/BM25 (+ optional vector) ranked recall, graph-lite 1-hop blend |
|
|
12
|
+
| `default` | the legacy non-ranked store (kept for back-compat) |
|
|
13
|
+
|
|
14
|
+
Switch backends:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
rubino memory backend # show the active backend + available names
|
|
18
|
+
rubino memory backend sqlite # switch (writes memory.backend to config.yml)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The agent loop, the in-chat `/memory` view, the `/status` panel, the `rubino memory` CLI, and the HTTP `/v1/memory` operations all use the **active** backend (fixed in #94/#106/#83 — these surfaces previously read a hardwired legacy table and never saw the facts the agent actually persists).
|
|
22
|
+
|
|
23
|
+
## The sqlite tiny-Zep backend
|
|
24
|
+
|
|
25
|
+
### What's stored
|
|
26
|
+
|
|
27
|
+
One declarative **fact** per row. Facts carry a `kind` (`user_profile`, `preference`, `fact`, `project`, `env`, …), the source session, a confidence, optional entity tags, and bi-temporal validity columns (`valid_from` / `valid_to`).
|
|
28
|
+
|
|
29
|
+
- **User profile** (`user_profile` facts) — durable facts about you, metered against `memory.user_char_limit`.
|
|
30
|
+
- **Project context** (`project` / `env` facts) — facts about the codebase/environment.
|
|
31
|
+
- **General facts** — everything else.
|
|
32
|
+
|
|
33
|
+
### How facts are extracted (write path)
|
|
34
|
+
|
|
35
|
+
When `memory.auto_extract` is on, auto-extraction runs as a post-turn job (`ExtractMemoryJob` — executed immediately after the turn in the default inline jobs mode): a single auxiliary-LLM call looks at the recent turn and returns `{add, supersede}`:
|
|
36
|
+
|
|
37
|
+
- **add** — new atomic facts (deduplicated via a Jaccard near-dup check against the live set, no second LLM call).
|
|
38
|
+
- **supersede** — a contradicted fact is **soft-retired** (its `valid_to` is set and `superseded_by` points at the replacement), not deleted — temporal correctness without losing provenance (Graphiti-style edge invalidation collapsed to one call).
|
|
39
|
+
|
|
40
|
+
When extraction stores facts, the chat prints a deterministic confirmation from the write path (`✓ saved to memory · 2 facts (e6bf776b, a91c03d2)`) — the agent's "I'll remember that" narration alone is not a save signal.
|
|
41
|
+
|
|
42
|
+
Every write goes through the same injection-defense floor as the legacy store: a `ThreatScanner` (prompt-injection / exfiltration patterns) plus a character budget. A fact that trips a guard is skipped, not allowed to splice tainted/over-budget content into a future system prompt.
|
|
43
|
+
|
|
44
|
+
Two budgets, deliberately separate:
|
|
45
|
+
|
|
46
|
+
- `memory.memory_char_limit` (2200) / `memory.user_char_limit` (1375) — the **injection** budget: how much is packed into the prompt at retrieval time.
|
|
47
|
+
- `memory.ingest_char_limit` (null = unbounded) — the **store** budget: storing facts isn't throttled by the injection budget, so long multi-session conversations don't stall once the injection budget fills.
|
|
48
|
+
|
|
49
|
+
### How facts are recalled (read path)
|
|
50
|
+
|
|
51
|
+
`retrieve` runs a **hybrid ranked** recall over LIVE facts (`valid_to IS NULL`):
|
|
52
|
+
|
|
53
|
+
1. **Direct relevance** — FTS5/BM25 over the query (and vector KNN when enabled), fused with Reciprocal Rank Fusion and lightly kind-weighted (durable `user_profile`/`preference`/`env` facts win ties). These are the only content-matching signals, so the fact a keyword probe ranks #1 stays #1.
|
|
54
|
+
2. **Tail supplements** — graph (1-hop entity neighbours of the query) then recency only **backfill** the remaining budget after direct hits. They can never outrank a direct content match (this was the dominant cause of single-shot recall misses).
|
|
55
|
+
|
|
56
|
+
Results are greedily packed under the retrieval char budget. Common stopwords ("user", "project", "the", …) are excluded from the FTS MATCH so a probe doesn't match every fact on a trivial word.
|
|
57
|
+
|
|
58
|
+
### Tuning
|
|
59
|
+
|
|
60
|
+
```yaml
|
|
61
|
+
memory:
|
|
62
|
+
sqlite:
|
|
63
|
+
vector: false # opt-in sqlite-vec / RubyLLM.embed KNN on top of FTS5 (off by default — no extra deps needed)
|
|
64
|
+
graph: true # graph-lite 1-hop entity/edge blend (on by default; set false to A/B the graph signal)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Vector mode requires both `vector: true` **and** `RubyLLM.embed` to be wired; otherwise it's FTS5-only.
|
|
68
|
+
|
|
69
|
+
## The `memory` tool
|
|
70
|
+
|
|
71
|
+
The agent persists facts autonomously via the `memory` tool (gated by `tools.memory`, on by default):
|
|
72
|
+
|
|
73
|
+
- `action: add` — record a new fact.
|
|
74
|
+
- `action: replace` — supersede an existing fact (`old_text` selects it).
|
|
75
|
+
- `action: remove` — hard-delete a fact.
|
|
76
|
+
- `target: user` writes the user profile; `target: memory` writes general memory.
|
|
77
|
+
|
|
78
|
+
The tool stores **one atomic fact per call** — separate facts go in separate calls so each can be superseded or forgotten independently. Every write is confirmed deterministically in chat by the tool-result line, e.g.:
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
└ ✓ Memory replaced (id=e6bf776b, kind=user_profile).
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Content is scanned for injection/exfiltration patterns and subject to the character budget. Because this lets the agent write to its own future context, see [security.md](security.md#autonomous-memory) for the trust model.
|
|
85
|
+
|
|
86
|
+
## Inspecting and managing memory
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
rubino memory list # most recent LIVE facts (active backend)
|
|
90
|
+
rubino memory list --all # include superseded (soft-retired) facts
|
|
91
|
+
rubino memory list --kind user_profile --limit 50
|
|
92
|
+
rubino memory show <id> # full fact incl. the temporal chain (id prefix accepted)
|
|
93
|
+
rubino memory delete <id> # hard-delete
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`list` and the in-chat `/memory` views show only **live** facts (`valid_to IS NULL`) — superseded facts are retained for provenance but hidden, so a contradicted fact is never presented as current and the header count always matches the rows. Pass `--all` to `rubino memory list` to see the supersession history; `rubino memory show <id>` prints a retired fact's `Retired:` / `Superseded by:` chain.
|
|
97
|
+
|
|
98
|
+
In-chat, `/memory` inspects, searches (`/memory <query>` or `/memory search <query>`), and forgets what the agent remembers. Both surfaces read the active backend.
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# Models & keys
|
|
2
|
+
|
|
3
|
+
Which provider, which model, which key — answered in 60 seconds. The fastest path is `rubino setup`, which writes all of the blocks below for you. This page is the manual reference and the per-provider copy-paste.
|
|
4
|
+
|
|
5
|
+
## The decision
|
|
6
|
+
|
|
7
|
+
| Provider | When | Default model the wizard writes |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| **OpenAI** | Recommended default; GPT models | `gpt-4.1` |
|
|
10
|
+
| **MiniMax** | Anthropic-compatible | `MiniMax-M2.7` |
|
|
11
|
+
| **Anthropic** | Claude models | `claude-sonnet-4-5` |
|
|
12
|
+
| **Google (Gemini)** | Gemini models | `gemini-2.5-pro` |
|
|
13
|
+
| **OpenAI-compatible gateway** | A gateway picks the upstream | `auto` |
|
|
14
|
+
| **fake** | Tests/demos only | `fake/happy-path` (needs `RUBINO_ALLOW_FAKE=1`) |
|
|
15
|
+
|
|
16
|
+
How resolution works: an explicit `model.provider` (anything other than `auto`) wins. When `provider: auto`, the provider is derived from the `model.default` id by ruby_llm's registry. A key for the resolved provider must be available either via `providers.<name>.api_key` in `config.yml` **or** the provider's native ENV var.
|
|
17
|
+
|
|
18
|
+
## ⚠️ The default→OpenRouter trap (refs #93)
|
|
19
|
+
|
|
20
|
+
The shipped default is:
|
|
21
|
+
|
|
22
|
+
```yaml
|
|
23
|
+
model:
|
|
24
|
+
default: "openai/gpt-4.1"
|
|
25
|
+
provider: "auto"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
In ruby_llm's model registry, the id `openai/gpt-4.1` resolves to **OpenRouter**, not OpenAI's own API. Historically, a brand-new user with no key hit ~80 seconds of silent retries against an endpoint they never chose, then got an empty answer and a success exit — a dead end with no signal.
|
|
29
|
+
|
|
30
|
+
**This is now fixed.** Before any model call, rubino checks that the resolved provider has a usable credential (`LLM::CredentialCheck`). If not:
|
|
31
|
+
|
|
32
|
+
- On a TTY → the onboarding wizard runs so you pick a provider + paste a key.
|
|
33
|
+
- Non-interactively (`-q` / piped / no TTY) → it prints a clear, actionable message and exits non-zero (no silent retry storm):
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
No API key configured for provider 'openai' (model openai/gpt-4.1).
|
|
37
|
+
Set it up one of these ways:
|
|
38
|
+
• run `rubino setup` for a guided first-run setup, or
|
|
39
|
+
• add OPENAI_API_KEY=<your-key> to ~/.rubino/.env, or
|
|
40
|
+
• set providers.openai.api_key in ~/.rubino/config.yml.
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The simplest fix is `rubino setup`. To deliberately use OpenAI's own API (not OpenRouter), set a bare `gpt-4.1` with `provider: openai` as shown below.
|
|
44
|
+
|
|
45
|
+
## Where keys live
|
|
46
|
+
|
|
47
|
+
- Keys go in `~/.rubino/.env` (mode `0600`) as `KEY=value`, or directly as `providers.<name>.api_key` in `config.yml`.
|
|
48
|
+
- In `config.yml` you can reference an env var with the substitution syntax: `api_key: "${MINIMAX_API_KEY}"` or `"{env:MINIMAX_API_KEY}"`.
|
|
49
|
+
- `RUBINO_HOME` relocates the whole home (config, `.env`, and the database follow it).
|
|
50
|
+
|
|
51
|
+
The native ENV var per provider: `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY` (or `GOOGLE_API_KEY`), `BEDROCK_API_KEY`, `MINIMAX_API_KEY`.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Per-provider setup
|
|
56
|
+
|
|
57
|
+
Each block goes in `~/.rubino/config.yml`; the key goes in `~/.rubino/.env`.
|
|
58
|
+
|
|
59
|
+
### MiniMax (Anthropic-compatible)
|
|
60
|
+
|
|
61
|
+
MiniMax speaks the Anthropic API, so it routes through the anthropic-compatible path.
|
|
62
|
+
|
|
63
|
+
```yaml
|
|
64
|
+
model:
|
|
65
|
+
default: "MiniMax-M2.7"
|
|
66
|
+
provider: "minimax"
|
|
67
|
+
|
|
68
|
+
providers:
|
|
69
|
+
minimax:
|
|
70
|
+
anthropic_compatible: true
|
|
71
|
+
base_url: "https://api.minimax.io/anthropic"
|
|
72
|
+
api_key: "${MINIMAX_API_KEY}"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# ~/.rubino/.env
|
|
77
|
+
MINIMAX_API_KEY=...
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
> MiniMax M2 ignores tool definitions and roleplays bash in markdown; use **MiniMax-M2.7** for working tool use.
|
|
81
|
+
|
|
82
|
+
### OpenAI (GPT) (recommended default)
|
|
83
|
+
|
|
84
|
+
Uses OpenAI's own API (not OpenRouter) when `provider: openai` and the model id is a bare OpenAI id.
|
|
85
|
+
|
|
86
|
+
```yaml
|
|
87
|
+
model:
|
|
88
|
+
default: "gpt-4.1"
|
|
89
|
+
provider: "openai"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# ~/.rubino/.env
|
|
94
|
+
OPENAI_API_KEY=sk-...
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
For Azure/custom endpoints set `providers.openai.base_url`.
|
|
98
|
+
|
|
99
|
+
### Anthropic (Claude)
|
|
100
|
+
|
|
101
|
+
```yaml
|
|
102
|
+
model:
|
|
103
|
+
default: "claude-sonnet-4-5"
|
|
104
|
+
provider: "anthropic"
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
# ~/.rubino/.env
|
|
109
|
+
ANTHROPIC_API_KEY=sk-ant-...
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Google (Gemini)
|
|
113
|
+
|
|
114
|
+
The provider id is `google`.
|
|
115
|
+
|
|
116
|
+
```yaml
|
|
117
|
+
model:
|
|
118
|
+
default: "gemini-2.5-pro"
|
|
119
|
+
provider: "google"
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
# ~/.rubino/.env
|
|
124
|
+
GEMINI_API_KEY=...
|
|
125
|
+
# GOOGLE_API_KEY is also accepted
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### OpenAI-compatible gateway
|
|
129
|
+
|
|
130
|
+
Point this at any OpenAI-compatible gateway; the gateway decides which upstream (OpenAI/MiniMax/Anthropic/…) and which model to call. Route everything to it with `provider: gateway` and `model: auto`, and set `base_url` + `api_key` for your gateway.
|
|
131
|
+
|
|
132
|
+
```yaml
|
|
133
|
+
model:
|
|
134
|
+
default: "auto"
|
|
135
|
+
provider: "gateway"
|
|
136
|
+
supports_vision: null # set true/false if the gateway hides the upstream model name
|
|
137
|
+
|
|
138
|
+
providers:
|
|
139
|
+
gateway:
|
|
140
|
+
openai_compatible: true
|
|
141
|
+
assume_model_exists: true
|
|
142
|
+
base_url: "https://your-gateway/v1"
|
|
143
|
+
api_key: "${OPENAI_API_KEY}"
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`openai_compatible` providers fall back to `OPENAI_API_KEY` when no `providers.<name>.api_key` is set.
|
|
147
|
+
|
|
148
|
+
### fake (testing only)
|
|
149
|
+
|
|
150
|
+
```yaml
|
|
151
|
+
model:
|
|
152
|
+
default: "fake/happy-path"
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
export RUBINO_ALLOW_FAKE=1 # required; chat/server refuse to boot fake otherwise
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
See the [README](../README.md#fake-llm-provider) for scenario authoring.
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Auxiliary models
|
|
164
|
+
|
|
165
|
+
Compression, approval scoring, vision, and document summarization can each run on a separate (often cheaper) model. By default they reuse the primary (`provider: "main"`). See [configuration.md](configuration.md#auxiliary) — for example, set `auxiliary.vision.model: "auto-vision"` to let an OpenAI-compatible gateway pick a vision model for the `vision` tool.
|
|
166
|
+
|
|
167
|
+
## Verifying
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
rubino doctor
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
`doctor` reports the resolved provider for your configured model and whether a usable credential is present — the same check the chat preflight uses.
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# OAuth provider connectors
|
|
2
|
+
|
|
3
|
+
Built-in OAuth integration lets users connect third-party accounts (Github, Google, etc.) so tools running inside rubino can act on their behalf.
|
|
4
|
+
|
|
5
|
+
## Design
|
|
6
|
+
|
|
7
|
+
Four pieces:
|
|
8
|
+
|
|
9
|
+
1. **`Rubino::OAuth::Provider`** — abstract class. Subclasses describe one provider: authorize URL builder (PKCE S256), token exchange, default scopes, account info fetcher.
|
|
10
|
+
2. **`Rubino::OAuth::Registry`** — Mutex-protected module. `load_from_config!` registers a provider instance per entry in `config.oauth.providers` at boot; lookup by id via `OAuth::Registry.fetch(id)`.
|
|
11
|
+
3. **`Rubino::OAuth::ConnectionRepository`** — Sequel-backed CRUD on `oauth_connections`. Encrypts `access_token`/`refresh_token` on write and decrypts on read. Upsert keyed on `(provider, account_id)`.
|
|
12
|
+
4. **`Rubino::OAuth::TokenEncryptor`** — AES-256-GCM with key from `RUBINO_ENCRYPTION_KEY` (32-byte base64). Wire format: `Base64(IV || ciphertext || tag)`.
|
|
13
|
+
|
|
14
|
+
Tools resolve tokens via the repository:
|
|
15
|
+
```ruby
|
|
16
|
+
repo = Rubino::OAuth::ConnectionRepository.new
|
|
17
|
+
conn = repo.list.find { |c| c[:provider] == "github" }
|
|
18
|
+
client = Octokit::Client.new(access_token: conn[:access_token])
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
> **Current scope:** no auto-refresh. Expired tokens are returned as-is; the tool that uses them is responsible for handling 401s (typically by surfacing a re-auth prompt). A future task will add transparent refresh inside the repository's read path.
|
|
22
|
+
|
|
23
|
+
## Built-in providers
|
|
24
|
+
|
|
25
|
+
| ID | Class | Default scopes | Required env |
|
|
26
|
+
|---|---|---|---|
|
|
27
|
+
| `github` | `OAuth::Provider::Github` | `repo`, `user:email` | `GITHUB_OAUTH_CLIENT_ID`, `GITHUB_OAUTH_CLIENT_SECRET` |
|
|
28
|
+
| `google` | `OAuth::Provider::Google` | `openid`, `email`, `profile` | `GOOGLE_OAUTH_CLIENT_ID`, `GOOGLE_OAUTH_CLIENT_SECRET` |
|
|
29
|
+
|
|
30
|
+
Adding a new provider = new file under `lib/rubino/oauth/provider/`, add it to `Rubino::OAuth::Registry::BUILTINS`, declare it in `config.oauth.providers`. `load_from_config!` (called at boot) instantiates and registers every provider whose section in the config carries both `client_id` and `client_secret`. ~50 LOC for a standard OAuth 2.0 provider.
|
|
31
|
+
|
|
32
|
+
## Flow (PKCE by default)
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
client rubino provider
|
|
36
|
+
│ │ │
|
|
37
|
+
│ POST /v1/oauth/.../connect │ │
|
|
38
|
+
│ ───────────────────────────►│ │
|
|
39
|
+
│ │ generates state + │
|
|
40
|
+
│ │ PKCE code_verifier │
|
|
41
|
+
│ { authorize_url, state, │ │
|
|
42
|
+
│ code_verifier } │ │
|
|
43
|
+
│ ◄───────────────────────────│ │
|
|
44
|
+
│ │ │
|
|
45
|
+
│ user redirected to authorize_url │
|
|
46
|
+
│ ─────────────────────────────────────────────────────►│
|
|
47
|
+
│ │ │
|
|
48
|
+
│ provider redirects to client with code + state │
|
|
49
|
+
│ ◄─────────────────────────────────────────────────────│
|
|
50
|
+
│ │ │
|
|
51
|
+
│ POST /v1/oauth/.../callback│ │
|
|
52
|
+
│ { code, state, expected_state, │
|
|
53
|
+
│ code_verifier, redirect_uri } │
|
|
54
|
+
│ ───────────────────────────►│ │
|
|
55
|
+
│ │ POST /token │
|
|
56
|
+
│ │ ───────────────────────►│
|
|
57
|
+
│ │ ◄───────────────────────│
|
|
58
|
+
│ serialized connection │ │
|
|
59
|
+
│ (id, provider, account_id, │ │
|
|
60
|
+
│ account_email, scopes, │ │
|
|
61
|
+
│ expires_at, metadata) │ │
|
|
62
|
+
│ ◄───────────────────────────│ │
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The **client** (e.g. a web UI) keeps `state` + `code_verifier` between connect and callback. rubino does not maintain a per-flow session — keeps it stateless.
|
|
66
|
+
|
|
67
|
+
## Storage
|
|
68
|
+
|
|
69
|
+
```sql
|
|
70
|
+
CREATE TABLE oauth_connections (
|
|
71
|
+
id text PRIMARY KEY, -- uuid
|
|
72
|
+
provider text NOT NULL,
|
|
73
|
+
account_id text NOT NULL, -- provider's user id
|
|
74
|
+
account_email text,
|
|
75
|
+
access_token text NOT NULL, -- encrypted, Base64(IV||ct||tag)
|
|
76
|
+
refresh_token text, -- encrypted, Base64(IV||ct||tag)
|
|
77
|
+
expires_at text, -- iso8601
|
|
78
|
+
scopes_json text NOT NULL, -- json array
|
|
79
|
+
metadata_json text, -- json
|
|
80
|
+
created_at text NOT NULL,
|
|
81
|
+
updated_at text NOT NULL,
|
|
82
|
+
UNIQUE (provider, account_id)
|
|
83
|
+
);
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The repository transparently encodes/decodes `scopes_json`/`metadata_json` so callers see `:scopes` (Array) and `:metadata` (Hash) on read.
|
|
87
|
+
|
|
88
|
+
Encryption key from `RUBINO_ENCRYPTION_KEY` (32-byte base64). Boot fails if missing in production.
|
|
89
|
+
|
|
90
|
+
**Tokens are never logged. Ever.** The logger has a redaction filter on `access_token`, `refresh_token`, `client_secret`.
|
|
91
|
+
|
|
92
|
+
## Configuration
|
|
93
|
+
|
|
94
|
+
`config/rubino.yml`:
|
|
95
|
+
```yaml
|
|
96
|
+
oauth:
|
|
97
|
+
providers:
|
|
98
|
+
github:
|
|
99
|
+
client_id: ${GITHUB_OAUTH_CLIENT_ID}
|
|
100
|
+
client_secret: ${GITHUB_OAUTH_CLIENT_SECRET}
|
|
101
|
+
scopes: [repo, user:email]
|
|
102
|
+
google:
|
|
103
|
+
client_id: ${GOOGLE_OAUTH_CLIENT_ID}
|
|
104
|
+
client_secret: ${GOOGLE_OAUTH_CLIENT_SECRET}
|
|
105
|
+
scopes:
|
|
106
|
+
- openid
|
|
107
|
+
- email
|
|
108
|
+
- profile
|
|
109
|
+
- https://www.googleapis.com/auth/calendar.readonly
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Providers not declared in config are not registered — `GET /v1/oauth/providers` only lists configured ones.
|
|
113
|
+
|
|
114
|
+
## Setup guides
|
|
115
|
+
|
|
116
|
+
### Github
|
|
117
|
+
|
|
118
|
+
1. Github → Settings → Developer settings → OAuth Apps → New
|
|
119
|
+
2. Authorization callback URL: `<your-client>/oauth/callback`
|
|
120
|
+
3. Copy Client ID + generate Client Secret
|
|
121
|
+
4. Export `GITHUB_OAUTH_CLIENT_ID` / `GITHUB_OAUTH_CLIENT_SECRET`
|
|
122
|
+
|
|
123
|
+
### Google
|
|
124
|
+
|
|
125
|
+
1. Google Cloud Console → APIs & Services → Credentials → Create OAuth client ID
|
|
126
|
+
2. Application type: Web. Authorized redirect URIs: `<your-client>/oauth/callback`
|
|
127
|
+
3. Enable required APIs (Calendar, Gmail, Drive, ...) based on scopes you want
|
|
128
|
+
4. Export `GOOGLE_OAUTH_CLIENT_ID` / `GOOGLE_OAUTH_CLIENT_SECRET`
|
|
129
|
+
|
|
130
|
+
## Why we did this (and not "delegate to client")
|
|
131
|
+
|
|
132
|
+
Rich did it: it has `/api/providers/oauth/*`. Reason it makes sense in rubino too:
|
|
133
|
+
|
|
134
|
+
- **Tools need tokens.** A `GithubTool` needs a token to call the API. If OAuth is the client's responsibility, the client has to forward tokens with every run, which is ugly and leaky.
|
|
135
|
+
- **Refresh logic will be centralized.** Once auto-refresh lands, expired tokens get refreshed in one place, not duplicated per client.
|
|
136
|
+
- **Encrypted persistence.** Clients shouldn't store user tokens long-term; the agent does, encrypted, with a redaction-aware logger.
|
|
137
|
+
|
|
138
|
+
The client (a web UI) handles only the redirect dance — opening the authorize URL in a browser and POSTing the code back. Everything else stays here.
|
|
139
|
+
|
|
140
|
+
## Non-goals
|
|
141
|
+
|
|
142
|
+
- **Apple Sign-In:** uses JWT-signed assertions, not standard OAuth. Postponed.
|
|
143
|
+
- **Multi-account per provider:** one connection per provider per instance is supported. Multi-account requires UI for selection — out of scope.
|
|
144
|
+
- **OAuth1.0:** Twitter/X is the only relevant one. Postponed.
|
|
145
|
+
- **OIDC discovery:** providers are explicit classes. No `.well-known/openid-configuration` autodiscovery.
|
data/docs/plugins.md
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# Plugin System
|
|
2
|
+
|
|
3
|
+
rubino defines 38 hook points for extending and customizing behavior.
|
|
4
|
+
|
|
5
|
+
> **Status:** The plugin registry and DSL (`Rubino.plugin do ... end`) are
|
|
6
|
+
> functional, but the hook points themselves are **not yet fired from the
|
|
7
|
+
> lifecycle** — they are declared in `Plugins::HOOKS` but not all are wired into
|
|
8
|
+
> the runtime yet. This document describes the intended hooks and their context
|
|
9
|
+
> shape (design intent); subscribing to a hook that isn't fired yet is harmless
|
|
10
|
+
> but has no effect.
|
|
11
|
+
|
|
12
|
+
## Creating a Plugin
|
|
13
|
+
|
|
14
|
+
Place Ruby files in `.rubino/plugins/` or `~/.rubino/plugins/`:
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
# .rubino/plugins/logging.rb
|
|
18
|
+
Rubino.plugin do
|
|
19
|
+
on(:tool_execute_before) do |context|
|
|
20
|
+
puts "[AUDIT] Tool: #{context[:tool_name]} args: #{context[:arguments]}"
|
|
21
|
+
context # Return context (optionally modified)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
on(:tool_execute_after) do |context|
|
|
25
|
+
puts "[AUDIT] Result: #{context[:result]&.truncated_preview}"
|
|
26
|
+
context
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Hook Behavior
|
|
32
|
+
|
|
33
|
+
- Hooks receive a context hash and should return it (optionally modified)
|
|
34
|
+
- Multiple handlers per hook are supported (executed in registration order)
|
|
35
|
+
- If a handler returns a Hash, it's merged into the context for the next handler
|
|
36
|
+
- Errors in hooks are caught and logged but don't break the main flow
|
|
37
|
+
|
|
38
|
+
## All 38 Hooks
|
|
39
|
+
|
|
40
|
+
### Tool Lifecycle
|
|
41
|
+
|
|
42
|
+
| Hook | Trigger | Context Keys |
|
|
43
|
+
|------|---------|--------------|
|
|
44
|
+
| `tool_execute_before` | Before any tool runs | `tool_name`, `arguments`, `session_id` |
|
|
45
|
+
| `tool_execute_after` | After tool completes | `tool_name`, `arguments`, `result`, `duration` |
|
|
46
|
+
| `tool_approval_before` | Before approval check | `tool_name`, `risk_level`, `arguments` |
|
|
47
|
+
| `tool_approval_after` | After approval decision | `tool_name`, `decision` (:allow/:ask/:deny) |
|
|
48
|
+
| `tool_result_transform` | Transform tool output | `tool_name`, `result` |
|
|
49
|
+
|
|
50
|
+
### Shell
|
|
51
|
+
|
|
52
|
+
| Hook | Trigger | Context Keys |
|
|
53
|
+
|------|---------|--------------|
|
|
54
|
+
| `shell_env` | Inject env vars into shell | `command`, `env` (mutable hash) |
|
|
55
|
+
| `shell_execute_before` | Before shell command | `command`, `cwd` |
|
|
56
|
+
| `shell_execute_after` | After shell command | `command`, `output`, `exit_code` |
|
|
57
|
+
|
|
58
|
+
### File Operations
|
|
59
|
+
|
|
60
|
+
| Hook | Trigger | Context Keys |
|
|
61
|
+
|------|---------|--------------|
|
|
62
|
+
| `file_read_before` | Before reading a file | `path` |
|
|
63
|
+
| `file_read_after` | After reading a file | `path`, `content`, `size` |
|
|
64
|
+
| `file_write_before` | Before writing a file | `path`, `content` |
|
|
65
|
+
| `file_write_after` | After writing a file | `path`, `size` |
|
|
66
|
+
|
|
67
|
+
### Context Compaction
|
|
68
|
+
|
|
69
|
+
| Hook | Trigger | Context Keys |
|
|
70
|
+
|------|---------|--------------|
|
|
71
|
+
| `compaction_before` | Before compaction starts | `session_id`, `message_count`, `token_estimate` |
|
|
72
|
+
| `compaction_after` | After compaction finishes | `session_id`, `saved_tokens`, `new_session_id` |
|
|
73
|
+
| `compaction_context_inject` | Inject custom context into compaction summary | `session_id`, `messages` |
|
|
74
|
+
|
|
75
|
+
### Sessions
|
|
76
|
+
|
|
77
|
+
| Hook | Trigger | Context Keys |
|
|
78
|
+
|------|---------|--------------|
|
|
79
|
+
| `session_start` | New session created | `session_id`, `model`, `source` |
|
|
80
|
+
| `session_end` | Session ended | `session_id`, `message_count` |
|
|
81
|
+
| `session_fork` | Session forked | `source_id`, `forked_id`, `at_message` |
|
|
82
|
+
| `session_persist` | Session state saved | `session_id`, `token_count` |
|
|
83
|
+
|
|
84
|
+
### Messages
|
|
85
|
+
|
|
86
|
+
| Hook | Trigger | Context Keys |
|
|
87
|
+
|------|---------|--------------|
|
|
88
|
+
| `message_before` | Before message is processed | `role`, `content`, `session_id` |
|
|
89
|
+
| `message_after` | After message persisted | `message_id`, `role`, `content` |
|
|
90
|
+
| `message_stream_chunk` | Each streaming chunk | `chunk`, `session_id` |
|
|
91
|
+
|
|
92
|
+
### Memory
|
|
93
|
+
|
|
94
|
+
| Hook | Trigger | Context Keys |
|
|
95
|
+
|------|---------|--------------|
|
|
96
|
+
| `memory_extract` | During memory extraction | `session_id`, `candidates` |
|
|
97
|
+
| `memory_save_before` | Before saving a memory | `kind`, `content` |
|
|
98
|
+
| `memory_retrieve_after` | After retrieving memories | `memories`, `context` |
|
|
99
|
+
|
|
100
|
+
### Jobs
|
|
101
|
+
|
|
102
|
+
| Hook | Trigger | Context Keys |
|
|
103
|
+
|------|---------|--------------|
|
|
104
|
+
| `job_before` | Before job execution | `job_type`, `payload` |
|
|
105
|
+
| `job_after` | After job completes | `job_type`, `result` |
|
|
106
|
+
| `job_failed` | When job fails | `job_type`, `error`, `attempts` |
|
|
107
|
+
|
|
108
|
+
### Model/LLM
|
|
109
|
+
|
|
110
|
+
| Hook | Trigger | Context Keys |
|
|
111
|
+
|------|---------|--------------|
|
|
112
|
+
| `model_call_before` | Before LLM API call | `model`, `messages`, `tools` |
|
|
113
|
+
| `model_call_after` | After LLM response | `model`, `response`, `tokens` |
|
|
114
|
+
| `model_response_transform` | Transform LLM response | `content`, `tool_calls` |
|
|
115
|
+
|
|
116
|
+
### Prompt Assembly
|
|
117
|
+
|
|
118
|
+
| Hook | Trigger | Context Keys |
|
|
119
|
+
|------|---------|--------------|
|
|
120
|
+
| `prompt_assemble_before` | Before building prompt | `session_id`, `memory_context` |
|
|
121
|
+
| `prompt_assemble_after` | After prompt built | `messages`, `token_estimate` |
|
|
122
|
+
|
|
123
|
+
### Agent
|
|
124
|
+
|
|
125
|
+
| Hook | Trigger | Context Keys |
|
|
126
|
+
|------|---------|--------------|
|
|
127
|
+
| `agent_switch` | Agent switched | `from`, `to` |
|
|
128
|
+
| `agent_route` | Input routed to agent | `input`, `agent_name` |
|
|
129
|
+
|
|
130
|
+
### System
|
|
131
|
+
|
|
132
|
+
| Hook | Trigger | Context Keys |
|
|
133
|
+
|------|---------|--------------|
|
|
134
|
+
| `config_reload` | Config reloaded | `config` |
|
|
135
|
+
| `startup` | Application starting | `version` |
|
|
136
|
+
| `shutdown` | Application stopping | `reason` |
|
|
137
|
+
|
|
138
|
+
## Examples
|
|
139
|
+
|
|
140
|
+
### Auto-format after writes
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
Rubino.plugin do
|
|
144
|
+
on(:file_write_after) do |context|
|
|
145
|
+
path = context[:path]
|
|
146
|
+
if path.end_with?(".rb")
|
|
147
|
+
system("rubocop -A '#{path}' 2>/dev/null")
|
|
148
|
+
elsif path.end_with?(".js", ".ts")
|
|
149
|
+
system("prettier --write '#{path}' 2>/dev/null")
|
|
150
|
+
end
|
|
151
|
+
context
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Inject environment into shell
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
Rubino.plugin do
|
|
160
|
+
on(:shell_env) do |context|
|
|
161
|
+
context[:env]["RAILS_ENV"] ||= "development"
|
|
162
|
+
context[:env]["BUNDLE_GEMFILE"] ||= File.join(Dir.pwd, "Gemfile")
|
|
163
|
+
context
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Block dangerous operations
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
Rubino.plugin do
|
|
172
|
+
on(:tool_execute_before) do |context|
|
|
173
|
+
if context[:tool_name] == "shell"
|
|
174
|
+
cmd = context.dig(:arguments, "command") || ""
|
|
175
|
+
if cmd.match?(/rm\s+-rf\s+\/|dd\s+if=|mkfs|format/)
|
|
176
|
+
raise Rubino::ToolError, "Blocked dangerous command: #{cmd}"
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
context
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Custom telemetry
|
|
185
|
+
|
|
186
|
+
```ruby
|
|
187
|
+
Rubino.plugin do
|
|
188
|
+
on(:model_call_after) do |context|
|
|
189
|
+
tokens = context[:tokens] || 0
|
|
190
|
+
model = context[:model]
|
|
191
|
+
File.open("usage.log", "a") { |f| f.puts "#{Time.now.iso8601} #{model} #{tokens}" }
|
|
192
|
+
context
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
```
|