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/security.md
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
rubino runs real tools — shell, file writes, Ruby, git. The safety model is layered: a non-bypassable hardline floor, explicit permission rules, an approval gate, a shell-confirmation policy, and a workspace sandbox. When run inside an isolated VM, the blast radius is limited to that VM.
|
|
4
|
+
|
|
5
|
+
## Is it safe to enable shell?
|
|
6
|
+
|
|
7
|
+
Yes. `tools.shell` is **on by default** because the agent ships to run inside an isolated VM where running commands is the whole point. Every command is still gated: by default `security.require_confirmation_for_shell` is `true`, so each shell command goes through an approval prompt, and a hardline floor blocks catastrophic commands regardless of any setting.
|
|
8
|
+
|
|
9
|
+
## The approval decision order
|
|
10
|
+
|
|
11
|
+
`Security::ApprovalPolicy#decide` resolves every tool call in this fixed order. The key invariant: **deny-class checks run before every allow path** — neither the hardline floor nor an explicit `permissions: deny` can be overridden by `yolo`, a `permissions: allow` rule, or the command allowlist.
|
|
12
|
+
|
|
13
|
+
1. **Hardline floor** (`:deny`) — a floor *below* yolo. Catastrophic, unrecoverable commands are denied unconditionally.
|
|
14
|
+
2. **`permissions: deny`** — an explicit deny rule also beats yolo.
|
|
15
|
+
3. **yolo / skip-approvals** — allow-exit (the doom-loop guard still applies).
|
|
16
|
+
4. **Doom-loop guard** — breaks an autopilot stuck repeating the same call.
|
|
17
|
+
5. **`permissions: allow` / `ask`** — remaining explicit rules.
|
|
18
|
+
6. **Command allowlist** (prefix match) — pre-approved commands → allow. Then the **read-only auto-allow** at the same seam: a shell command the parser can prove read-only (see [Auto-allowed read-only commands](#auto-allowed-read-only-commands)) → allow.
|
|
19
|
+
7. **Shell confirm policy** — `confirm_all` → ask; `dangerous_only` → ask only if the command matches a dangerous pattern, else allow.
|
|
20
|
+
8. **Mode fallback** — `skip` allows; `auto` asks only for high-risk tools; `manual` asks for any risky tool.
|
|
21
|
+
|
|
22
|
+
## The hardline floor
|
|
23
|
+
|
|
24
|
+
A deliberately **tiny** unconditional blocklist of commands with no recovery path — they never run via the agent, no matter the mode or rules:
|
|
25
|
+
|
|
26
|
+
- recursive delete of `/`, a protected system directory (`/etc`, `/usr`, …), or the home directory (`~` / `$HOME`)
|
|
27
|
+
- filesystem format (`mkfs`)
|
|
28
|
+
- `dd` to / redirect into a raw block device (`/dev/sda`, …)
|
|
29
|
+
- recursive `chmod`/`chown` of the root filesystem
|
|
30
|
+
- fork bomb
|
|
31
|
+
- kill all processes (`kill -1`)
|
|
32
|
+
- system shutdown / reboot / halt / poweroff (incl. `init 0/6`, `systemctl poweroff`, `telinit`)
|
|
33
|
+
- `sudo -S` (password guessing via stdin) — unless `SUDO_PASSWORD` is set
|
|
34
|
+
|
|
35
|
+
Recoverable-but-risky operations (`git reset --hard`, `rm -rf /tmp/x`, `chmod -R 777`, `curl | sh`) are **not** here — they belong to the dangerous-pattern layer, where yolo/approval can pass them through. The same hardline check runs again as defense-in-depth inside `ShellTool` before execution.
|
|
36
|
+
|
|
37
|
+
## Permission rules
|
|
38
|
+
|
|
39
|
+
Pattern rules in `config.yml` (wildcard support) give explicit verdicts:
|
|
40
|
+
|
|
41
|
+
```yaml
|
|
42
|
+
permissions:
|
|
43
|
+
"git *": "allow"
|
|
44
|
+
"shell rm -rf *": "deny"
|
|
45
|
+
"shell bundle *": "allow"
|
|
46
|
+
"write ~/.env": "deny"
|
|
47
|
+
"read *": "allow"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Actions: `allow`, `ask`, `deny`. A `deny` rule is a deny-class check and beats every allow path.
|
|
51
|
+
|
|
52
|
+
## Shell confirmation policy
|
|
53
|
+
|
|
54
|
+
`security.confirm_policy` (with `security.require_confirmation_for_shell` as a legacy alias):
|
|
55
|
+
|
|
56
|
+
- **`confirm_all`** (default; alias `true`) — every shell command not otherwise allowed/denied prompts for approval.
|
|
57
|
+
- **`dangerous_only`** (alias `false`) — safe commands run unprompted; only commands matching a dangerous pattern prompt. The hardline floor and `permissions: deny` still run first, so this never weakens the floor.
|
|
58
|
+
|
|
59
|
+
## Command allowlist
|
|
60
|
+
|
|
61
|
+
Prefix-matched commands pre-approved without a prompt:
|
|
62
|
+
|
|
63
|
+
```yaml
|
|
64
|
+
security:
|
|
65
|
+
command_allowlist:
|
|
66
|
+
- "git status"
|
|
67
|
+
- "git diff"
|
|
68
|
+
- "bundle exec rspec"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
An **empty** allowlist pre-approves nothing — pre-approval is opt-in.
|
|
72
|
+
|
|
73
|
+
## Auto-allowed read-only commands
|
|
74
|
+
|
|
75
|
+
A built-in allowlist layer (`Security::ReadonlyCommands`, **on by default**) lets provably read-only shell commands run without a prompt. It is evaluated at the same decision step as the command allowlist — *below* the hardline floor and `permissions: deny`, which always win, even for commands you add yourself.
|
|
76
|
+
|
|
77
|
+
The built-in set: `ls`, `pwd`, `find`, `cat`, `head`, `tail`, `grep`, `rg`, `wc`, `file`, `stat`, `du`, `df`, `which`, `whoami`, `date`, `tree`, `echo`, plus read-only git subcommands (`git status|log|diff|show|rev-parse|blame`, `git branch` in pure listing form, `git remote`/`git remote -v`).
|
|
78
|
+
|
|
79
|
+
A command auto-allows only when the **entire line** parses as safe:
|
|
80
|
+
|
|
81
|
+
- no output redirection (`>`, `>>`, `2>`; `tee` is simply not in the set) — plain `<` input redirection is fine;
|
|
82
|
+
- no command substitution (`` ` `` or `$(...)`) or process substitution (`<(...)`, `>(...)`) in a live context (single-quoted text is literal and stays allowed);
|
|
83
|
+
- chains (`|`, `&&`, `;`, `||`) only when **every** segment starts with a command from the set — `grep -rn TODO lib | head -20` runs, `cat file; rm file` prompts;
|
|
84
|
+
- no wrapper heads smuggling execution (`env`, `xargs`, `sh -c`, `bash -c`, `sudo`, `nohup` are not in the set);
|
|
85
|
+
- no leading variable assignments (`FOO=bar ls` prompts — an assignment can change what the command resolves to);
|
|
86
|
+
- no mutating flags on otherwise-safe heads: `find -exec/-execdir/-ok/-okdir/-delete/-fprintf/-fprint/-fls`, `date -s/--set`, `tree -o`, `git ... --output`;
|
|
87
|
+
- no `&` backgrounding, comments, or unbalanced quotes;
|
|
88
|
+
- no dangerous-pattern match on the whole line (defense-in-depth for user-extended sets).
|
|
89
|
+
|
|
90
|
+
Anything the parser cannot prove safe **fails closed to the normal approval prompt** — never to silent execution.
|
|
91
|
+
|
|
92
|
+
Configure it under `approvals`:
|
|
93
|
+
|
|
94
|
+
```yaml
|
|
95
|
+
approvals:
|
|
96
|
+
auto_allow_readonly: true # set false to prompt for everything again
|
|
97
|
+
readonly_commands: # extend the built-in set; same parse validation applies
|
|
98
|
+
- "jq" # bare name matches that command head
|
|
99
|
+
- "docker ps" # multi-word entry matches those leading tokens
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Deny/approve scope: once vs session
|
|
103
|
+
|
|
104
|
+
At the approval prompt you can decide for just this call or for the rest of the session. Session approvals are remembered by a **prefix/pattern class**, not the raw command:
|
|
105
|
+
|
|
106
|
+
- a **dangerous** command remembers its pattern class (approving `git push --force origin main` once also covers `git push -f other`);
|
|
107
|
+
- a **plain** command remembers only the exact command (approving `git status` does not auto-approve `git diff`).
|
|
108
|
+
|
|
109
|
+
Session approvals live in-process only (an `always`/disk-persistent tier is reserved but not wired). The granularity matches the matcher, so approving `shell ls` never auto-approves `shell rm -rf /`.
|
|
110
|
+
|
|
111
|
+
## Abandoned approvals
|
|
112
|
+
|
|
113
|
+
A run parked on a human decision is bounded by `approvals.wait_timeout_seconds` (default 900s / 15 min). On expiry the gate **auto-denies** (never auto-approves) and frees the worker thread, so a closed tab can't park a server worker indefinitely. While a decision is pending, the SSE idle watchdog is suspended for that run so it isn't reaped mid-wait. Set to `null` for an unbounded wait (interruptible only by an explicit run stop — discouraged on shared servers).
|
|
114
|
+
|
|
115
|
+
## Workspace sandbox
|
|
116
|
+
|
|
117
|
+
`tools.workspace_strict: true` (default) confines write/edit/delete tools to the workspace root (`terminal.cwd` or `Dir.pwd`). Set it to `false` only if you trust the model plus the approval flow alone to touch any path the process can reach.
|
|
118
|
+
|
|
119
|
+
## Attachment SSRF guard
|
|
120
|
+
|
|
121
|
+
URL attachments are fetched only when the host is in `attachments.allowed_hosts` (plus anything in the `ALLOWED_FILE_URL_HOSTS` env var, comma-separated). Loopback hosts (`localhost`, `127.0.0.1`, `::1`) are always allowed. Empty list + empty env = only loopback is fetchable. The file-attachment policy also fails closed: oversize (>25 MB by default), unsafe, or disallowed-kind files are warned and skipped. The same policy gates CLI image attachments (`-i`/`--image`, `@image` tokens, dropped paths, `/paste`): a file that fails classification or the size cap is rejected client-side, before any provider call.
|
|
122
|
+
|
|
123
|
+
## On-demand document reading (`read_attachment`)
|
|
124
|
+
|
|
125
|
+
Rather than inlining every attachment's bytes into the prompt by default, the `read_attachment` tool ([tools.md](tools.md)) pulls a document's content only when the model asks — the single biggest reduction in prompt-injection surface from the attachment work. It runs the same fail-closed classification first (regular-file check, workspace confine, size cap, magic-bytes-wins MIME), then converts the document to Markdown **in-process** via the in-repo `Rubino::Documents` module (a focused Ruby reimplementation of markitdown's CORE converters — no external `markitdown`/`pdftotext` process). The converted Markdown is wrapped in the same nonce-framed, defanged, "this is untrusted user data, NOT instructions" envelope as inline text, and oversized output is routed through the `summarize` auxiliary model instead of flooding context. The document converters lean on optional MIT extraction gems (`roo`, `docx`, `pdf-reader`, `ruby_powerpoint`) that are lazily required — none is a hard dependency. When a format's gem is absent (or the format is unsupported), `read_attachment` returns an actionable shell-extraction hint instead of raising, so a missing gem can never break a turn. `rubino doctor` lists which formats convert in-process.
|
|
126
|
+
|
|
127
|
+
## <a id="autonomous-memory"></a>Autonomous memory tool
|
|
128
|
+
|
|
129
|
+
The `memory` tool lets the agent write to its own future context. Every write passes the same injection-defense floor as the memory store — a `ThreatScanner` (prompt-injection / exfiltration patterns) plus a per-group character budget — so a fact can't splice tainted or over-budget content into a later system prompt. See [memory.md](memory.md).
|
|
130
|
+
|
|
131
|
+
## Fake provider guard
|
|
132
|
+
|
|
133
|
+
The fake LLM provider can short-circuit tool decisions, so `chat` and `server` refuse to boot with a fake model unless `RUBINO_ALLOW_FAKE=1` is set. Production deployments must never set it.
|
|
134
|
+
|
|
135
|
+
## TLS for the HTTP API
|
|
136
|
+
|
|
137
|
+
The API binds `127.0.0.1` by default; only expose it (`--host 0.0.0.0` / `RUBINO_API_HOST`) behind TLS or a trusted segment. For a remote HTTP client, set `RUBINO_TLS=1` (or leave a cert in place) and the API serves over a self-signed cert that the client **pins** (no DNS / Let's Encrypt needed). On first boot it generates `cert.pem` + `key.pem` under `$RUBINO_HOME/tls` (CN/SAN = host/IP, ~10y) and reuses them. Hand the public cert to a pinning client with:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
rubino tls-cert # prints $RUBINO_HOME/tls/cert.pem (generating it if absent)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The private key never leaves the box. (refs #69)
|
|
144
|
+
|
|
145
|
+
Set `RUBINO_ENCRYPTION_KEY` to encrypt stored OAuth tokens at rest (required for the OAuth routes). See [oauth-providers.md](oauth-providers.md).
|
data/docs/skills.md
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
# Skills
|
|
2
|
+
|
|
3
|
+
Skills are reusable instruction packs the agent can pull into context when a task
|
|
4
|
+
calls for them. A skill bundles specialized knowledge — APIs, tool-specific
|
|
5
|
+
commands, proven workflows, and your team's conventions and quality bars — into a
|
|
6
|
+
single `SKILL.md` (plus optional bundled reference files). Instead of bloating
|
|
7
|
+
every prompt with that knowledge, the agent sees a short *index* of available
|
|
8
|
+
skills up front and loads the full instructions only for the ones that are
|
|
9
|
+
relevant.
|
|
10
|
+
|
|
11
|
+
## What a skill is
|
|
12
|
+
|
|
13
|
+
A skill is a Markdown file (with YAML frontmatter) and, optionally, a directory of
|
|
14
|
+
bundled files alongside it. The agent treats a loaded skill's body as authoritative
|
|
15
|
+
instructions for the matching task.
|
|
16
|
+
|
|
17
|
+
### Where skills live
|
|
18
|
+
|
|
19
|
+
By default the registry scans two directories (project-local first, then the
|
|
20
|
+
home-level one):
|
|
21
|
+
|
|
22
|
+
```yaml
|
|
23
|
+
# config: skills section (docs/configuration.md#skills)
|
|
24
|
+
skills:
|
|
25
|
+
enabled: true
|
|
26
|
+
paths:
|
|
27
|
+
- ".rubino/skills" # project-local
|
|
28
|
+
- "~/.rubino/skills" # shared across projects
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Override the search paths via the `skills.paths` config key. On a name collision
|
|
32
|
+
the **directory** layout wins over the flat-file layout (it is the richer unit).
|
|
33
|
+
|
|
34
|
+
### Two layouts
|
|
35
|
+
|
|
36
|
+
| Layout | Path | Skill name | Bundled files |
|
|
37
|
+
| --- | --- | --- | --- |
|
|
38
|
+
| Directory (preferred) | `<dir>/<name>/SKILL.md` | the directory name | yes — anything next to `SKILL.md` |
|
|
39
|
+
| Flat file (legacy) | `<dir>/<name>.md` | the file basename | no |
|
|
40
|
+
|
|
41
|
+
The directory layout matches the Claude skill format and is preferred
|
|
42
|
+
because it can carry bundled references, scripts, and assets.
|
|
43
|
+
|
|
44
|
+
### Built-in (gem-bundled) skills
|
|
45
|
+
|
|
46
|
+
On top of the configured user paths, the registry **always** scans the
|
|
47
|
+
`skills/` directory shipped *inside the gem*. This is how a skill reaches every
|
|
48
|
+
install with no copy step and updates automatically on `gem update`. The bundled
|
|
49
|
+
**`ruby-expert`** skill (deep Ruby/Rails knowledge across idioms, OO design,
|
|
50
|
+
concurrency, Rails, testing, performance, security, and more) ships this way.
|
|
51
|
+
|
|
52
|
+
Built-ins are scanned **before** the user paths, so a user skill of the same
|
|
53
|
+
name placed in `.rubino/skills` or `~/.rubino/skills` transparently overrides the
|
|
54
|
+
built-in (last writer wins on the name-indexed merge). To run with only your own
|
|
55
|
+
skills, set:
|
|
56
|
+
|
|
57
|
+
```yaml
|
|
58
|
+
skills:
|
|
59
|
+
include_builtin: false # default true
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Authoring a `SKILL.md`
|
|
63
|
+
|
|
64
|
+
A `SKILL.md` is YAML frontmatter followed by the instruction body:
|
|
65
|
+
|
|
66
|
+
```markdown
|
|
67
|
+
---
|
|
68
|
+
name: pdf-extraction
|
|
69
|
+
description: Extract text and tables from PDFs using markitdown; handles scanned pages.
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
# PDF extraction
|
|
73
|
+
|
|
74
|
+
When the user gives you a PDF, prefer `markitdown <file>` over ad-hoc parsers.
|
|
75
|
+
For scanned/image PDFs, first OCR with ... (full instructions here).
|
|
76
|
+
|
|
77
|
+
See `references/markitdown-flags.md` for the flag cheatsheet.
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Frontmatter fields the registry reads:
|
|
81
|
+
|
|
82
|
+
- **`name`** — the skill's invocation name. If omitted, it defaults to the
|
|
83
|
+
directory name (directory layout) or the file basename (flat layout).
|
|
84
|
+
- **`description`** — the one-liner shown in the index. This is the *only* text the
|
|
85
|
+
agent sees before deciding to load the skill, so make it match-on-sight: say
|
|
86
|
+
what the skill is for and when it applies.
|
|
87
|
+
|
|
88
|
+
Everything after the closing `---` is the body that gets loaded at Level 2 (below).
|
|
89
|
+
A skill with no frontmatter still works: the name falls back to the basename and the
|
|
90
|
+
description falls back to the first heading line.
|
|
91
|
+
|
|
92
|
+
Bundled files in the directory layout live anywhere under the skill dir (e.g.
|
|
93
|
+
`references/api.md`, `scripts/run.py`). VCS and junk dirs (`.git`, `node_modules`,
|
|
94
|
+
`__pycache__`, …) are excluded. Bundled-file reads are sandboxed to the skill's own
|
|
95
|
+
directory — paths that escape it (via `..`, absolute paths, or out-of-dir symlinks)
|
|
96
|
+
are rejected.
|
|
97
|
+
|
|
98
|
+
## The 3-level progressive disclosure
|
|
99
|
+
|
|
100
|
+
The core idea: the agent should *know which skills exist* without paying the token
|
|
101
|
+
cost of reading them all, then pull in the full text only for the one(s) it
|
|
102
|
+
actually needs. This happens in three levels.
|
|
103
|
+
|
|
104
|
+
### Level 1 — DISCOVERY (the index)
|
|
105
|
+
|
|
106
|
+
At the start of a turn the system prompt carries a `## Skills` block listing every
|
|
107
|
+
enabled skill as `name: description` — and nothing more. The agent knows a skill
|
|
108
|
+
exists and what it is for, but has **not** read its instructions. This index is the
|
|
109
|
+
load-bearing auto-trigger: surfacing the catalogue in the system prompt (not just in
|
|
110
|
+
the `skill` tool's description) is what makes the model proactively scan for a
|
|
111
|
+
relevant skill before replying.
|
|
112
|
+
|
|
113
|
+
Disabled skills are excluded from the index, so a skill toggled off never appears.
|
|
114
|
+
|
|
115
|
+
### Level 2 — SKILL LOADED (the body)
|
|
116
|
+
|
|
117
|
+
When the agent decides a skill is relevant it calls the `skill(name)` tool, which
|
|
118
|
+
pulls the **full `SKILL.md` body** into context. This is the moment a skill goes
|
|
119
|
+
from *"the agent knows it exists"* to *"the agent is using it"* — the actual unit of
|
|
120
|
+
skill **usage**.
|
|
121
|
+
|
|
122
|
+
This is exactly the event you want to measure. Loading a body:
|
|
123
|
+
|
|
124
|
+
- returns the body (prefixed `Skill '<name>' loaded:`) to the model, and
|
|
125
|
+
- if the skill is a directory with bundled files, appends a list of those files so
|
|
126
|
+
the model knows what it can pull next, and
|
|
127
|
+
- emits the `SKILL_LOADED` observability signal (see below).
|
|
128
|
+
|
|
129
|
+
If the named skill doesn't exist, the tool returns the list of available skills; if
|
|
130
|
+
it exists but is disabled, it returns a distinct "disabled" message.
|
|
131
|
+
|
|
132
|
+
### Level 3 — REFERENCES (bundled files on demand)
|
|
133
|
+
|
|
134
|
+
The body can point at bundled reference files. The agent loads one by calling the
|
|
135
|
+
same tool with a `file_path`:
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
skill(name: "pdf-extraction", file_path: "references/markitdown-flags.md")
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The file's contents are read (sandboxed to the skill dir) and returned. This keeps
|
|
142
|
+
deep reference material out of context until the moment it's needed. A `file_path`
|
|
143
|
+
that isn't found returns the live list of available bundled files instead.
|
|
144
|
+
|
|
145
|
+
So the disclosure ladder is: **index (Level 1) → body (Level 2) → references
|
|
146
|
+
(Level 3)**, each step pulled in only when warranted.
|
|
147
|
+
|
|
148
|
+
## Creating skills
|
|
149
|
+
|
|
150
|
+
Beyond loading existing skills, the agent can author new ones so a complex,
|
|
151
|
+
repeatable task is captured once and reused. There are two mechanisms — both
|
|
152
|
+
gated by `skills.enabled` (default true).
|
|
153
|
+
|
|
154
|
+
### 1. Deterministic post-turn distillation (primary)
|
|
155
|
+
|
|
156
|
+
After every turn, `DistillSkillJob` runs alongside `ExtractMemoryJob`. Its gate
|
|
157
|
+
is **fully deterministic** (no model call): it fires only when
|
|
158
|
+
|
|
159
|
+
- the run produced a non-empty final answer (succeeded), **and**
|
|
160
|
+
- the turn used at least `RA_DISTILL_TOOL_THRESHOLD` tool calls (default **5**,
|
|
161
|
+
mirroring the reference "5+"), **and**
|
|
162
|
+
- no existing skill already covers the work.
|
|
163
|
+
|
|
164
|
+
Only on a gate-pass does it spend **one** auxiliary-LLM call to distil the
|
|
165
|
+
transcript into a `SKILL.md` candidate, which it writes to the first
|
|
166
|
+
`skills.paths` dir. Trivial sessions pass the gate zero times, so they cost zero
|
|
167
|
+
extra calls. Raise or lower the bound with `RA_DISTILL_TOOL_THRESHOLD`.
|
|
168
|
+
|
|
169
|
+
This is the mechanism that actually creates skills in practice: an A/B bench
|
|
170
|
+
found that a prompt nudge or an on-demand tool alone are ignored under load
|
|
171
|
+
(F1 = 0), while the deterministic post-turn job created good, reusable skills
|
|
172
|
+
with no false positives.
|
|
173
|
+
|
|
174
|
+
### 2. On-demand `skill(action: "create")` (manual)
|
|
175
|
+
|
|
176
|
+
The agent can also create a skill inline during a turn, with no extra LLM call:
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
skill(action: "create",
|
|
180
|
+
name: "kebab-case-name",
|
|
181
|
+
description: "What it's for and WHEN it applies.",
|
|
182
|
+
body: "# Title\n\nProven step-by-step instructions and pitfalls.")
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
It validates the name (kebab-case, ≤64 chars), requires a non-empty description
|
|
186
|
+
(≤1024 chars) and body, writes `<name>/SKILL.md` with valid frontmatter, and
|
|
187
|
+
refuses to overwrite an existing skill. The new skill is immediately
|
|
188
|
+
discoverable. `action` defaults to `"load"`, so existing `skill(name:)` calls
|
|
189
|
+
are unaffected.
|
|
190
|
+
|
|
191
|
+
Both paths emit `SKILL_CREATED` (`{ name:, file_path: }`) on the turn-scoped bus
|
|
192
|
+
and count toward `skills_created_total` (see Observability).
|
|
193
|
+
|
|
194
|
+
## Observability
|
|
195
|
+
|
|
196
|
+
You can measure skill *use* — not just that skills exist — through one event and two
|
|
197
|
+
metrics (added in #132).
|
|
198
|
+
|
|
199
|
+
### The `SKILL_LOADED` event
|
|
200
|
+
|
|
201
|
+
When the `skill` tool successfully loads a body (Level 2), it emits `SKILL_LOADED`
|
|
202
|
+
on the turn-scoped event bus. The recorder/SSE layer surfaces it as **`skill.loaded`**
|
|
203
|
+
(parity with `tool.started`, `subagent.spawned`, etc.).
|
|
204
|
+
|
|
205
|
+
- **Internal symbol:** `Interaction::Events::SKILL_LOADED`
|
|
206
|
+
- **Recorder / SSE name:** `skill.loaded`
|
|
207
|
+
- **Payload:** `{ name: <skill name> }` (the run association — `run_id` — is stamped
|
|
208
|
+
by the recorder, like every other event).
|
|
209
|
+
|
|
210
|
+
It fires once per successful body load. Loading a Level-3 bundled file does **not**
|
|
211
|
+
emit it — the signal tracks the level-2 "skill is now in use" transition.
|
|
212
|
+
|
|
213
|
+
### Metrics
|
|
214
|
+
|
|
215
|
+
Two Prometheus counters expose skill activity on `GET /v1/metrics`:
|
|
216
|
+
|
|
217
|
+
| Metric | Increments when | Measures |
|
|
218
|
+
| --- | --- | --- |
|
|
219
|
+
| `skills_loaded_total` | a skill body is successfully loaded via the `skill` tool (Level 2) | **usage / adoption** — how often skills are actually pulled in |
|
|
220
|
+
| `skills_created_total` | a skill not seen on the prior scan appears on a registry **re-scan** | **creation** — how often new skills show up |
|
|
221
|
+
|
|
222
|
+
Their registered HELP strings:
|
|
223
|
+
|
|
224
|
+
- `skills_loaded_total` — *"Number of times a skill was successfully loaded via the
|
|
225
|
+
`skill` tool."*
|
|
226
|
+
- `skills_created_total` — *"Number of new skills observed by the registry on a
|
|
227
|
+
re-scan (disk-diff signal; no creation tool exists)."*
|
|
228
|
+
|
|
229
|
+
**Why creation is a disk-diff, not a tool.** There is no skill-creation tool — the
|
|
230
|
+
agent (or a human) just writes files. So the cleanest in-process signal is the
|
|
231
|
+
registry noticing a name on a re-scan that wasn't there before. Consequences worth
|
|
232
|
+
knowing:
|
|
233
|
+
|
|
234
|
+
- The first scan is treated as initial enumeration and does **not** count existing
|
|
235
|
+
skills as "created" — only a *re*-discover books new names.
|
|
236
|
+
- Counting is per new name that appears (`by: <count of new names>`).
|
|
237
|
+
- A skill removed and later re-added would be re-counted. That's fine for a usage
|
|
238
|
+
signal; it is not a strict ledger.
|
|
239
|
+
|
|
240
|
+
**How to read them.** `skills_loaded_total` answers the adoption question — *"of the
|
|
241
|
+
sessions where a relevant skill existed, did the agent actually load it?"* Pair it
|
|
242
|
+
with `skills_created_total` to see whether newly authored skills are being picked up:
|
|
243
|
+
a rising creation count with a flat load count means new skills aren't getting used
|
|
244
|
+
(weak descriptions, wrong paths, or the index not triggering). Both are best-effort
|
|
245
|
+
instrumentation — they never alter skill-loading behavior.
|
|
246
|
+
|
|
247
|
+
## The `skill` tool
|
|
248
|
+
|
|
249
|
+
The agent interacts with skills through a single tool, `skill`, that handles
|
|
250
|
+
loads (Level 2 / Level 3) and on-demand creation:
|
|
251
|
+
|
|
252
|
+
- `skill(name: "<skill>")` — load the body.
|
|
253
|
+
- `skill(name: "<skill>", file_path: "references/...")` — load a bundled file.
|
|
254
|
+
- `skill(action: "create", name:, description:, body:)` — author a new skill
|
|
255
|
+
(see [Creating skills](#creating-skills)).
|
|
256
|
+
|
|
257
|
+
It is a low-risk tool. For the full tool entry see **[docs/tools.md](tools.md#skill)**.
|
|
258
|
+
|
|
259
|
+
### Disabling skills
|
|
260
|
+
|
|
261
|
+
The whole tool is gated like any other tool by the `tools.skill` config flag
|
|
262
|
+
(opt-out — enabled unless explicitly set to `false`):
|
|
263
|
+
|
|
264
|
+
```yaml
|
|
265
|
+
tools:
|
|
266
|
+
skill: false # the agent can no longer load skills
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Individual skills can also be toggled on/off (persisted in the `skill_states`
|
|
270
|
+
table, default-enabled) through any of three equivalent surfaces, all running
|
|
271
|
+
the same registry-validated write (`Skills::Toggle`):
|
|
272
|
+
|
|
273
|
+
- **In chat** — `/skills enable <name>` / `/skills disable <name>` (#188).
|
|
274
|
+
Disabling the currently *active* skill also clears the pin (the assembler
|
|
275
|
+
would silently drop a disabled skill, leaving a lying chip).
|
|
276
|
+
- **CLI** — `rubino skills enable|disable NAME`; `rubino skills list` shows the
|
|
277
|
+
markers and `rubino skills show NAME` prints the `SKILL.md` body so you can
|
|
278
|
+
review a skill before enabling it.
|
|
279
|
+
- **HTTP API** — `PUT /v1/skills/<name>` with `{ "enabled": false }` — see
|
|
280
|
+
[docs/api/v1.md](api/v1.md).
|
|
281
|
+
|
|
282
|
+
Disabling a skill removes it from the Level-1 index, makes the tool return a
|
|
283
|
+
distinct "disabled" message if invoked by name, and blocks `/skills <name>`
|
|
284
|
+
activation until re-enabled.
|
|
285
|
+
|
|
286
|
+
Note: a bare `/skills <name>` does **not** enable or disable —
|
|
287
|
+
it activates one (next section), which is a different concept.
|
|
288
|
+
|
|
289
|
+
### Active skill (`/skills`)
|
|
290
|
+
|
|
291
|
+
On top of the model-driven 3-level disclosure, the *user* can pin one skill as
|
|
292
|
+
the session's **active skill** from interactive chat:
|
|
293
|
+
|
|
294
|
+
- `/skills` — list the available skills, with `(disabled)` and `(active)`
|
|
295
|
+
markers.
|
|
296
|
+
- `/skills <name>` — activate `<name>`: its full body is **force-loaded into the
|
|
297
|
+
system prompt every turn** (no `skill` tool call needed), until cleared or the
|
|
298
|
+
process exits. Typing `/skills ` opens a dropdown picker of skill names (plus
|
|
299
|
+
the `enable`/`disable` verbs), headed by a `✗ none` clear entry. A *disabled*
|
|
300
|
+
skill is refused with a pointer to `/skills enable <name>`.
|
|
301
|
+
- `/skills none` (or picking `✗ none`) — clear the active skill:
|
|
302
|
+
`✓ Cleared active skill (was: <name>).`
|
|
303
|
+
|
|
304
|
+
While a skill is active the status bar under the input shows it — `default · skill <name> · …` —
|
|
305
|
+
so you always know what extra instructions the model is carrying. A fresh
|
|
306
|
+
`rubino chat` boots with no active skill.
|
|
307
|
+
|
|
308
|
+
Activation respects the folder-trust gate: a project-local skill that lives in
|
|
309
|
+
an untrusted directory is refused with a reason (its `SKILL.md` would never be
|
|
310
|
+
injected), instead of showing an active chip with no effect.
|
|
311
|
+
|
|
312
|
+
**Activate vs enable/disable:** activating loads one skill's body into context
|
|
313
|
+
for *this session* (a per-session pin); enabling/disabling (the chat/CLI/API
|
|
314
|
+
toggle above) controls whether a skill exists in the Level-1 index *at all*,
|
|
315
|
+
for every session. They are independent surfaces.
|
|
316
|
+
|
|
317
|
+
### Skills vs custom commands
|
|
318
|
+
|
|
319
|
+
They look similar but trigger differently: a **skill** is loaded *by the model* when
|
|
320
|
+
it judges a task relevant (model-driven, via the `skill` tool), whereas a **custom
|
|
321
|
+
command** is invoked *by the user* with a slash command (user-driven). See
|
|
322
|
+
**[docs/commands.md](commands.md)** for custom commands.
|