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/api/v1.md
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
# HTTP API v1
|
|
2
|
+
|
|
3
|
+
Base URL: `http://<host>:<port>/v1` (default port 4820)
|
|
4
|
+
Auth: `Authorization: Bearer <RUBINO_API_KEY>` on every request, EXCEPT `GET /v1/health` and `GET /v1/metrics`.
|
|
5
|
+
Content type: `application/json` unless noted.
|
|
6
|
+
|
|
7
|
+
Every endpoint listed below is covered by an end-to-end contract spec in `spec/rubino/api/contract/`. If a behavior here disagrees with the contract suite, the contract suite is the source of truth.
|
|
8
|
+
|
|
9
|
+
## Conventions
|
|
10
|
+
|
|
11
|
+
| | |
|
|
12
|
+
|---|---|
|
|
13
|
+
| **IDs** | UUIDv4 strings (`SecureRandom.uuid`). |
|
|
14
|
+
| **Timestamps** | ISO 8601 UTC with the `Z` suffix. |
|
|
15
|
+
| **Error envelope** | `{"error": {"code": "string", "message": "string", "details": {...}?}}` — identical shape on every error code. `details` is present only when the error carries one (notably 422). |
|
|
16
|
+
| **Status codes** | 200/201/202/204 success · 401 auth · 404 missing resource or route · 409 conflict · 422 validation · 500 internal · 502 upstream. |
|
|
17
|
+
| **SSE** | streams use `id: <seq>\nevent: <type>\ndata: <json-payload>\n\n`. Supports `Last-Event-ID` for replay. |
|
|
18
|
+
| **Bearer** | scheme is case-insensitive (`Bearer` and `bearer` both accepted). Wrong/missing token → 401 envelope. |
|
|
19
|
+
| **Routing 404** | unknown route returns 404 in the standard envelope with `code: "not_found"`. |
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Health & metrics (unauthenticated)
|
|
24
|
+
|
|
25
|
+
### `GET /v1/health` → 200 | 503
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"status": "ok",
|
|
29
|
+
"version": "0.3.0",
|
|
30
|
+
"deps": {
|
|
31
|
+
"db": { "status": "ok" },
|
|
32
|
+
"scheduler": { "status": "ok", "scheduled_jobs": 0 }
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
Returns 503 with `status: "degraded"` when any dep reports non-ok.
|
|
37
|
+
|
|
38
|
+
### `GET /v1/metrics` → 200 `text/plain; version=0.0.4`
|
|
39
|
+
Prometheus text exposition. Registered metrics include `http_requests_total`, `http_request_duration_seconds`, `cron_fires_total`, `webhook_deliveries_total`, `oauth_token_exchanges_total`, `runs_total`, `runs_completed_total`, `skills_loaded_total`, `skills_created_total`. The two skill counters measure adoption vs. creation — see **[docs/skills.md](../skills.md#observability)**.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Sessions
|
|
44
|
+
|
|
45
|
+
### `GET /v1/sessions` → 200
|
|
46
|
+
Session index, most recent first. Query: `limit` (default 20, max 100), `q` (full-text search over message content; hits are deduped to sessions).
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"sessions": [
|
|
50
|
+
{ "id": "uuid", "title": "string|null", "status": "string",
|
|
51
|
+
"created_at": "ts", "updated_at": "ts",
|
|
52
|
+
"message_count": 12, "token_count": 3456 }
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### `POST /v1/sessions` → 201
|
|
58
|
+
```json
|
|
59
|
+
// req — all fields optional
|
|
60
|
+
{ "title": "string?", "parent_id": "uuid?", "instructions": "string?" }
|
|
61
|
+
// res
|
|
62
|
+
{ "id": "uuid", "title": "string|null", "parent_id": "uuid|null", "created_at": "ts" }
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### `GET /v1/sessions/:id` → 200 | 404
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"id": "uuid",
|
|
69
|
+
"title": "string|null",
|
|
70
|
+
"instructions": null,
|
|
71
|
+
"status": "string",
|
|
72
|
+
"created_at": "ts",
|
|
73
|
+
"messages": [
|
|
74
|
+
{ "id": "uuid", "role": "user|assistant|tool", "content": "string", "created_at": "ts" }
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### `DELETE /v1/sessions/:id` → 204 | 404
|
|
80
|
+
Cascade-deletes messages, runs, events, summaries, tool calls.
|
|
81
|
+
|
|
82
|
+
### `POST /v1/sessions/:id/retry` → 202 | 404 | 409
|
|
83
|
+
Deletes the last user message (and everything after it), enqueues a fresh run with the same input.
|
|
84
|
+
```json
|
|
85
|
+
{ "run_id": "uuid", "session_id": "uuid", "status": "running" }
|
|
86
|
+
```
|
|
87
|
+
409 when the session has no user message to retry.
|
|
88
|
+
|
|
89
|
+
### `POST /v1/sessions/:id/undo` → 200 | 404 | 409
|
|
90
|
+
Removes the last user message and everything after it (no re-run).
|
|
91
|
+
```json
|
|
92
|
+
{ "removed_messages": 3 }
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Runs
|
|
98
|
+
|
|
99
|
+
### `POST /v1/sessions/:id/runs` → 201 | 404 | 422
|
|
100
|
+
```json
|
|
101
|
+
// req
|
|
102
|
+
{
|
|
103
|
+
"input": "string", // required
|
|
104
|
+
"attachments": ["string"]?,
|
|
105
|
+
"skills": ["string"]?,
|
|
106
|
+
"model": "string|null",
|
|
107
|
+
"provider": "string|null"
|
|
108
|
+
}
|
|
109
|
+
// res
|
|
110
|
+
{ "id": "uuid", "session_id": "uuid", "status": "running", "created_at": "ts" }
|
|
111
|
+
```
|
|
112
|
+
The run is dispatched to a background Executor immediately; tail `/v1/runs/:id/events` for state.
|
|
113
|
+
|
|
114
|
+
### `POST /v1/runs/:id/stop` → 200 | 404
|
|
115
|
+
Cooperative stop — flips the `stop_requested` flag; the executor exits between turns.
|
|
116
|
+
```json
|
|
117
|
+
{ "id": "uuid", "status": "stop_requested" }
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### `GET /v1/runs/:id/events` → 200 (SSE) | 404
|
|
121
|
+
Replays persisted events first (honoring `Last-Event-ID`), then polls for new ones until the run reaches a terminal status (`completed`, `failed`, `stopped`) or disappears. Headers:
|
|
122
|
+
```
|
|
123
|
+
content-type: text/event-stream
|
|
124
|
+
cache-control: no-cache
|
|
125
|
+
x-accel-buffering: no
|
|
126
|
+
```
|
|
127
|
+
Event types that can appear on the stream: `message.completed`, `tool.started`, `tool.progress`, `tool.completed`, `artifact.created`, `input.injected`, `skill.loaded`, `subagent.spawned`, `subagent.completed`, `subagent.failed`, `approval.required` (`{approval_id, question, tool, command, choices, ...}`), `approval.decided` (`{approval_id, decision}`), `approval.expired` (`{approval_id}`), `clarify.required` (`{clarify_id, question}`), `run.completed`, `run.failed`, `run.stopped`.
|
|
128
|
+
|
|
129
|
+
`approval.decided` follows the `approval.required` frame once a decision lands (posted via the approvals endpoint), carrying the chosen `decision`; `approval.expired` fires instead if the gate's await deadline elapses first, after which the server treats the call as a safe deny. `run.stopped` is emitted on the clean-stop branch (a stop request that the run honored) — it is both this distinct event AND the terminal `stopped` status.
|
|
130
|
+
|
|
131
|
+
Unknown event types should be ignored rather than treated as errors, so a future addition never breaks a client.
|
|
132
|
+
|
|
133
|
+
The `id:` sequence is a global monotonic counter: ids keep climbing across runs, so they are NOT per-run ordinals. The stream is already scoped to one run (`for_run`), so use the `id:` only for `Last-Event-ID` replay, not to infer how many events a run produced.
|
|
134
|
+
|
|
135
|
+
API runs are **non-streaming**: the full answer arrives in the final `run.completed` frame. No `message.delta` is ever emitted — the agent loop disables token streaming for every API run (the always-registered `question` tool makes each run an interactive turn) — and there is no `reasoning.delta` event anywhere in the system. Do not wait for incremental text deltas.
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Approvals & clarifications
|
|
140
|
+
|
|
141
|
+
Both endpoints unblock an in-process gate registered when the run started. Decisions sent to a run with no live gate return 409.
|
|
142
|
+
|
|
143
|
+
### `POST /v1/runs/:run_id/approvals/:approval_id` → 200 | 404 | 409 | 422
|
|
144
|
+
```json
|
|
145
|
+
// req
|
|
146
|
+
{ "decision": "once|session|always|always_prefix|always_command|deny|deny_always" }
|
|
147
|
+
// res
|
|
148
|
+
{ "approval_id": "string", "decision": "string" }
|
|
149
|
+
```
|
|
150
|
+
The enum is closed — anything else fails validation with 422. The approve values are kept in sync with `UI::API::APPROVE_DECISIONS`, plus the two explicit deny forms:
|
|
151
|
+
|
|
152
|
+
| decision | effect |
|
|
153
|
+
|---|---|
|
|
154
|
+
| `once` | approve this call only |
|
|
155
|
+
| `session` | approve this scope for the rest of the session (in-memory) |
|
|
156
|
+
| `always_prefix` | approve + persist an allow rule for the command's prefix (offered only when a prefix rule is derivable) |
|
|
157
|
+
| `always_command` | approve + persist an allow rule for this exact command |
|
|
158
|
+
| `always` | back-compat alias for `always_command` (older clients post it) |
|
|
159
|
+
| `deny` | deny this call once — nothing persisted, re-prompts next time |
|
|
160
|
+
| `deny_always` | deny + persist a `permissions:deny` rule, auto-denied across sessions |
|
|
161
|
+
|
|
162
|
+
The `approval.required` SSE frame carries a `choices` array listing the decisions offered for THAT request (e.g. `session` only when session caching applies, `always_prefix` only with a derivable prefix); clients should render `choices` rather than hardcoding the enum.
|
|
163
|
+
|
|
164
|
+
### `POST /v1/runs/:run_id/clarifications/:clarify_id` → 200 | 404 | 409 | 422
|
|
165
|
+
```json
|
|
166
|
+
// req
|
|
167
|
+
{ "response": "string" }
|
|
168
|
+
// res
|
|
169
|
+
{ "clarify_id": "string", "accepted": true }
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Skills
|
|
175
|
+
|
|
176
|
+
### `GET /v1/skills` → 200
|
|
177
|
+
```json
|
|
178
|
+
[{ "name": "string", "description": "string", "enabled": true }]
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### `PUT /v1/skills/:name` → 200 | 404 | 422
|
|
182
|
+
```json
|
|
183
|
+
// req
|
|
184
|
+
{ "enabled": true }
|
|
185
|
+
// res
|
|
186
|
+
{ "name": "string", "enabled": true }
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Models
|
|
192
|
+
|
|
193
|
+
### `GET /v1/models` → 200
|
|
194
|
+
```json
|
|
195
|
+
[{ "id": "provider/model", "provider": "string|null", "context_window": 128000 }]
|
|
196
|
+
```
|
|
197
|
+
Source defaults to `RubyLLM.models.all`; some fields may be `null` for models that don't expose them.
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Mode
|
|
202
|
+
|
|
203
|
+
Process-level toggle that gates tool availability + approval policy. Lives in the Ruby heap (see `Rubino::Modes`); a restart resets to `default`. Valid values: `"default"`, `"plan"`, `"yolo"`.
|
|
204
|
+
|
|
205
|
+
| mode | effect |
|
|
206
|
+
|---|---|
|
|
207
|
+
| `default` | all tools registered, approval rules from config |
|
|
208
|
+
| `plan` | read-only tools only (no edits/shell/git) |
|
|
209
|
+
| `yolo` | all tools, approval policy bypassed |
|
|
210
|
+
|
|
211
|
+
### `GET /v1/mode` → 200
|
|
212
|
+
```json
|
|
213
|
+
{
|
|
214
|
+
"mode": "default",
|
|
215
|
+
"description": "all tools, approvals from config",
|
|
216
|
+
"available": [
|
|
217
|
+
{ "mode": "default", "description": "..." },
|
|
218
|
+
{ "mode": "plan", "description": "..." },
|
|
219
|
+
{ "mode": "yolo", "description": "..." }
|
|
220
|
+
]
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
The minimum guaranteed shape is `{ "mode": "default"|"plan"|"yolo" }`; `description` and `available` are exposed so clients can render a picker without hardcoding the catalogue.
|
|
224
|
+
|
|
225
|
+
### `PUT /v1/mode` → 200 | 422
|
|
226
|
+
```json
|
|
227
|
+
// req
|
|
228
|
+
{ "mode": "default"|"plan"|"yolo" }
|
|
229
|
+
// res
|
|
230
|
+
{ "mode": "plan", "previous": "default", "description": "..." }
|
|
231
|
+
```
|
|
232
|
+
Emits the same `mode_changed` UI event the CLI fires on `/mode plan`, so any in-flight SSE stream notices. An invalid `mode` (missing, wrong type, or not in the enum) returns the canonical error envelope:
|
|
233
|
+
```json
|
|
234
|
+
{ "error": { "code": "validation", "message": "invalid request body", "details": { "errors": { "mode": ["..."] } } } }
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## Files
|
|
240
|
+
|
|
241
|
+
Workspace is sandboxed under `config.paths.home`. Path traversal raises `Workspace::PathTraversal < ValidationError` → 422.
|
|
242
|
+
|
|
243
|
+
### `GET /v1/files?path=relative/path` → 200 | 404 | 422
|
|
244
|
+
Raw bytes as `application/octet-stream`. 422 when `path` query is missing/empty or escapes the sandbox. 404 when no file is at that path.
|
|
245
|
+
|
|
246
|
+
### `POST /v1/files` → 201 | 422
|
|
247
|
+
Multipart `multipart/form-data` with a `file` part. 422 when the content-type is not multipart, or the `file` field is missing.
|
|
248
|
+
```json
|
|
249
|
+
{ "id": "uuid", "filename": "string", "size": 12345 }
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## Cron jobs
|
|
255
|
+
|
|
256
|
+
In-process `rufus-scheduler` singleton. The scheduler does not survive multi-process scale-out (every worker would run every tick).
|
|
257
|
+
|
|
258
|
+
### `POST /v1/jobs` → 201 | 422
|
|
259
|
+
```json
|
|
260
|
+
// req
|
|
261
|
+
{
|
|
262
|
+
"name": "string", // required
|
|
263
|
+
"schedule": "cron string", // required
|
|
264
|
+
"prompt": "string", // required
|
|
265
|
+
"skills": ["string"]?,
|
|
266
|
+
"model": "string|null",
|
|
267
|
+
"provider": "string|null",
|
|
268
|
+
"deliver": "local|webhook" // optional, default "local"
|
|
269
|
+
}
|
|
270
|
+
// res — full serialized job
|
|
271
|
+
{ "id": "uuid", "name": "string", "schedule": "string", "prompt": "string",
|
|
272
|
+
"skills": [], "model": null, "provider": null, "deliver": "local",
|
|
273
|
+
"enabled": true, "last_run_at": null, "last_run_id": null,
|
|
274
|
+
"created_at": "ts", "updated_at": "ts" }
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### `GET /v1/jobs?include_disabled=true|false` → 200
|
|
278
|
+
Array of serialized jobs. `include_disabled=false` filters out paused.
|
|
279
|
+
|
|
280
|
+
### `GET /v1/jobs/:id` → 200 | 404
|
|
281
|
+
### `PATCH /v1/jobs/:id` → 200 | 404 | 422
|
|
282
|
+
Partial update. Same fields as create plus `enabled`. `skills` is an Array (returns as `skills`, never `skills_json` — the JSON column is internal).
|
|
283
|
+
|
|
284
|
+
### `DELETE /v1/jobs/:id` → 204 | 404
|
|
285
|
+
Removes the row AND unschedules the rufus handle.
|
|
286
|
+
|
|
287
|
+
### `POST /v1/jobs/:id/pause` → 200
|
|
288
|
+
Disables + unschedules. Returns the serialized job.
|
|
289
|
+
|
|
290
|
+
### `POST /v1/jobs/:id/resume` → 200
|
|
291
|
+
Enables + reschedules.
|
|
292
|
+
|
|
293
|
+
### `POST /v1/jobs/:id/trigger` → 202 | 404
|
|
294
|
+
Fires one run immediately, returns a run reference.
|
|
295
|
+
```json
|
|
296
|
+
{ "job_id": "uuid", "run_id": "uuid", "session_id": "uuid" }
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Webhook delivery: when `deliver: "webhook"`, on run completion rubino POSTs to `RUBINO_WEBHOOK_URL` with `{ job_id, run_id, status, session_id }`.
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## Memory
|
|
304
|
+
|
|
305
|
+
The persistent memory store (same backend the CLI's `rubino memory` uses — see [`docs/memory.md`](../memory.md)).
|
|
306
|
+
|
|
307
|
+
### `GET /v1/memory` → 200
|
|
308
|
+
Query: `limit` (default 50, max 200), `offset`, `q` (substring filter on content).
|
|
309
|
+
```json
|
|
310
|
+
{
|
|
311
|
+
"memory": [
|
|
312
|
+
{ "id": "uuid", "kind": "string", "content": "string",
|
|
313
|
+
"created_at": "ts", "updated_at": "ts" }
|
|
314
|
+
]
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### `GET /v1/memory/stats` → 200
|
|
319
|
+
```json
|
|
320
|
+
{ "backend": "sqlite", "count": 42 }
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### `DELETE /v1/memory/:id` → 204 | 404
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
## Tasks
|
|
328
|
+
|
|
329
|
+
Background subagent runs spawned by the `task` tool (in-process registry; entries do not survive a restart).
|
|
330
|
+
|
|
331
|
+
### `GET /v1/tasks` → 200
|
|
332
|
+
```json
|
|
333
|
+
{
|
|
334
|
+
"tasks": [
|
|
335
|
+
{ "id": "string", "subagent": "string", "prompt": "string",
|
|
336
|
+
"status": "running|completed|failed|cancelled",
|
|
337
|
+
"started_at": "ts|null", "elapsed_seconds": 1.234,
|
|
338
|
+
"result_summary": "string|null" }
|
|
339
|
+
]
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### `GET /v1/tasks/:id` → 200 | 404
|
|
344
|
+
The summary shape above plus `finished_at`, the full `result`, and `error`.
|
|
345
|
+
|
|
346
|
+
### `POST /v1/tasks/:id/stop` → 202 | 404 | 409
|
|
347
|
+
Cooperative cancel of a running task (descendant ask-gates are cancelled too). Returns the task detail; 409 when the task already finished.
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
## OAuth
|
|
352
|
+
|
|
353
|
+
See [`docs/oauth-providers.md`](../oauth-providers.md) for the full PKCE flow, encryption key requirements, and per-provider setup. The HTTP surface:
|
|
354
|
+
|
|
355
|
+
### `GET /v1/oauth/providers` → 200
|
|
356
|
+
Lists providers registered at boot via `OAuth::Registry.load_from_config!`. Empty when no `oauth.providers.*` section in config carries both `client_id` and `client_secret`.
|
|
357
|
+
```json
|
|
358
|
+
[{ "id": "github", "display_name": "Github", "scopes": ["repo", "user:email"] }]
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
### `POST /v1/oauth/providers/:id/connect` → 200 | 404 | 422
|
|
362
|
+
Builds a PKCE authorize request. The client MUST persist `state` and `code_verifier` between connect and callback — rubino is stateless on the OAuth flow.
|
|
363
|
+
```json
|
|
364
|
+
// req
|
|
365
|
+
{ "redirect_uri": "https://your-client/oauth/callback", "scopes": ["string"]? }
|
|
366
|
+
// res
|
|
367
|
+
{
|
|
368
|
+
"authorize_url": "https://provider.example/authorize?...",
|
|
369
|
+
"state": "string",
|
|
370
|
+
"code_verifier": "string",
|
|
371
|
+
"provider": "github"
|
|
372
|
+
}
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### `POST /v1/oauth/providers/:id/callback` → 201 | 404 | 422 | 502
|
|
376
|
+
The client posts the code it received at `redirect_uri`, plus the `state`/`code_verifier` it kept from connect, plus the `expected_state` it stored (constant-time compared against `state`).
|
|
377
|
+
```json
|
|
378
|
+
// req — every field required
|
|
379
|
+
{
|
|
380
|
+
"code": "string",
|
|
381
|
+
"state": "string",
|
|
382
|
+
"expected_state": "string",
|
|
383
|
+
"code_verifier": "string",
|
|
384
|
+
"redirect_uri": "string"
|
|
385
|
+
}
|
|
386
|
+
// res 201 — serialized connection, tokens stripped
|
|
387
|
+
{
|
|
388
|
+
"id": "uuid",
|
|
389
|
+
"provider": "github",
|
|
390
|
+
"account_id": "string",
|
|
391
|
+
"account_email": "string|null",
|
|
392
|
+
"expires_at": "ts|null",
|
|
393
|
+
"scopes": ["string"],
|
|
394
|
+
"metadata": { },
|
|
395
|
+
"created_at": "ts",
|
|
396
|
+
"updated_at": "ts"
|
|
397
|
+
}
|
|
398
|
+
```
|
|
399
|
+
422 when `state != expected_state`. 502 when token exchange against the provider raises (counted under `oauth_token_exchanges_total{outcome="error"}`).
|
|
400
|
+
|
|
401
|
+
### `GET /v1/oauth/connections` → 200
|
|
402
|
+
Array of serialized connections. Tokens are NEVER returned over the wire.
|
|
403
|
+
|
|
404
|
+
### `DELETE /v1/oauth/connections/:id` → 204 | 404
|
|
405
|
+
Removes the row (encrypted tokens included). Does not attempt provider-side revocation.
|
|
406
|
+
|
|
407
|
+
---
|
|
408
|
+
|
|
409
|
+
## Operational notes
|
|
410
|
+
|
|
411
|
+
- **Auth bypass** — only `/v1/health` and `/v1/metrics` (see `Auth::SKIP_PATHS`). Every other route requires a valid bearer.
|
|
412
|
+
- **Token redaction** — the structured logger masks `access_token`, `refresh_token`, `id_token`, `client_secret`, `api_key`, `password`, `secret`, `bearer`, `authorization`, `http_authorization` (case-insensitive, recursive) before serialization.
|
|
413
|
+
- **Single process** — the scheduler, gate registry, and event polling all live in the Ruby heap. Scaling to multiple workers would require pulling these out (Redis, Postgres LISTEN, etc.).
|
|
414
|
+
- **Encryption key** — `RUBINO_ENCRYPTION_KEY` (32-byte base64) is required to use any OAuth route. Boot fails fast without it.
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
rubino is a lightweight agent that runs on a PC or inside a VM. It follows a
|
|
6
|
+
layered architecture with strict separation of concerns:
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
Presentation Layer → CLI, JSON API Server
|
|
10
|
+
Orchestration Layer → Agent Router, Interaction Lifecycle
|
|
11
|
+
Core Layer → Agent Loop, Context, Memory, Jobs, Tools
|
|
12
|
+
Infrastructure Layer → LLM Adapter, Database, MCP, OAuth
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Key Design Principles
|
|
16
|
+
|
|
17
|
+
1. **All output goes through UI** — No `puts`/`print` in core modules
|
|
18
|
+
2. **LLM is isolated** — Only `LLM::RubyLLMAdapter` talks to ruby_llm
|
|
19
|
+
3. **SQLite is the single database** — Sessions, memory, jobs, events
|
|
20
|
+
4. **Event-driven** — Core emits events, UI/plugins subscribe
|
|
21
|
+
5. **Plugin hooks** — 38 declared extension points for customization (design surface; few are wired today)
|
|
22
|
+
6. **Config is not architecture** — Configuration describes what; architecture decides how
|
|
23
|
+
|
|
24
|
+
## Module Map
|
|
25
|
+
|
|
26
|
+
### `agent/`
|
|
27
|
+
Multiple agent types and @mention routing exist as a design surface; the
|
|
28
|
+
rubino runs a single agent by default and multi-agent routing is dormant.
|
|
29
|
+
- `AgentRegistry` — Defines all agent types (build, plan, explore, general, utility)
|
|
30
|
+
- `Router` — Routes input to appropriate agent via @mention
|
|
31
|
+
- `Definition` — Agent type with model, tools, permissions, MCP scoping
|
|
32
|
+
- `Runner` — Top-level orchestrator for a user interaction
|
|
33
|
+
- `Loop` — Core LLM call + tool execution cycle
|
|
34
|
+
- `IterationBudget` — Prevents runaway loops
|
|
35
|
+
- `ToolExecutor` — Executes tools with approval and result formatting
|
|
36
|
+
|
|
37
|
+
### `interaction/`
|
|
38
|
+
- `Lifecycle` — Full turn lifecycle: input → memory → context → model → tools → persist → jobs
|
|
39
|
+
- `State` — State machine (idle → calling_model → executing_tools → finished)
|
|
40
|
+
- `EventBus` — Pub/sub for decoupling core from UI
|
|
41
|
+
- `Events` — All typed event constants
|
|
42
|
+
|
|
43
|
+
### `context/`
|
|
44
|
+
- `PromptAssembler` — Builds the full prompt from all sources
|
|
45
|
+
- `TokenBudget` — Calculates token usage and decides when to compact (`needs_compaction?`)
|
|
46
|
+
- `Compressor` — Orchestrates compaction (flush memory → split → summarize → lineage)
|
|
47
|
+
- `MessageBoundary` — Splits messages into head/middle/tail
|
|
48
|
+
- `SummaryBuilder` — Generates structured summaries via LLM
|
|
49
|
+
- `ToolPairSanitizer` — Keeps tool_call/result pairs intact
|
|
50
|
+
- `FileDiscovery` — Finds project context files (.rubino.md, AGENTS.md, etc.)
|
|
51
|
+
|
|
52
|
+
### `memory/`
|
|
53
|
+
- `Store` — CRUD for memories (7 kinds: user_profile, preference, fact, etc.)
|
|
54
|
+
- `Retriever` — Loads relevant memories for prompt inclusion
|
|
55
|
+
- `Extractor` — Pattern-based extraction from conversations
|
|
56
|
+
- `Deduplicator` — Jaccard similarity deduplication
|
|
57
|
+
- `Flusher` — Pre-compaction memory flush
|
|
58
|
+
|
|
59
|
+
### `session/`
|
|
60
|
+
- `Repository` — Session CRUD with prefix-matching find
|
|
61
|
+
- `Store` — Message persistence
|
|
62
|
+
- `Message` — Value object with to_context / to_row
|
|
63
|
+
|
|
64
|
+
Forking is not a dedicated class: a new session inherits history via the
|
|
65
|
+
API's `parent_session_id` path.
|
|
66
|
+
|
|
67
|
+
### `jobs/`
|
|
68
|
+
- `Queue` — SQLite-backed job queue with priority and scheduling
|
|
69
|
+
- `Runner` — Executes jobs, records runs
|
|
70
|
+
- `Worker` — Polling loop for background processing
|
|
71
|
+
- `Registry` — Maps job types to handler classes
|
|
72
|
+
- Handlers: ExtractMemory, SummarizeSession, CompactSession, CleanupSessions, DistillSkill
|
|
73
|
+
|
|
74
|
+
### `tools/`
|
|
75
|
+
- `Base` — Abstract tool interface (name, description, input_schema, risk_level, call)
|
|
76
|
+
- `Registry` — Singleton registry with enable/disable
|
|
77
|
+
- `Result` — Structured result (success/error/denied)
|
|
78
|
+
- The built-in tools (authoritative, drift-checked count and list in [tools.md](tools.md)) + custom tool loader + formatter integration
|
|
79
|
+
- `CustomToolLoader` — DSL for user-defined tools
|
|
80
|
+
|
|
81
|
+
### `llm/`
|
|
82
|
+
- `RubyLLMAdapter` — Wraps ruby_llm (chat, stream, structured output)
|
|
83
|
+
- `ProviderResolver` — Auto-detects provider from model name
|
|
84
|
+
- `ModelRegistry` — Known models with context windows
|
|
85
|
+
- `ContentBuilder` — Multipart content for vision (text + images)
|
|
86
|
+
|
|
87
|
+
### `mcp/`
|
|
88
|
+
Experimental — booted at chat startup when `mcp.servers` is configured
|
|
89
|
+
(see [mcp.md](mcp.md)).
|
|
90
|
+
- `Manager` — Manages multiple MCP client connections
|
|
91
|
+
- `MCPToolWrapper` — Wraps MCP tools into Tools::Base interface
|
|
92
|
+
|
|
93
|
+
### `security/`
|
|
94
|
+
- `ApprovalPolicy` — Decides allow/ask/deny per tool call
|
|
95
|
+
- `PatternMatcher` — Wildcard pattern matching for permissions
|
|
96
|
+
- `DoomLoopDetector` — Detects repeated identical tool calls
|
|
97
|
+
- `CommandAllowlist` — Pre-approved shell commands
|
|
98
|
+
|
|
99
|
+
### `plugins/`
|
|
100
|
+
- `Registry` — Central hook registry; the hook set (38 points) is declared in
|
|
101
|
+
`plugins.rb` as a design surface, with few hooks wired today
|
|
102
|
+
- Loaded from `.rubino/plugins/`
|
|
103
|
+
|
|
104
|
+
### `skills/`
|
|
105
|
+
- `Skill` — Parsed SKILL.md with YAML frontmatter
|
|
106
|
+
- `Registry` — Discovery from configured paths
|
|
107
|
+
- `SkillTool` — Tool for on-demand skill loading
|
|
108
|
+
|
|
109
|
+
### `commands/`
|
|
110
|
+
- `Command` — Parsed command.md with template rendering
|
|
111
|
+
- `Loader` — Discovery from configured paths
|
|
112
|
+
- `Executor` — Handles slash commands and built-ins
|
|
113
|
+
|
|
114
|
+
### `api/`
|
|
115
|
+
- `Server` — Rack + Puma boot
|
|
116
|
+
- `Router` — pattern-based dispatcher
|
|
117
|
+
- `Middleware::{Auth,ErrorHandler,JsonParser}` — Bearer auth, typed-error mapping, JSON body parsing
|
|
118
|
+
- `Operations::*` — request handlers (sessions, runs, approvals, clarifications, skills, models, files, cron jobs, oauth)
|
|
119
|
+
|
|
120
|
+
### `oauth/`
|
|
121
|
+
- `Provider` (+ `Github`, `Google`) — provider abstraction with PKCE auth flow
|
|
122
|
+
- `Registry` — process-wide registry hydrated from config
|
|
123
|
+
- `ConnectionRepository` — encrypted token persistence (AES-256-GCM via `TokenEncryptor`)
|
|
124
|
+
|
|
125
|
+
### `config/`
|
|
126
|
+
- `Loader` — Basic YAML loader
|
|
127
|
+
- `EnhancedLoader` — Multi-layer precedence with substitutions
|
|
128
|
+
- `RemoteConfig` — Enterprise remote config fetching
|
|
129
|
+
- `Configuration` — Typed accessors for all config sections
|
|
130
|
+
- `Writer` — Persists config changes
|
|
131
|
+
- `Defaults` — All default values
|
|
132
|
+
|
|
133
|
+
### `database/`
|
|
134
|
+
- `Connection` — SQLite + WAL mode via Sequel
|
|
135
|
+
- `Migrator` — Versioned migrations
|
|
136
|
+
|
|
137
|
+
### `ui/`
|
|
138
|
+
- `Base` — Abstract interface (info, error, stream, table, ask, confirm, etc.)
|
|
139
|
+
- `CLI` — TTY-based terminal output
|
|
140
|
+
- `Null` — Silent adapter for testing
|
|
141
|
+
- `API` — Structured event collector
|
|
142
|
+
|
|
143
|
+
## Data Flow
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
User Input
|
|
147
|
+
│
|
|
148
|
+
├─→ Commands::Executor (if /command)
|
|
149
|
+
│ └─→ Render template → feed to agent
|
|
150
|
+
│
|
|
151
|
+
├─→ Agent::Router (if @mention)
|
|
152
|
+
│ └─→ Select agent definition
|
|
153
|
+
│
|
|
154
|
+
└─→ Interaction::Lifecycle
|
|
155
|
+
│
|
|
156
|
+
├─ Persist user message
|
|
157
|
+
├─ Load memory (Retriever)
|
|
158
|
+
├─ Extract images (ContentBuilder)
|
|
159
|
+
├─ Build context (PromptAssembler)
|
|
160
|
+
├─ Check token budget (TokenBudget)
|
|
161
|
+
├─ Compact if needed (Compressor)
|
|
162
|
+
│
|
|
163
|
+
├─ Agent::Loop
|
|
164
|
+
│ ├─ Call LLM (RubyLLMAdapter)
|
|
165
|
+
│ ├─ Stream to UI
|
|
166
|
+
│ ├─ If tool_calls:
|
|
167
|
+
│ │ ├─ Check permissions (ApprovalPolicy)
|
|
168
|
+
│ │ ├─ Check doom loop (DoomLoopDetector)
|
|
169
|
+
│ │ ├─ Execute tool (ToolExecutor)
|
|
170
|
+
│ │ ├─ Run plugin hooks
|
|
171
|
+
│ │ └─ Loop back to LLM
|
|
172
|
+
│ └─ Final text response
|
|
173
|
+
│
|
|
174
|
+
├─ Persist session
|
|
175
|
+
├─ Enqueue jobs (extract memory, summarize)
|
|
176
|
+
└─ Emit events → UI + SSE clients
|
|
177
|
+
```
|