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,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
module Rubino
|
|
7
|
+
module Tools
|
|
8
|
+
# Tool for fetching web page content and converting to text/markdown.
|
|
9
|
+
class WebFetchTool < Base
|
|
10
|
+
MAX_BODY_SIZE = 100_000
|
|
11
|
+
TIMEOUT = 30
|
|
12
|
+
|
|
13
|
+
def name
|
|
14
|
+
"webfetch"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Gated by `tools.web` (shared with websearch), not `tools.webfetch`.
|
|
18
|
+
def config_key
|
|
19
|
+
"web"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def description
|
|
23
|
+
"Fetch content from a URL and return it as text. " \
|
|
24
|
+
"Useful for reading documentation, API references, and web pages."
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def input_schema
|
|
28
|
+
{
|
|
29
|
+
type: "object",
|
|
30
|
+
properties: {
|
|
31
|
+
url: {
|
|
32
|
+
type: "string",
|
|
33
|
+
description: "The URL to fetch content from"
|
|
34
|
+
},
|
|
35
|
+
format: {
|
|
36
|
+
type: "string",
|
|
37
|
+
enum: %w[text html],
|
|
38
|
+
description: "Output format: 'text' (default, strips HTML) or 'html' (raw)"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
required: %w[url]
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def risk_level
|
|
46
|
+
:low
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def call(arguments)
|
|
50
|
+
url = arguments["url"] || arguments[:url]
|
|
51
|
+
format = arguments["format"] || arguments[:format] || "text"
|
|
52
|
+
|
|
53
|
+
fetch_url(url, format: format)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def fetch_url(url, format:, redirects: 5)
|
|
59
|
+
return "Error: Too many redirects" if redirects <= 0
|
|
60
|
+
|
|
61
|
+
uri = URI.parse(url)
|
|
62
|
+
uri = URI.parse("https://#{url}") unless uri.scheme
|
|
63
|
+
|
|
64
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
65
|
+
http.use_ssl = (uri.scheme == "https")
|
|
66
|
+
http.open_timeout = TIMEOUT
|
|
67
|
+
http.read_timeout = TIMEOUT
|
|
68
|
+
|
|
69
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
|
70
|
+
request["User-Agent"] = "Rubino/#{Rubino::VERSION}"
|
|
71
|
+
request["Accept"] = "text/html,text/plain,application/json"
|
|
72
|
+
|
|
73
|
+
response = http.request(request)
|
|
74
|
+
|
|
75
|
+
case response
|
|
76
|
+
when Net::HTTPRedirection
|
|
77
|
+
fetch_url(response["location"], format: format, redirects: redirects - 1)
|
|
78
|
+
when Net::HTTPSuccess
|
|
79
|
+
content_type = response["content-type"].to_s
|
|
80
|
+
return binary_refusal(url, content_type) if binary_content_type?(content_type)
|
|
81
|
+
|
|
82
|
+
# Force UTF-8 + scrub so gsub! in strip_html doesn't trip
|
|
83
|
+
# "source sequence is illegal/malformed utf-8" when the upstream
|
|
84
|
+
# response is labelled text/* but contains stray non-UTF-8 bytes
|
|
85
|
+
# (which is the common case for misencoded HTML / CRLF logs).
|
|
86
|
+
body = response.body.to_s.dup.force_encoding("UTF-8").scrub("?")
|
|
87
|
+
if body.bytesize > MAX_BODY_SIZE
|
|
88
|
+
body = body.byteslice(0,
|
|
89
|
+
MAX_BODY_SIZE).to_s.force_encoding("UTF-8").scrub("?")
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
if format == "html"
|
|
93
|
+
body
|
|
94
|
+
else
|
|
95
|
+
strip_html(body)
|
|
96
|
+
end
|
|
97
|
+
else
|
|
98
|
+
"Error: HTTP #{response.code} - #{response.message}"
|
|
99
|
+
end
|
|
100
|
+
rescue StandardError => e
|
|
101
|
+
"Error fetching URL: #{e.message}"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
BINARY_TYPE_PATTERNS = [
|
|
105
|
+
%r{\Aapplication/(pdf|zip|x-tar|x-gzip|x-bzip2|x-7z-compressed|x-rar|octet-stream|x-msdownload|vnd\.openxmlformats|vnd\.ms-)},
|
|
106
|
+
%r{\Aimage/}, %r{\Aaudio/}, %r{\Avideo/},
|
|
107
|
+
%r{\Afont/}
|
|
108
|
+
].freeze
|
|
109
|
+
|
|
110
|
+
def binary_content_type?(content_type)
|
|
111
|
+
type = content_type.to_s.split(";").first.to_s.strip.downcase
|
|
112
|
+
BINARY_TYPE_PATTERNS.any? { |re| type.match?(re) }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def binary_refusal(url, content_type)
|
|
116
|
+
"Error: refusing to fetch binary content as text " \
|
|
117
|
+
"(URL=#{url}, Content-Type=#{content_type.split(";").first.to_s.strip}). " \
|
|
118
|
+
"Use a dedicated tool (e.g. read_file after downloading, attach_file, " \
|
|
119
|
+
"or an image-aware model) for binary assets."
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def strip_html(html)
|
|
123
|
+
# Basic HTML to text conversion
|
|
124
|
+
text = html.dup
|
|
125
|
+
|
|
126
|
+
# Remove script and style blocks
|
|
127
|
+
text.gsub!(%r{<script[^>]*>.*?</script>}mi, "")
|
|
128
|
+
text.gsub!(%r{<style[^>]*>.*?</style>}mi, "")
|
|
129
|
+
|
|
130
|
+
# Convert common elements
|
|
131
|
+
text.gsub!(%r{<br\s*/?>}i, "\n")
|
|
132
|
+
text.gsub!(%r{</(p|div|h[1-6]|li|tr)>}i, "\n")
|
|
133
|
+
text.gsub!(/<(h[1-6])[^>]*>/i, "\n## ")
|
|
134
|
+
text.gsub!(/<li[^>]*>/i, "- ")
|
|
135
|
+
|
|
136
|
+
# Remove remaining tags
|
|
137
|
+
text.gsub!(/<[^>]+>/, "")
|
|
138
|
+
|
|
139
|
+
# Decode common entities
|
|
140
|
+
text.gsub!("&", "&")
|
|
141
|
+
text.gsub!("<", "<")
|
|
142
|
+
text.gsub!(">", ">")
|
|
143
|
+
text.gsub!(""", '"')
|
|
144
|
+
text.gsub!("'", "'")
|
|
145
|
+
text.gsub!(" ", " ")
|
|
146
|
+
|
|
147
|
+
# Clean up whitespace
|
|
148
|
+
text.gsub!(/\n{3,}/, "\n\n")
|
|
149
|
+
text.strip
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module Rubino
|
|
8
|
+
module Tools
|
|
9
|
+
# Tool for performing web searches via external search APIs.
|
|
10
|
+
# Supports Tavily, SearXNG, and a fallback DuckDuckGo scraper.
|
|
11
|
+
class WebSearchTool < Base
|
|
12
|
+
def name
|
|
13
|
+
"websearch"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Gated by `tools.web` (shared with webfetch), not `tools.websearch`.
|
|
17
|
+
def config_key
|
|
18
|
+
"web"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def description
|
|
22
|
+
"Search the web for information. Returns relevant results with titles, " \
|
|
23
|
+
"URLs, and snippets. Useful for finding documentation, researching " \
|
|
24
|
+
"dependencies, and answering questions about external topics."
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def input_schema
|
|
28
|
+
{
|
|
29
|
+
type: "object",
|
|
30
|
+
properties: {
|
|
31
|
+
query: {
|
|
32
|
+
type: "string",
|
|
33
|
+
description: "The search query"
|
|
34
|
+
},
|
|
35
|
+
max_results: {
|
|
36
|
+
type: "integer",
|
|
37
|
+
description: "Maximum number of results (default: 5)"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
required: %w[query]
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def risk_level
|
|
45
|
+
:low
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def call(arguments)
|
|
49
|
+
query = arguments["query"] || arguments[:query]
|
|
50
|
+
max_results = arguments["max_results"] || arguments[:max_results] || 5
|
|
51
|
+
|
|
52
|
+
if ENV["TAVILY_API_KEY"]
|
|
53
|
+
search_tavily(query, max_results)
|
|
54
|
+
elsif ENV["SEARXNG_URL"]
|
|
55
|
+
search_searxng(query, max_results)
|
|
56
|
+
else
|
|
57
|
+
search_ddg(query, max_results)
|
|
58
|
+
end
|
|
59
|
+
rescue StandardError => e
|
|
60
|
+
"Search error: #{e.message}"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
# Tavily API (preferred - high quality results)
|
|
66
|
+
def search_tavily(query, max_results)
|
|
67
|
+
uri = URI("https://api.tavily.com/search")
|
|
68
|
+
body = {
|
|
69
|
+
api_key: ENV.fetch("TAVILY_API_KEY", nil),
|
|
70
|
+
query: query,
|
|
71
|
+
max_results: max_results,
|
|
72
|
+
include_answer: true,
|
|
73
|
+
search_depth: "basic"
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
response = post_json(uri, body)
|
|
77
|
+
data = JSON.parse(response)
|
|
78
|
+
|
|
79
|
+
results = []
|
|
80
|
+
results << "**Answer:** #{data["answer"]}\n" if data["answer"]
|
|
81
|
+
|
|
82
|
+
(data["results"] || []).each do |r|
|
|
83
|
+
results << format_result(r["title"], r["url"], r["content"])
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
results.empty? ? "No results found for: #{query}" : results.join("\n\n")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# SearXNG (self-hosted, privacy-focused)
|
|
90
|
+
def search_searxng(query, max_results)
|
|
91
|
+
base_url = ENV["SEARXNG_URL"].chomp("/")
|
|
92
|
+
uri = URI("#{base_url}/search")
|
|
93
|
+
uri.query = URI.encode_www_form(
|
|
94
|
+
q: query,
|
|
95
|
+
format: "json",
|
|
96
|
+
pageno: 1
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
response = get_json(uri)
|
|
100
|
+
data = JSON.parse(response)
|
|
101
|
+
|
|
102
|
+
results = (data["results"] || []).first(max_results).map do |r|
|
|
103
|
+
format_result(r["title"], r["url"], r["content"])
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
results.empty? ? "No results found for: #{query}" : results.join("\n\n")
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# DuckDuckGo HTML scraper (fallback, no API key needed)
|
|
110
|
+
def search_ddg(query, max_results)
|
|
111
|
+
uri = URI("https://html.duckduckgo.com/html/")
|
|
112
|
+
body = URI.encode_www_form(q: query)
|
|
113
|
+
|
|
114
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
115
|
+
http.use_ssl = true
|
|
116
|
+
request = Net::HTTP::Post.new(uri.path)
|
|
117
|
+
request["User-Agent"] = "Rubino/#{Rubino::VERSION}"
|
|
118
|
+
request.body = body
|
|
119
|
+
|
|
120
|
+
response = http.request(request)
|
|
121
|
+
parse_ddg_html(response.body, max_results)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def parse_ddg_html(html, max_results)
|
|
125
|
+
results = []
|
|
126
|
+
|
|
127
|
+
# Extract result blocks
|
|
128
|
+
html.scan(%r{<a rel="nofollow" class="result__a" href="([^"]+)"[^>]*>(.+?)</a>.*?<a class="result__snippet"[^>]*>(.+?)</a>}m) do |url, title, snippet|
|
|
129
|
+
clean_title = title.gsub(/<[^>]+>/, "").strip
|
|
130
|
+
clean_snippet = snippet.gsub(/<[^>]+>/, "").strip
|
|
131
|
+
clean_url = url.strip
|
|
132
|
+
|
|
133
|
+
# DuckDuckGo wraps URLs in redirects
|
|
134
|
+
if clean_url.include?("uddg=")
|
|
135
|
+
clean_url = begin
|
|
136
|
+
URI.decode_www_form_component(clean_url.match(/uddg=([^&]+)/)[1])
|
|
137
|
+
rescue StandardError
|
|
138
|
+
clean_url
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
results << format_result(clean_title, clean_url, clean_snippet)
|
|
143
|
+
break if results.size >= max_results
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
results.empty? ? "No results found (DDG fallback)" : results.join("\n\n")
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def format_result(title, url, snippet)
|
|
150
|
+
"**#{title}**\n#{url}\n#{snippet}"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def post_json(uri, body)
|
|
154
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
155
|
+
http.use_ssl = (uri.scheme == "https")
|
|
156
|
+
http.open_timeout = 10
|
|
157
|
+
http.read_timeout = 15
|
|
158
|
+
|
|
159
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
|
160
|
+
request["Content-Type"] = "application/json"
|
|
161
|
+
request.body = JSON.generate(body)
|
|
162
|
+
|
|
163
|
+
http.request(request).body
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def get_json(uri)
|
|
167
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
168
|
+
http.use_ssl = (uri.scheme == "https")
|
|
169
|
+
http.open_timeout = 10
|
|
170
|
+
http.read_timeout = 15
|
|
171
|
+
|
|
172
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
|
173
|
+
request["Accept"] = "application/json"
|
|
174
|
+
|
|
175
|
+
http.request(request).body
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Rubino
|
|
6
|
+
module Tools
|
|
7
|
+
# Writes content to a file, creating parent directories if needed.
|
|
8
|
+
# Overwrites existing files (the LLM is expected to Read first when in
|
|
9
|
+
# doubt). Kept intentionally narrow — no append mode, no partial writes;
|
|
10
|
+
# those belong in `edit` / `multi_edit`.
|
|
11
|
+
class WriteTool < Base
|
|
12
|
+
def name
|
|
13
|
+
"write"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def description
|
|
17
|
+
"Write content to a file, overwriting any existing content. " \
|
|
18
|
+
"Creates parent directories if they do not exist. " \
|
|
19
|
+
"Use `edit` or `multi_edit` to modify an existing file in place."
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def input_schema
|
|
23
|
+
{
|
|
24
|
+
type: "object",
|
|
25
|
+
properties: {
|
|
26
|
+
file_path: { type: "string", description: "Absolute or relative file path" },
|
|
27
|
+
content: { type: "string", description: "Full file content to write" }
|
|
28
|
+
},
|
|
29
|
+
required: %w[file_path content]
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def risk_level
|
|
34
|
+
:medium
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def call(arguments)
|
|
38
|
+
file_path = arguments["file_path"] || arguments[:file_path]
|
|
39
|
+
content = arguments["content"] || arguments[:content] || ""
|
|
40
|
+
|
|
41
|
+
return "Error: file_path is required" if file_path.nil? || file_path.to_s.empty?
|
|
42
|
+
|
|
43
|
+
expanded = File.expand_path(file_path)
|
|
44
|
+
return workspace_violation_message(file_path) unless within_workspace?(expanded)
|
|
45
|
+
|
|
46
|
+
FileUtils.mkdir_p(File.dirname(expanded))
|
|
47
|
+
|
|
48
|
+
existed = File.exist?(expanded)
|
|
49
|
+
File.write(expanded, content)
|
|
50
|
+
|
|
51
|
+
verb = existed ? "overwrote" : "created"
|
|
52
|
+
bytes = content.to_s.bytesize
|
|
53
|
+
lines = content.to_s.lines.size
|
|
54
|
+
{ output: "#{verb} #{file_path} (#{bytes} bytes)",
|
|
55
|
+
metrics: "#{lines} line#{"s" if lines != 1} · #{bytes}B" }
|
|
56
|
+
rescue StandardError => e
|
|
57
|
+
"Error writing #{file_path}: #{e.message}"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
data/lib/rubino/trust.rb
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module Rubino
|
|
7
|
+
# Proportionate folder-trust, modelled on VS Code Workspace Trust and Claude
|
|
8
|
+
# Code's directory-trust dialog — but DELIBERATELY lighter, because rubino
|
|
9
|
+
# auto-RUNS no code from a project directory (config is HOME-only, there are
|
|
10
|
+
# no folder-open hooks, custom slash commands are user-triggered, and the
|
|
11
|
+
# arbitrary-Ruby tool loader can no longer load from cwd — see #44).
|
|
12
|
+
#
|
|
13
|
+
# What the gate protects: the ONE thing rubino auto-loads from a directory is
|
|
14
|
+
# *text into the system prompt* — its AGENTS.md / CLAUDE.md / .rubino.md /
|
|
15
|
+
# .cursorrules project-context files and its .rubino/skills catalogue. A
|
|
16
|
+
# hostile repo can use those to STEER the agent (prompt injection) the moment
|
|
17
|
+
# you start there. So, like VS Code's Restricted Mode, an untrusted directory
|
|
18
|
+
# still works — it just runs WITHOUT that directory's project context and
|
|
19
|
+
# skills until you vouch for it.
|
|
20
|
+
#
|
|
21
|
+
# What it does NOT do: there is no feature-disabling Restricted Mode (no
|
|
22
|
+
# auto-executed code to disable) and no per-tool gating — that would be
|
|
23
|
+
# ceremony without payoff given rubino's actual exposure.
|
|
24
|
+
#
|
|
25
|
+
# The decision is remembered in trusted_dirs.json under RUBINO_HOME so a
|
|
26
|
+
# trusted directory is never re-prompted (mirrors trustedDirectories).
|
|
27
|
+
module Trust
|
|
28
|
+
FILENAME = "trusted_dirs.json"
|
|
29
|
+
|
|
30
|
+
class << self
|
|
31
|
+
# True when +dir+ has been remembered as trusted. Compares on canonical
|
|
32
|
+
# (realpath) form so a symlinked/relative path matches its stored entry.
|
|
33
|
+
def trusted?(dir)
|
|
34
|
+
real = canonical(dir)
|
|
35
|
+
return false unless real
|
|
36
|
+
|
|
37
|
+
load_dirs.any? { |d| canonical(d) == real }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Remembers +dir+ as trusted (idempotent). Stores the canonical path so
|
|
41
|
+
# later lookups match regardless of how the dir is later referenced.
|
|
42
|
+
def remember(dir)
|
|
43
|
+
real = canonical(dir)
|
|
44
|
+
return unless real
|
|
45
|
+
|
|
46
|
+
dirs = load_dirs
|
|
47
|
+
return if dirs.any? { |d| canonical(d) == real }
|
|
48
|
+
|
|
49
|
+
save_dirs(dirs + [real])
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# The remembered list, canonicalised (for display / tests).
|
|
53
|
+
def trusted_dirs
|
|
54
|
+
load_dirs
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def store_path
|
|
58
|
+
File.join(Rubino.home_path, FILENAME)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def load_dirs
|
|
64
|
+
return [] unless File.exist?(store_path)
|
|
65
|
+
|
|
66
|
+
data = JSON.parse(File.read(store_path))
|
|
67
|
+
data.is_a?(Array) ? data.map(&:to_s) : []
|
|
68
|
+
rescue StandardError
|
|
69
|
+
[]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def save_dirs(dirs)
|
|
73
|
+
FileUtils.mkdir_p(File.dirname(store_path))
|
|
74
|
+
File.write(store_path, JSON.pretty_generate(dirs.uniq))
|
|
75
|
+
rescue StandardError
|
|
76
|
+
nil
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def canonical(path)
|
|
80
|
+
return nil if path.nil? || path.to_s.empty?
|
|
81
|
+
|
|
82
|
+
File.realpath(File.expand_path(path.to_s))
|
|
83
|
+
rescue StandardError
|
|
84
|
+
nil
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|