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,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require "digest"
|
|
5
|
+
require "base64"
|
|
6
|
+
require "oauth2"
|
|
7
|
+
|
|
8
|
+
module Rubino
|
|
9
|
+
module OAuth
|
|
10
|
+
# Abstract OAuth 2.0 provider. Subclasses declare endpoints + default scopes
|
|
11
|
+
# and implement #fetch_account_info to populate account_id/account_email
|
|
12
|
+
# after a successful token exchange.
|
|
13
|
+
#
|
|
14
|
+
# Configured per-provider with client_id, client_secret, scopes from
|
|
15
|
+
# rubino.yml. PKCE (S256) is enabled by default for the auth_code flow.
|
|
16
|
+
# The agent is stateless across the redirect: the client persists the
|
|
17
|
+
# returned +state+ and +code_verifier+ between connect and callback.
|
|
18
|
+
class Provider
|
|
19
|
+
attr_reader :client_id, :client_secret, :scopes, :metadata
|
|
20
|
+
|
|
21
|
+
def self.id
|
|
22
|
+
raise NotImplementedError
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.display_name
|
|
26
|
+
id.to_s.capitalize
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.site
|
|
30
|
+
raise NotImplementedError
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.authorize_path
|
|
34
|
+
raise NotImplementedError
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.token_path
|
|
38
|
+
raise NotImplementedError
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.default_scopes
|
|
42
|
+
[]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def initialize(client_id:, client_secret:, scopes: nil, metadata: {})
|
|
46
|
+
@client_id = client_id
|
|
47
|
+
@client_secret = client_secret
|
|
48
|
+
@scopes = (scopes || self.class.default_scopes).map(&:to_s)
|
|
49
|
+
@metadata = metadata
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def id
|
|
53
|
+
self.class.id
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Build the authorize URL the client must redirect the user to.
|
|
57
|
+
#
|
|
58
|
+
# The returned +state+ and +code_verifier+ MUST be persisted by the
|
|
59
|
+
# caller and replayed on the callback — rubino keeps no per-flow
|
|
60
|
+
# session.
|
|
61
|
+
#
|
|
62
|
+
# @param redirect_uri [String] absolute callback URL registered with the provider
|
|
63
|
+
# @param scopes [Array<String>, nil] overrides the instance default scopes when present
|
|
64
|
+
# @param extra [Hash] additional query parameters appended to the authorize URL
|
|
65
|
+
# @return [Hash] with keys +:authorize_url+ (String), +:state+ (String,
|
|
66
|
+
# urlsafe base64), +:code_verifier+ (String, PKCE verifier)
|
|
67
|
+
def build_authorize_request(redirect_uri:, scopes: nil, extra: {})
|
|
68
|
+
state = SecureRandom.urlsafe_base64(32)
|
|
69
|
+
code_verifier = SecureRandom.urlsafe_base64(64)
|
|
70
|
+
code_challenge = pkce_challenge(code_verifier)
|
|
71
|
+
|
|
72
|
+
url = oauth2_client.auth_code.authorize_url(
|
|
73
|
+
redirect_uri: redirect_uri,
|
|
74
|
+
scope: Array(scopes || @scopes).join(scope_separator),
|
|
75
|
+
state: state,
|
|
76
|
+
code_challenge: code_challenge,
|
|
77
|
+
code_challenge_method: "S256",
|
|
78
|
+
**extra
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
{ authorize_url: url, state: state, code_verifier: code_verifier }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Exchange the authorization code for tokens.
|
|
85
|
+
#
|
|
86
|
+
# @param code [String] authorization code returned by the provider
|
|
87
|
+
# @param redirect_uri [String] same redirect_uri used in {#build_authorize_request}
|
|
88
|
+
# @param code_verifier [String] PKCE verifier paired with the original challenge
|
|
89
|
+
# @return [Hash] with keys +:access_token+ (String), +:refresh_token+
|
|
90
|
+
# (String, nil), +:expires_at+ (String ISO8601 UTC, nil), +:scopes+
|
|
91
|
+
# (Array<String>)
|
|
92
|
+
def exchange_code(code:, redirect_uri:, code_verifier:)
|
|
93
|
+
token = oauth2_client.auth_code.get_token(
|
|
94
|
+
code,
|
|
95
|
+
redirect_uri: redirect_uri,
|
|
96
|
+
code_verifier: code_verifier
|
|
97
|
+
)
|
|
98
|
+
normalize(token)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def refresh(refresh_token)
|
|
102
|
+
token = OAuth2::AccessToken.new(oauth2_client, "", refresh_token: refresh_token)
|
|
103
|
+
normalize(token.refresh!)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Provider-specific call to /userinfo (or equivalent) using the access
|
|
107
|
+
# token.
|
|
108
|
+
#
|
|
109
|
+
# @param _access_token [String]
|
|
110
|
+
# @return [Hash] with keys +:account_id+ (String), +:account_email+
|
|
111
|
+
# (String, nil), +:metadata+ (Hash)
|
|
112
|
+
def fetch_account_info(_access_token)
|
|
113
|
+
raise NotImplementedError
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
private
|
|
117
|
+
|
|
118
|
+
def oauth2_client
|
|
119
|
+
@oauth2_client ||= OAuth2::Client.new(
|
|
120
|
+
@client_id,
|
|
121
|
+
@client_secret,
|
|
122
|
+
site: self.class.site,
|
|
123
|
+
authorize_url: self.class.authorize_path,
|
|
124
|
+
token_url: self.class.token_path
|
|
125
|
+
)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def scope_separator
|
|
129
|
+
" "
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def normalize(token)
|
|
133
|
+
expires_at = token.expires_at ? Time.at(token.expires_at).utc.iso8601 : nil
|
|
134
|
+
{
|
|
135
|
+
access_token: token.token,
|
|
136
|
+
refresh_token: token.refresh_token,
|
|
137
|
+
expires_at: expires_at,
|
|
138
|
+
scopes: (token.params["scope"] || @scopes.join(" ")).to_s.split(/[\s,]+/).reject(&:empty?)
|
|
139
|
+
}
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# PKCE S256 challenge: SHA-256 of the verifier, base64url-encoded with
|
|
143
|
+
# padding stripped (RFC 7636 §4.2 — providers reject "=" padding).
|
|
144
|
+
def pkce_challenge(verifier)
|
|
145
|
+
Base64.urlsafe_encode64(Digest::SHA256.digest(verifier), padding: false)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubino
|
|
4
|
+
module OAuth
|
|
5
|
+
# Process-wide registry of configured OAuth providers. Mutex-protected so
|
|
6
|
+
# +register+ / +reset!+ are safe under concurrent boot or reload.
|
|
7
|
+
#
|
|
8
|
+
# Hydrated from +oauth.providers.*+ in Rubino.configuration; only ids
|
|
9
|
+
# listed in {BUILTINS} are considered, and any section missing both
|
|
10
|
+
# +client_id+ and +client_secret+ is silently skipped so a partial config
|
|
11
|
+
# never raises at boot (it just hides the provider from
|
|
12
|
+
# +/v1/oauth/providers+).
|
|
13
|
+
module Registry
|
|
14
|
+
BUILTINS = {
|
|
15
|
+
github: "Rubino::OAuth::Provider::Github",
|
|
16
|
+
google: "Rubino::OAuth::Provider::Google"
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
def register(id, instance)
|
|
21
|
+
mutex.synchronize { providers[id.to_sym] = instance }
|
|
22
|
+
instance
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @param id [String, Symbol]
|
|
26
|
+
# @return [Provider]
|
|
27
|
+
# @raise [Rubino::NotFoundError] when no provider is registered for +id+
|
|
28
|
+
def fetch(id)
|
|
29
|
+
providers[id.to_sym] or raise NotFoundError.new("oauth_provider", id)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def fetch_or_nil(id)
|
|
33
|
+
providers[id.to_sym]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def all
|
|
37
|
+
providers.values
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def ids
|
|
41
|
+
providers.keys
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def reset!
|
|
45
|
+
mutex.synchronize { providers.clear }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Hydrate from the loaded Rubino configuration. Reads oauth.providers.*
|
|
49
|
+
# sections; for each id matching a BUILTIN, instantiates and registers
|
|
50
|
+
# using its declared client_id/client_secret/scopes. Replaces any
|
|
51
|
+
# previously registered providers ({#reset!} runs first).
|
|
52
|
+
#
|
|
53
|
+
# @param configuration [#dig] anything responding to +dig("oauth", "providers")+
|
|
54
|
+
# @return [Array<Provider>] providers registered by this call
|
|
55
|
+
def load_from_config!(configuration = Rubino.configuration)
|
|
56
|
+
reset!
|
|
57
|
+
oauth_cfg = configuration.dig("oauth", "providers") || {}
|
|
58
|
+
oauth_cfg.each do |id, cfg|
|
|
59
|
+
klass_name = BUILTINS[id.to_sym]
|
|
60
|
+
next unless klass_name
|
|
61
|
+
next unless cfg["client_id"] && cfg["client_secret"]
|
|
62
|
+
|
|
63
|
+
klass = Object.const_get(klass_name)
|
|
64
|
+
register(id, klass.new(
|
|
65
|
+
client_id: cfg["client_id"],
|
|
66
|
+
client_secret: cfg["client_secret"],
|
|
67
|
+
scopes: cfg["scopes"],
|
|
68
|
+
metadata: cfg.reject { |k, _| %w[client_id client_secret scopes].include?(k) }
|
|
69
|
+
))
|
|
70
|
+
end
|
|
71
|
+
all
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def providers
|
|
77
|
+
@providers ||= {}
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def mutex
|
|
81
|
+
@mutex ||= Mutex.new
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
require "base64"
|
|
5
|
+
|
|
6
|
+
module Rubino
|
|
7
|
+
module OAuth
|
|
8
|
+
# AES-256-GCM symmetric encryption for OAuth tokens at rest.
|
|
9
|
+
#
|
|
10
|
+
# Key supplied via RUBINO_ENCRYPTION_KEY env (32 raw bytes encoded as
|
|
11
|
+
# standard base64). Generate one with:
|
|
12
|
+
# ruby -rsecurerandom -rbase64 -e 'puts Base64.strict_encode64(SecureRandom.random_bytes(32))'
|
|
13
|
+
#
|
|
14
|
+
# Wire format is Base64(IV || ciphertext || tag) with a 12-byte IV and a
|
|
15
|
+
# 16-byte GCM auth tag. {.from_env} raises {KeyMissingError} when the env
|
|
16
|
+
# var is missing or not a 32-byte key; {#decrypt} raises
|
|
17
|
+
# {InvalidCiphertextError} on tampered or truncated payloads.
|
|
18
|
+
class TokenEncryptor
|
|
19
|
+
CIPHER = "aes-256-gcm"
|
|
20
|
+
IV_LEN = 12
|
|
21
|
+
TAG_LEN = 16
|
|
22
|
+
|
|
23
|
+
class KeyMissingError < Rubino::Error
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class InvalidCiphertextError < Rubino::Error
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Build an encryptor using the key in RUBINO_ENCRYPTION_KEY.
|
|
30
|
+
#
|
|
31
|
+
# @return [TokenEncryptor]
|
|
32
|
+
# @raise [KeyMissingError] if the env var is unset, empty, or does not
|
|
33
|
+
# decode to exactly 32 bytes
|
|
34
|
+
def self.from_env
|
|
35
|
+
raw = ENV.fetch("RUBINO_ENCRYPTION_KEY", nil)
|
|
36
|
+
raise KeyMissingError, "RUBINO_ENCRYPTION_KEY not set" if raw.nil? || raw.empty?
|
|
37
|
+
|
|
38
|
+
key = Base64.strict_decode64(raw)
|
|
39
|
+
raise KeyMissingError, "RUBINO_ENCRYPTION_KEY must decode to 32 bytes" unless key.bytesize == 32
|
|
40
|
+
|
|
41
|
+
new(key)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def initialize(key)
|
|
45
|
+
raise ArgumentError, "key must be 32 bytes" unless key.bytesize == 32
|
|
46
|
+
|
|
47
|
+
@key = key
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @param plaintext [String, nil]
|
|
51
|
+
# @return [String, nil] Base64(IV || ciphertext || tag), or nil when
|
|
52
|
+
# plaintext is nil (so nullable token columns round-trip unchanged)
|
|
53
|
+
def encrypt(plaintext)
|
|
54
|
+
return nil if plaintext.nil?
|
|
55
|
+
|
|
56
|
+
cipher = OpenSSL::Cipher.new(CIPHER).encrypt
|
|
57
|
+
cipher.key = @key
|
|
58
|
+
iv = cipher.random_iv
|
|
59
|
+
ciphertext = cipher.update(plaintext.to_s) + cipher.final
|
|
60
|
+
Base64.strict_encode64(iv + ciphertext + cipher.auth_tag)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# @param payload [String, nil] a value previously returned by {#encrypt}
|
|
64
|
+
# @return [String, nil] the original plaintext, or nil when payload is nil
|
|
65
|
+
# @raise [InvalidCiphertextError] if the payload is too short or the GCM
|
|
66
|
+
# auth tag does not verify (tampering, wrong key, truncation)
|
|
67
|
+
def decrypt(payload)
|
|
68
|
+
return nil if payload.nil?
|
|
69
|
+
|
|
70
|
+
bytes = Base64.strict_decode64(payload)
|
|
71
|
+
raise InvalidCiphertextError, "payload too short" if bytes.bytesize <= IV_LEN + TAG_LEN
|
|
72
|
+
|
|
73
|
+
iv = bytes.byteslice(0, IV_LEN)
|
|
74
|
+
tag = bytes.byteslice(-TAG_LEN, TAG_LEN)
|
|
75
|
+
ciphertext = bytes.byteslice(IV_LEN, bytes.bytesize - IV_LEN - TAG_LEN)
|
|
76
|
+
|
|
77
|
+
cipher = OpenSSL::Cipher.new(CIPHER).decrypt
|
|
78
|
+
cipher.key = @key
|
|
79
|
+
cipher.iv = iv
|
|
80
|
+
cipher.auth_tag = tag
|
|
81
|
+
cipher.update(ciphertext) + cipher.final
|
|
82
|
+
rescue OpenSSL::Cipher::CipherError => e
|
|
83
|
+
raise InvalidCiphertextError, "decryption failed: #{e.message}"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubino
|
|
4
|
+
module Plugins
|
|
5
|
+
# Central registry for plugins and their hooks.
|
|
6
|
+
class Registry
|
|
7
|
+
def initialize
|
|
8
|
+
@hooks = Hash.new { |h, k| h[k] = [] }
|
|
9
|
+
@plugins = []
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Registers a hook handler
|
|
13
|
+
def on(event, &block)
|
|
14
|
+
raise Error, "Unknown hook: #{event}. Valid: #{HOOKS.join(", ")}" unless HOOKS.include?(event.to_sym)
|
|
15
|
+
|
|
16
|
+
@hooks[event.to_sym] << block
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Executes all handlers for a hook, passing context through each
|
|
20
|
+
def run_hook(event, context = {})
|
|
21
|
+
@hooks[event.to_sym].each do |handler|
|
|
22
|
+
result = handler.call(context)
|
|
23
|
+
# If handler returns a hash, merge it into context
|
|
24
|
+
context = context.merge(result) if result.is_a?(Hash)
|
|
25
|
+
end
|
|
26
|
+
context
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Returns true if any handlers are registered for this hook
|
|
30
|
+
def has_hook?(event)
|
|
31
|
+
@hooks[event.to_sym].any?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Loads a plugin from a file
|
|
35
|
+
def load_plugin(path)
|
|
36
|
+
load(path)
|
|
37
|
+
@plugins << path
|
|
38
|
+
rescue StandardError => e
|
|
39
|
+
Rubino.ui.warning("Failed to load plugin #{path}: #{e.message}")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Loads all plugins from configured paths
|
|
43
|
+
def load_all!
|
|
44
|
+
plugin_paths.each do |dir|
|
|
45
|
+
expanded = File.expand_path(dir)
|
|
46
|
+
next unless File.directory?(expanded)
|
|
47
|
+
|
|
48
|
+
Dir.glob(File.join(expanded, "*.rb")).each do |path|
|
|
49
|
+
load_plugin(path)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Returns count of loaded plugins
|
|
55
|
+
def plugin_count
|
|
56
|
+
@plugins.size
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Clears all hooks and plugins (for testing)
|
|
60
|
+
def reset!
|
|
61
|
+
@hooks.clear
|
|
62
|
+
@plugins.clear
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def plugin_paths
|
|
68
|
+
[
|
|
69
|
+
".rubino/plugins",
|
|
70
|
+
"~/.rubino/plugins"
|
|
71
|
+
]
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubino
|
|
4
|
+
# Plugin system with event hooks.
|
|
5
|
+
# Plugins can subscribe to events and modify behavior.
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# Rubino.plugin do
|
|
9
|
+
# on(:tool_execute_before) do |context|
|
|
10
|
+
# # Modify or inspect tool call before execution
|
|
11
|
+
# end
|
|
12
|
+
#
|
|
13
|
+
# on(:tool_execute_after) do |context|
|
|
14
|
+
# # React to tool results
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# on(:session_start) do |context|
|
|
18
|
+
# # Do something when a session starts
|
|
19
|
+
# end
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
module Plugins
|
|
23
|
+
# All supported hook points (30+)
|
|
24
|
+
HOOKS = %i[
|
|
25
|
+
tool_execute_before
|
|
26
|
+
tool_execute_after
|
|
27
|
+
tool_approval_before
|
|
28
|
+
tool_approval_after
|
|
29
|
+
tool_result_transform
|
|
30
|
+
|
|
31
|
+
shell_env
|
|
32
|
+
shell_execute_before
|
|
33
|
+
shell_execute_after
|
|
34
|
+
|
|
35
|
+
file_read_before
|
|
36
|
+
file_read_after
|
|
37
|
+
file_write_before
|
|
38
|
+
file_write_after
|
|
39
|
+
|
|
40
|
+
compaction_before
|
|
41
|
+
compaction_after
|
|
42
|
+
compaction_context_inject
|
|
43
|
+
|
|
44
|
+
session_start
|
|
45
|
+
session_end
|
|
46
|
+
session_fork
|
|
47
|
+
session_persist
|
|
48
|
+
|
|
49
|
+
message_before
|
|
50
|
+
message_after
|
|
51
|
+
message_stream_chunk
|
|
52
|
+
|
|
53
|
+
memory_extract
|
|
54
|
+
memory_save_before
|
|
55
|
+
memory_retrieve_after
|
|
56
|
+
|
|
57
|
+
job_before
|
|
58
|
+
job_after
|
|
59
|
+
job_failed
|
|
60
|
+
|
|
61
|
+
model_call_before
|
|
62
|
+
model_call_after
|
|
63
|
+
model_response_transform
|
|
64
|
+
|
|
65
|
+
prompt_assemble_before
|
|
66
|
+
prompt_assemble_after
|
|
67
|
+
|
|
68
|
+
agent_switch
|
|
69
|
+
agent_route
|
|
70
|
+
|
|
71
|
+
config_reload
|
|
72
|
+
startup
|
|
73
|
+
shutdown
|
|
74
|
+
].freeze
|
|
75
|
+
|
|
76
|
+
class << self
|
|
77
|
+
def registry
|
|
78
|
+
@registry ||= Registry.new
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def reset!
|
|
82
|
+
@registry = nil
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|