rubino-agent 0.3.0 → 0.5.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.
Files changed (196) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +11 -2
  3. data/AGENTS.md +1 -1
  4. data/CHANGELOG.md +172 -5
  5. data/CONTRIBUTING.md +10 -1
  6. data/README.md +14 -5
  7. data/Rakefile +31 -0
  8. data/docs/agents.md +42 -23
  9. data/docs/architecture.md +2 -2
  10. data/docs/commands.md +35 -3
  11. data/docs/configuration.md +20 -23
  12. data/docs/getting-started.md +5 -3
  13. data/docs/security.md +16 -5
  14. data/docs/skills.md +31 -0
  15. data/docs/troubleshooting.md +1 -1
  16. data/exe/rubino +16 -2
  17. data/install.sh +721 -59
  18. data/lib/rubino/active_agent.rb +73 -0
  19. data/lib/rubino/agent/action_claim_guard.rb +881 -0
  20. data/lib/rubino/agent/agent_registry.rb +5 -2
  21. data/lib/rubino/agent/definition.rb +1 -9
  22. data/lib/rubino/agent/fallback_chain.rb +0 -6
  23. data/lib/rubino/agent/iteration_budget.rb +109 -3
  24. data/lib/rubino/agent/loop.rb +476 -20
  25. data/lib/rubino/agent/model_call_runner.rb +81 -3
  26. data/lib/rubino/agent/prompts/build.txt +22 -5
  27. data/lib/rubino/agent/response_validator.rb +8 -0
  28. data/lib/rubino/agent/runner.rb +133 -8
  29. data/lib/rubino/agent/tool_executor.rb +166 -14
  30. data/lib/rubino/agent/truncation_continuation.rb +4 -1
  31. data/lib/rubino/api/server.rb +19 -0
  32. data/lib/rubino/attachments/classify.rb +35 -17
  33. data/lib/rubino/boot/config_guard.rb +71 -0
  34. data/lib/rubino/cli/chat/completion_builder.rb +42 -6
  35. data/lib/rubino/cli/chat/idle_card_host.rb +7 -1
  36. data/lib/rubino/cli/chat/session_resolver.rb +87 -21
  37. data/lib/rubino/cli/chat_command.rb +1189 -50
  38. data/lib/rubino/cli/commands.rb +282 -2
  39. data/lib/rubino/cli/config_command.rb +68 -8
  40. data/lib/rubino/cli/doctor_command.rb +204 -12
  41. data/lib/rubino/cli/jobs_command.rb +12 -0
  42. data/lib/rubino/cli/memory_command.rb +53 -20
  43. data/lib/rubino/cli/onboarding_wizard.rb +79 -6
  44. data/lib/rubino/cli/session_command.rb +172 -18
  45. data/lib/rubino/cli/setup_command.rb +131 -8
  46. data/lib/rubino/cli/skills_command.rb +183 -9
  47. data/lib/rubino/cli/trust_gate.rb +16 -7
  48. data/lib/rubino/commands/built_ins.rb +2 -0
  49. data/lib/rubino/commands/command.rb +12 -2
  50. data/lib/rubino/commands/executor.rb +149 -12
  51. data/lib/rubino/commands/handlers/agent_switch.rb +100 -0
  52. data/lib/rubino/commands/handlers/agents.rb +156 -41
  53. data/lib/rubino/commands/handlers/config.rb +4 -1
  54. data/lib/rubino/commands/handlers/help.rb +113 -14
  55. data/lib/rubino/commands/handlers/memory.rb +15 -5
  56. data/lib/rubino/commands/handlers/sessions.rb +26 -3
  57. data/lib/rubino/commands/handlers/status.rb +9 -4
  58. data/lib/rubino/commands/loader.rb +12 -0
  59. data/lib/rubino/config/configuration.rb +86 -24
  60. data/lib/rubino/config/defaults.rb +140 -33
  61. data/lib/rubino/config/loader.rb +62 -12
  62. data/lib/rubino/config/validator.rb +341 -0
  63. data/lib/rubino/config/writer.rb +123 -31
  64. data/lib/rubino/context/compressor.rb +184 -22
  65. data/lib/rubino/context/environment_inspector.rb +2 -2
  66. data/lib/rubino/context/file_discovery.rb +2 -2
  67. data/lib/rubino/context/message_boundary.rb +27 -1
  68. data/lib/rubino/context/project_languages.rb +90 -0
  69. data/lib/rubino/context/prompt_assembler.rb +105 -22
  70. data/lib/rubino/context/summary_builder.rb +45 -4
  71. data/lib/rubino/context/token_budget.rb +36 -11
  72. data/lib/rubino/context/token_estimate.rb +45 -0
  73. data/lib/rubino/context/tool_result_pruner.rb +81 -0
  74. data/lib/rubino/database/connection.rb +154 -3
  75. data/lib/rubino/database/migrations/001_create_initial_schema.rb +314 -40
  76. data/lib/rubino/database/migrator.rb +98 -5
  77. data/lib/rubino/documents/cap_exceeded.rb +13 -0
  78. data/lib/rubino/documents/converters/csv.rb +4 -3
  79. data/lib/rubino/documents/converters/docx.rb +29 -5
  80. data/lib/rubino/documents/converters/html.rb +5 -1
  81. data/lib/rubino/documents/converters/json.rb +2 -1
  82. data/lib/rubino/documents/converters/pdf.rb +11 -2
  83. data/lib/rubino/documents/converters/plain.rb +2 -1
  84. data/lib/rubino/documents/converters/pptx.rb +11 -2
  85. data/lib/rubino/documents/converters/xlsx.rb +35 -4
  86. data/lib/rubino/documents/converters/xml.rb +2 -1
  87. data/lib/rubino/documents/limits.rb +210 -0
  88. data/lib/rubino/documents.rb +10 -3
  89. data/lib/rubino/errors.rb +36 -5
  90. data/lib/rubino/interaction/cancel_token.rb +19 -3
  91. data/lib/rubino/interaction/events.rb +13 -0
  92. data/lib/rubino/interaction/lifecycle.rb +99 -13
  93. data/lib/rubino/interaction/polishing.rb +176 -0
  94. data/lib/rubino/jobs/cron_job_repository.rb +5 -8
  95. data/lib/rubino/jobs/handlers/cleanup_sessions_job.rb +11 -0
  96. data/lib/rubino/jobs/handlers/distill_skill_job.rb +65 -9
  97. data/lib/rubino/jobs/queue.rb +63 -8
  98. data/lib/rubino/jobs/runner.rb +24 -6
  99. data/lib/rubino/jobs/worker.rb +0 -4
  100. data/lib/rubino/llm/adapter_response.rb +47 -4
  101. data/lib/rubino/llm/credential_check.rb +15 -16
  102. data/lib/rubino/llm/error_classifier.rb +89 -1
  103. data/lib/rubino/llm/inline_think_filter.rb +69 -12
  104. data/lib/rubino/llm/request.rb +30 -3
  105. data/lib/rubino/llm/ruby_llm_adapter.rb +394 -46
  106. data/lib/rubino/llm/tool_bridge.rb +113 -9
  107. data/lib/rubino/mcp/manager.rb +18 -1
  108. data/lib/rubino/mcp/mcp_tool_wrapper.rb +14 -3
  109. data/lib/rubino/memory/aux_retry.rb +107 -0
  110. data/lib/rubino/memory/backends/sqlite.rb +73 -44
  111. data/lib/rubino/memory/backends.rb +23 -7
  112. data/lib/rubino/memory/salience_gate.rb +103 -0
  113. data/lib/rubino/memory/sqlite_extraction.rb +70 -0
  114. data/lib/rubino/memory/sqlite_extraction_prompt.rb +11 -0
  115. data/lib/rubino/memory/store.rb +33 -5
  116. data/lib/rubino/memory/threat_scanner.rb +52 -0
  117. data/lib/rubino/output/cost.rb +52 -0
  118. data/lib/rubino/output/headless_block_latch.rb +53 -0
  119. data/lib/rubino/output/result_serializer.rb +222 -0
  120. data/lib/rubino/output/turn_recorder.rb +77 -0
  121. data/lib/rubino/security/approval_policy.rb +227 -32
  122. data/lib/rubino/security/command_allowlist.rb +79 -4
  123. data/lib/rubino/security/doom_loop_detector.rb +21 -2
  124. data/lib/rubino/security/hardline_guard.rb +189 -16
  125. data/lib/rubino/security/pattern_matcher.rb +28 -5
  126. data/lib/rubino/security/prefix_deriver.rb +25 -6
  127. data/lib/rubino/security/readonly_commands.rb +145 -5
  128. data/lib/rubino/security/secret_path.rb +134 -0
  129. data/lib/rubino/security/url_safety.rb +255 -0
  130. data/lib/rubino/session/repository.rb +212 -11
  131. data/lib/rubino/session/store.rb +139 -14
  132. data/lib/rubino/skills/installer.rb +230 -0
  133. data/lib/rubino/skills/prompt_index.rb +2 -2
  134. data/lib/rubino/skills/registry.rb +52 -1
  135. data/lib/rubino/skills/skill.rb +64 -3
  136. data/lib/rubino/skills/skill_tool.rb +16 -5
  137. data/lib/rubino/tools/background_tasks.rb +157 -13
  138. data/lib/rubino/tools/base.rb +204 -3
  139. data/lib/rubino/tools/edit_tool.rb +73 -18
  140. data/lib/rubino/tools/glob_tool.rb +48 -9
  141. data/lib/rubino/tools/grep_tool.rb +103 -9
  142. data/lib/rubino/tools/multi_edit_tool.rb +64 -9
  143. data/lib/rubino/tools/patch_tool.rb +5 -0
  144. data/lib/rubino/tools/read_attachment_tool.rb +3 -1
  145. data/lib/rubino/tools/read_tool.rb +33 -15
  146. data/lib/rubino/tools/read_tracker.rb +153 -35
  147. data/lib/rubino/tools/registry.rb +113 -12
  148. data/lib/rubino/tools/result.rb +9 -1
  149. data/lib/rubino/tools/ruby_tool.rb +0 -0
  150. data/lib/rubino/tools/shell_registry.rb +70 -0
  151. data/lib/rubino/tools/shell_tool.rb +40 -1
  152. data/lib/rubino/tools/summarize_file_tool.rb +6 -0
  153. data/lib/rubino/tools/task_stop_tool.rb +10 -16
  154. data/lib/rubino/tools/task_tool.rb +36 -8
  155. data/lib/rubino/tools/vision_tool.rb +5 -0
  156. data/lib/rubino/tools/webfetch_tool.rb +39 -7
  157. data/lib/rubino/tools/websearch_tool.rb +92 -30
  158. data/lib/rubino/tools/write_tool.rb +23 -4
  159. data/lib/rubino/ui/api.rb +10 -1
  160. data/lib/rubino/ui/base.rb +11 -0
  161. data/lib/rubino/ui/bottom_composer.rb +382 -74
  162. data/lib/rubino/ui/cli.rb +515 -83
  163. data/lib/rubino/ui/completion_menu.rb +11 -7
  164. data/lib/rubino/ui/headless_trace.rb +63 -0
  165. data/lib/rubino/ui/live_region.rb +70 -7
  166. data/lib/rubino/ui/markdown_renderer.rb +142 -7
  167. data/lib/rubino/ui/notifier.rb +0 -2
  168. data/lib/rubino/ui/null.rb +52 -5
  169. data/lib/rubino/ui/paste_store.rb +16 -2
  170. data/lib/rubino/ui/queued_indicators.rb +6 -1
  171. data/lib/rubino/ui/status_bar.rb +61 -7
  172. data/lib/rubino/ui/streaming_markdown.rb +59 -6
  173. data/lib/rubino/ui/subagent_view.rb +29 -4
  174. data/lib/rubino/ui/tool_label.rb +52 -0
  175. data/lib/rubino/update_check.rb +39 -4
  176. data/lib/rubino/util/atomic_file.rb +117 -0
  177. data/lib/rubino/util/ignore_rules.rb +120 -0
  178. data/lib/rubino/util/output.rb +229 -12
  179. data/lib/rubino/util/secrets_mask.rb +70 -7
  180. data/lib/rubino/util/spill_store.rb +153 -0
  181. data/lib/rubino/version.rb +1 -1
  182. data/lib/rubino/workspace.rb +9 -1
  183. data/lib/rubino.rb +191 -7
  184. data/rubino-agent.gemspec +1 -0
  185. data/skills/ruby-expert/SKILL.md +1 -0
  186. metadata +42 -12
  187. data/lib/rubino/agent/router.rb +0 -65
  188. data/lib/rubino/database/migrations/002_create_runs.rb +0 -45
  189. data/lib/rubino/database/migrations/003_create_skill_states.rb +0 -15
  190. data/lib/rubino/database/migrations/004_create_cron_jobs.rb +0 -36
  191. data/lib/rubino/database/migrations/005_create_oauth_connections.rb +0 -27
  192. data/lib/rubino/database/migrations/006_create_webhook_deliveries.rb +0 -34
  193. data/lib/rubino/database/migrations/007_create_messages_fts.rb +0 -59
  194. data/lib/rubino/database/migrations/008_create_memory_facts.rb +0 -75
  195. data/lib/rubino/database/migrations/009_create_memory_graph.rb +0 -55
  196. data/lib/rubino/database/migrations/010_add_owner_pid_to_sessions.rb +0 -20
@@ -102,8 +102,10 @@ auxiliary:
102
102
  ```yaml
103
103
  agent:
104
104
  max_turns: 90 # Max turns per session
105
- max_tool_iterations: 8 # Max consecutive tool calls
106
- max_turn_seconds: 120 # Timeout per turn
105
+ max_tool_iterations: 25 # Max per-turn model<->tool round-trips (cap)
106
+ budget_extension_prompt: true # At the cap, prompt continue/summarize/abort (interactive only)
107
+ budget_extension_step: null # "+N" per extension (null = max_tool_iterations)
108
+ max_turn_seconds: 120 # Timeout per turn (outer rail; extensions never raise it)
107
109
  api_max_retries: 5 # LLM API retry count (exp backoff)
108
110
  api_retry_backoff_cap_seconds: 16 # Max per-retry backoff draw
109
111
  api_retry_backoff_overload_cap_seconds: 60 # Higher cap used only for overload (529/503)
@@ -287,7 +289,7 @@ tools:
287
289
  workspace_strict: true # Sandbox write/edit/delete to workspace_root; false = any reachable path
288
290
  git: true
289
291
  shell: true # ON by default (the agent ships to run inside an isolated VM);
290
- # every command is still gated by security.require_confirmation_for_shell
292
+ # dangerous commands are still gated by security.confirm_policy
291
293
  ruby: true
292
294
  web: false # Gates BOTH the webfetch and websearch tools
293
295
  memory: true
@@ -372,12 +374,13 @@ attachments:
372
374
 
373
375
  ```yaml
374
376
  security:
375
- # confirm_policy: "confirm_all" # confirm_all (default) | dangerous_only; derived from the alias below when absent
376
- require_confirmation_for_shell: true # legacy alias for confirm_policy; true => confirm_all
377
- command_allowlist: # prefix-matched commands pre-approved (empty = approve nothing)
377
+ confirm_policy: "dangerous_only" # dangerous_only (default) | confirm_all
378
+ # (the old require_confirmation_for_shell key was removed)
379
+ command_allowlist: # pre-approved commands (read-only intent only; empty = approve nothing)
378
380
  - "git status"
379
381
  - "git diff"
380
- - "bundle exec rspec"
382
+ # Test/build runners (bundle exec rspec, rake, npm test) are NOT shipped here:
383
+ # they load and run arbitrary project code, so add one only if you accept that.
381
384
  website_blocklist:
382
385
  enabled: false
383
386
  domains: []
@@ -461,20 +464,6 @@ formatters:
461
464
  "*.py": "black"
462
465
  ```
463
466
 
464
- ### agents
465
-
466
- Custom agent definitions:
467
-
468
- ```yaml
469
- agents:
470
- security:
471
- type: subagent
472
- model: "anthropic/claude-sonnet-4-20250514"
473
- description: "Security-focused code review"
474
- tools: [read, grep, glob]
475
- mcp_servers: []
476
- ```
477
-
478
467
  ### prompts
479
468
 
480
469
  System-prompt layering. The defaults ship the built-in role prompts.
@@ -513,9 +502,17 @@ formatters:
513
502
  "*.py": "black"
514
503
  ```
515
504
 
516
- ### agents (planned)
505
+ ### agents (planned — not yet read)
506
+
507
+ > **Status: planned, has no effect today.** The `agents:` key is reserved but is
508
+ > **not read** by the registry, so declaring custom agents in `config.yml` does
509
+ > nothing yet. Primary-agent *switching* among the built-in agents already ships
510
+ > (`/agent`, `/<name>`, Tab — see [agents.md](agents.md#primary-agent-switching));
511
+ > what is not wired is authoring NEW agents from config. To register a custom
512
+ > agent today, use `AgentRegistry#register` programmatically (see
513
+ > [agents.md](agents.md#custom-agents-via-code)).
517
514
 
518
- Custom agent definitions (multi-agent routing is not fully wired yet — see [agents.md](agents.md)):
515
+ The intended shape, once config-authored agents land:
519
516
 
520
517
  ```yaml
521
518
  agents:
@@ -4,7 +4,7 @@ From nothing to a working first answer in about five minutes. This is the happy
4
4
 
5
5
  ## 1. Install
6
6
 
7
- The fastest path on Linux (x86_64 / arm64) is the one-line installer. It installs a compatible Ruby via [`rv`](https://github.com/spinel-coop/rv), then the gem — all in user space, no sudo:
7
+ The fastest path on Linux and macOS (x86_64 / arm64) is the one-line installer. It installs a compatible Ruby and then the gem — all in user space, no sudo:
8
8
 
9
9
  ```bash
10
10
  curl -fsSL https://raw.githubusercontent.com/Jhonnyr97/rubino-agent/main/install.sh | bash
@@ -17,7 +17,9 @@ curl -fsSL https://raw.githubusercontent.com/Jhonnyr97/rubino-agent/main/install
17
17
  less install.sh && bash install.sh
18
18
  ```
19
19
 
20
- The installer is idempotent (safe to re-run) and prints the exact `PATH` line for the `rubino` executable when it finishes.
20
+ On Linux it offers a choice of Ruby provider — [`rv`](https://github.com/spinel-coop/rv) or [`mise`](https://mise.jdx.dev) (and Homebrew if `brew` is already present on macOS); pick non-interactively with `RUBINO_INSTALL_METHOD=rv|mise|brew`. On a **Debian 12 / old-glibc** box, prefer **mise**: `rv`'s musl build there yields a Ruby this glibc system can't run, so the installer steers `rv → mise` automatically. (For the mise method, `RUBINO_INSTALL_SCOPE=global|local` chooses user-wide vs this-directory-only. See the [README install matrix](../README.md#install).)
21
+
22
+ The installer is idempotent (safe to re-run). When it finishes it **persists the activation / `PATH` line to your shell rc** (`.zshrc` / `.bashrc` / `.profile`) and then **verifies in a fresh shell** that `rubino` is on `PATH`, failing loudly if it isn't — so a new terminal just works. Opt out of rc edits with `RUBINO_NO_MODIFY_RC=1` (it then prints the line for you to add).
21
23
 
22
24
  **Already manage Ruby yourself?** Requirements are Ruby >= 3.1 and SQLite3; then:
23
25
 
@@ -85,7 +87,7 @@ The first thing you see is a banner with the workspace, git branch, and model. T
85
87
 
86
88
  ```
87
89
  ▍❯ what does this project do?
88
- default · MiniMax-M3 · ctx ~0/128k
90
+ default · openai/gpt-4.1 · ctx ~0/128k
89
91
  ```
90
92
 
91
93
  > If you skipped the wizard during `setup`, a bare `rubino chat` re-runs it before the first turn (when on a TTY). If you're piping input or using `-q`, there's no prompt to run — instead you get a clear, actionable error telling you how to set a key (see below).
data/docs/security.md CHANGED
@@ -4,7 +4,7 @@ rubino runs real tools — shell, file writes, Ruby, git. The safety model is la
4
4
 
5
5
  ## Is it safe to enable shell?
6
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.
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.confirm_policy` is `dangerous_only`, so a command matching a dangerous pattern goes through an approval prompt (set it to `confirm_all` to prompt on every command), and a hardline floor blocks catastrophic commands regardless of any setting.
8
8
 
9
9
  ## The approval decision order
10
10
 
@@ -51,10 +51,12 @@ Actions: `allow`, `ask`, `deny`. A `deny` rule is a deny-class check and beats e
51
51
 
52
52
  ## Shell confirmation policy
53
53
 
54
- `security.confirm_policy` (with `security.require_confirmation_for_shell` as a legacy alias):
54
+ `security.confirm_policy`:
55
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.
56
+ - **`dangerous_only`** (default) — 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.
57
+ - **`confirm_all`** — every shell command not otherwise allowed/denied prompts for approval.
58
+
59
+ (The old `security.require_confirmation_for_shell` key was **removed** — it is no longer honored. Use `security.confirm_policy`.)
58
60
 
59
61
  ## Command allowlist
60
62
 
@@ -65,11 +67,14 @@ security:
65
67
  command_allowlist:
66
68
  - "git status"
67
69
  - "git diff"
68
- - "bundle exec rspec"
69
70
  ```
70
71
 
71
72
  An **empty** allowlist pre-approves nothing — pre-approval is opt-in.
72
73
 
74
+ A matched entry pre-approves only its **read-only intent**, never a smuggled write/exec form: an allowlisted head can't carry an output/exec flag (`git diff --output FILE`, `sort -o FILE`, `find -exec/-delete`), a git **global** flag (`git -c alias.x='!cmd' x`, `git -c core.sshCommand=…`, `git -C dir`, `--exec-path`), or a mutating/code-loading git subcommand (`git apply`, `git am`, `git push`, hooks). Heads whose argument is itself a program (`awk`, `sed`, `perl`, `python`, `ruby`, `node`, `tar`, `tee`, `xargs`, shells) are **never** auto-approved even if allowlisted — they still prompt. The **shipped default** is read-only git only; test/build runners (`bundle exec rspec`, `rake`, `npm test`) are deliberately not shipped auto-approved because they load and execute arbitrary project code by design — add one explicitly only if you accept that.
75
+
76
+ > An allowlist is a **convenience** layer, not a security boundary. Per industry practice (Claude Code/Codex, GTFOBins) a deny/allow list of command strings cannot be exhaustive; the OS sandbox is the real floor. This layer is narrowed to close the default-config and bare-`git` RCEs, not to be relied on as the only barrier.
77
+
73
78
  ## Auto-allowed read-only commands
74
79
 
75
80
  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.
@@ -99,6 +104,12 @@ approvals:
99
104
  - "docker ps" # multi-word entry matches those leading tokens
100
105
  ```
101
106
 
107
+ ## Headless / non-interactive approvals fail closed
108
+
109
+ A one-shot or scripted run (`rubino prompt`, `chat -q`, or any run with no TTY) has **no interactive session to approve from**, so it **fails closed**: a tool that would otherwise prompt — a write/edit, or a shell command **not** covered by your `permissions` / command allowlist / read-only auto-allow — is **blocked, not run**. A single-line `blocked: <tool> needs approval but no interactive session (use --yolo to allow, or allowlist it)` goes to stderr and the run exits **2**, so automation/CI fails loudly instead of silently skipping (or, worse, auto-executing) the action. Anything you already allowlisted, and every read-only command, still runs unprompted.
110
+
111
+ To opt back into full auto-execute, pass **`--yolo`**; **`--no-yolo`** forces fail-closed even if a yolo default was set. `--yolo` is honored **only** as a CLI flag — a project-local or persisted config can never grant it, so an untrusted checkout can't silently switch a scripted run into auto-execute. The hardline floor and explicit `permissions: deny` rules still apply under `--yolo`. (See [commands.md §Exit codes](commands.md#exit-codes-scripting-around-prompt--one-shot).)
112
+
102
113
  ## Deny/approve scope: once vs session
103
114
 
104
115
  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:
data/docs/skills.md CHANGED
@@ -31,6 +31,12 @@ skills:
31
31
  Override the search paths via the `skills.paths` config key. On a name collision
32
32
  the **directory** layout wins over the flat-file layout (it is the richer unit).
33
33
 
34
+ The registry additionally scans the **agent-neutral** skill dirs — project
35
+ `.agents/skills/` and `~/.agents/skills/` (the emerging cross-agent convention
36
+ used by `npx skills`, Gemini CLI, goose) — at the lowest precedence: a
37
+ rubino-path skill of the same name wins, and nothing changes when those dirs
38
+ are absent. The project-local one is trust-gated exactly like `.rubino/skills`.
39
+
34
40
  ### Two layouts
35
41
 
36
42
  | Layout | Path | Skill name | Bundled files |
@@ -59,6 +65,31 @@ skills:
59
65
  include_builtin: false # default true
60
66
  ```
61
67
 
68
+ ### Installing skills from git (`rubino skills install`)
69
+
70
+ Any git repo shipping the `<name>/SKILL.md` layout is a skill source — there is
71
+ no marketplace and nothing else is vendored in the gem. Skills are
72
+ shallow-cloned and copied into `~/.rubino/skills`, where the registry discovers
73
+ them like any hand-written skill:
74
+
75
+ ```bash
76
+ rubino skills install anthropics/skills --list # see what a source ships
77
+ rubino skills install anthropics/skills --skill pdf # pick by name (repeatable)
78
+ rubino skills install owner/repo --all # take everything
79
+ rubino skills install https://gitlab.com/o/r.git # any git URL works
80
+ rubino skills install --documents # anthropics/skills: pdf docx pptx xlsx
81
+ rubino skills update # re-fetch installed skills
82
+ rubino skills remove NAME # delete dir + provenance
83
+ ```
84
+
85
+ With no `--skill`/`--all` and multiple skills in the source, the CLI prints the
86
+ catalogue and asks you to pick (off a TTY it just prints the hint). Provenance
87
+ is recorded per installed skill in `~/.rubino/skills/.sources.json`
88
+ (`name → {source, path, commit}`): `rubino skills list` shows it in the Source
89
+ column, and `update` re-fetches from the recorded source, reporting
90
+ *up to date* vs *updated* by comparing commits. `remove` only deletes skills
91
+ this mechanism installed — hand-written skills are never touched.
92
+
62
93
  ### Authoring a `SKILL.md`
63
94
 
64
95
  A `SKILL.md` is YAML frontmatter followed by the instruction body:
@@ -48,7 +48,7 @@ The sqlite backend ranks by direct content relevance (FTS5/BM25) first; graph/re
48
48
 
49
49
  ## The agent keeps asking to approve every shell command
50
50
 
51
- That's `security.require_confirmation_for_shell: true` (the default `confirm_all` policy). Options: approve for the session at the prompt, add prefixes to `security.command_allowlist`, switch to `dangerous_only` (`security.confirm_policy: dangerous_only`), or use `/mode yolo` / `--yolo` to skip prompts (the hardline floor and `permissions: deny` still apply). See [security.md](security.md).
51
+ That's the `confirm_all` policy (`security.confirm_policy: confirm_all`). The default is `dangerous_only` (only dangerous-pattern commands prompt). Options: switch back with `rubino config set security.confirm_policy dangerous_only`, approve for the session at the prompt, add prefixes to `security.command_allowlist`, or use `/mode yolo` / `--yolo` to skip prompts (the hardline floor and `permissions: deny` still apply). See [security.md](security.md).
52
52
 
53
53
  ## A command was denied even with `--yolo`
54
54
 
data/exe/rubino CHANGED
@@ -1,9 +1,23 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
+ # On a bare POSIX/C-locale system Ruby's default_external is US-ASCII, so any
5
+ # File.read of a UTF-8 prompt/template (em-dashes etc.) raises
6
+ # Encoding::CompatibilityError before the agent even boots (#273; same class as
7
+ # #250/#251). Pin the process to UTF-8 up front so NO read path can regress,
8
+ # regardless of the user's locale. Promoting to UTF-8 is always safe; only
9
+ # touch it when the locale left us on a non-UTF-8 default.
10
+ Encoding.default_external = Encoding::UTF_8 unless Encoding.default_external == Encoding::UTF_8
11
+ Encoding.default_internal = Encoding::UTF_8 unless Encoding.default_internal == Encoding::UTF_8
12
+
4
13
  require "rubino"
5
14
 
6
- # Load .env before anything else so ENV vars are available immediately
7
- Rubino::Config::Loader.new.load
15
+ # Load .env before anything else so ENV vars are available immediately.
16
+ # A malformed config.yml must NOT crash the process with a raw Ruby+Psych
17
+ # backtrace (CFG-1) — ConfigGuard turns it into a clean boot abort so every
18
+ # command (including `rubino doctor`) reports it gracefully.
19
+ # Pass ARGV so the config-issue warning is suppressed for the pure-meta
20
+ # commands (version/help) that never need a configured model.
21
+ Rubino::Boot::ConfigGuard.load!(argv: ARGV)
8
22
 
9
23
  Rubino::CLI::Commands.start(ARGV)