rubino-agent 0.5.0 → 0.5.1
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 +4 -4
- data/.rubocop.yml +6 -0
- data/.rubocop_todo.yml +1 -0
- data/CHANGELOG.md +317 -0
- data/README.md +56 -7
- data/Rakefile +17 -0
- data/docs/agents.md +40 -25
- data/docs/architecture.md +2 -9
- data/docs/commands.md +18 -6
- data/docs/configuration.md +154 -7
- data/docs/mcp.md +3 -3
- data/docs/memory.md +3 -3
- data/docs/security.md +1 -1
- data/docs/tools.md +45 -49
- data/ext/landlock/extconf.rb +78 -0
- data/ext/landlock/landlock.c +253 -0
- data/lib/rubino/agent/action_claim_guard.rb +61 -29
- data/lib/rubino/agent/definition.rb +3 -19
- data/lib/rubino/agent/iteration_budget.rb +1 -1
- data/lib/rubino/agent/loop.rb +188 -22
- data/lib/rubino/agent/prompts/build.txt +36 -5
- data/lib/rubino/agent/prompts/general.txt +8 -3
- data/lib/rubino/agent/runner.rb +179 -10
- data/lib/rubino/agent/tool_executor.rb +205 -20
- data/lib/rubino/agent/truncation_continuation.rb +7 -4
- data/lib/rubino/api/operations/approvals/decide_operation.rb +0 -4
- data/lib/rubino/api/operations/clarifications/decide_operation.rb +0 -4
- data/lib/rubino/api/operations/cron_jobs/create_operation.rb +0 -4
- data/lib/rubino/api/operations/cron_jobs/delete_operation.rb +0 -4
- data/lib/rubino/api/operations/cron_jobs/list_operation.rb +0 -4
- data/lib/rubino/api/operations/cron_jobs/pause_operation.rb +1 -5
- data/lib/rubino/api/operations/cron_jobs/resume_operation.rb +1 -5
- data/lib/rubino/api/operations/cron_jobs/show_operation.rb +0 -4
- data/lib/rubino/api/operations/cron_jobs/trigger_operation.rb +0 -4
- data/lib/rubino/api/operations/cron_jobs/update_operation.rb +0 -4
- data/lib/rubino/api/operations/files/read_operation.rb +1 -5
- data/lib/rubino/api/operations/files/upload_operation.rb +0 -4
- data/lib/rubino/api/operations/health_operation.rb +1 -5
- data/lib/rubino/api/operations/memory/delete_operation.rb +0 -4
- data/lib/rubino/api/operations/memory/index_operation.rb +0 -4
- data/lib/rubino/api/operations/memory/stats_operation.rb +0 -4
- data/lib/rubino/api/operations/metrics_operation.rb +1 -1
- data/lib/rubino/api/operations/mode/show_operation.rb +0 -4
- data/lib/rubino/api/operations/mode/update_operation.rb +0 -4
- data/lib/rubino/api/operations/models/list_operation.rb +0 -4
- data/lib/rubino/api/operations/oauth/connections/disconnect_operation.rb +0 -4
- data/lib/rubino/api/operations/oauth/connections/list_operation.rb +0 -4
- data/lib/rubino/api/operations/oauth/providers/callback_operation.rb +0 -4
- data/lib/rubino/api/operations/oauth/providers/connect_operation.rb +0 -4
- data/lib/rubino/api/operations/oauth/providers/list_operation.rb +0 -4
- data/lib/rubino/api/operations/runs/create_operation.rb +0 -4
- data/lib/rubino/api/operations/runs/events_operation.rb +0 -4
- data/lib/rubino/api/operations/runs/stop_operation.rb +0 -4
- data/lib/rubino/api/operations/sessions/create_operation.rb +0 -4
- data/lib/rubino/api/operations/sessions/delete_operation.rb +0 -4
- data/lib/rubino/api/operations/sessions/index_operation.rb +0 -4
- data/lib/rubino/api/operations/sessions/retry_operation.rb +0 -4
- data/lib/rubino/api/operations/sessions/show_operation.rb +0 -4
- data/lib/rubino/api/operations/sessions/undo_operation.rb +0 -4
- data/lib/rubino/api/operations/skills/list_operation.rb +0 -4
- data/lib/rubino/api/operations/skills/toggle_operation.rb +0 -4
- data/lib/rubino/api/operations/tasks/index_operation.rb +0 -4
- data/lib/rubino/api/operations/tasks/show_operation.rb +0 -4
- data/lib/rubino/api/operations/tasks/stop_operation.rb +0 -4
- data/lib/rubino/api/router.rb +2 -2
- data/lib/rubino/attachments/policy.rb +8 -0
- data/lib/rubino/attachments/preamble.rb +16 -8
- data/lib/rubino/cli/chat/completion_builder.rb +2 -2
- data/lib/rubino/cli/chat/session_resolver.rb +100 -30
- data/lib/rubino/cli/chat_command.rb +607 -113
- data/lib/rubino/cli/commands.rb +93 -1
- data/lib/rubino/cli/config_command.rb +54 -7
- data/lib/rubino/cli/doctor_command.rb +73 -20
- data/lib/rubino/cli/jobs_command.rb +38 -11
- data/lib/rubino/cli/memory_command.rb +29 -9
- data/lib/rubino/cli/onboarding_wizard.rb +6 -1
- data/lib/rubino/cli/server_command.rb +43 -1
- data/lib/rubino/cli/session_command.rb +129 -29
- data/lib/rubino/cli/setup_command.rb +166 -4
- data/lib/rubino/cli/skills_command.rb +21 -0
- data/lib/rubino/commands/built_ins.rb +2 -2
- data/lib/rubino/commands/executor.rb +16 -11
- data/lib/rubino/commands/handlers/agents.rb +199 -30
- data/lib/rubino/commands/handlers/config.rb +4 -0
- data/lib/rubino/commands/handlers/display.rb +50 -0
- data/lib/rubino/commands/handlers/help.rb +2 -9
- data/lib/rubino/commands/handlers/mcp.rb +7 -32
- data/lib/rubino/commands/handlers/memory.rb +10 -35
- data/lib/rubino/commands/handlers/sessions.rb +64 -50
- data/lib/rubino/commands/handlers/skills.rb +47 -28
- data/lib/rubino/commands/handlers/status.rb +56 -6
- data/lib/rubino/compression/compression_result.rb +35 -0
- data/lib/rubino/compression/compressor.rb +109 -0
- data/lib/rubino/compression/content_router.rb +240 -0
- data/lib/rubino/compression/diff_compressor.rb +252 -0
- data/lib/rubino/compression/javascript_code_skeleton.rb +15 -0
- data/lib/rubino/compression/json_compressor.rb +274 -0
- data/lib/rubino/compression/line_skeleton.rb +92 -0
- data/lib/rubino/compression/log_compressor.rb +299 -0
- data/lib/rubino/compression/python_code_skeleton.rb +122 -0
- data/lib/rubino/compression/ruby_code_skeleton.rb +80 -0
- data/lib/rubino/compression/tree_sitter_code_skeleton.rb +118 -0
- data/lib/rubino/compression/tsx_code_skeleton.rb +15 -0
- data/lib/rubino/compression/typescript_code_skeleton.rb +15 -0
- data/lib/rubino/config/configuration.rb +70 -86
- data/lib/rubino/config/defaults.rb +229 -8
- data/lib/rubino/config/loader.rb +9 -1
- data/lib/rubino/config/reasoning_prefs.rb +23 -0
- data/lib/rubino/config/validator.rb +50 -7
- data/lib/rubino/context/compressor.rb +1 -1
- data/lib/rubino/context/file_discovery.rb +0 -8
- data/lib/rubino/context/message_boundary.rb +2 -7
- data/lib/rubino/context/project_languages.rb +0 -7
- data/lib/rubino/context/prompt_assembler.rb +7 -2
- data/lib/rubino/context/summary_builder.rb +34 -25
- data/lib/rubino/context/token_budget.rb +3 -3
- data/lib/rubino/database/migrations/001_create_initial_schema.rb +1 -1
- data/lib/rubino/database/migrator.rb +0 -26
- data/lib/rubino/files/workspace.rb +2 -2
- data/lib/rubino/interaction/events.rb +0 -3
- data/lib/rubino/interaction/input_queue.rb +11 -0
- data/lib/rubino/interaction/lifecycle.rb +144 -25
- data/lib/rubino/interaction/polishing.rb +8 -0
- data/lib/rubino/interaction/probe.rb +1 -1
- data/lib/rubino/jobs/cron_job_repository.rb +0 -4
- data/lib/rubino/jobs/handlers/distill_skill_job.rb +3 -13
- data/lib/rubino/jobs/queue.rb +70 -5
- data/lib/rubino/jobs/worker.rb +1 -1
- data/lib/rubino/llm/adapter_factory.rb +1 -1
- data/lib/rubino/llm/auxiliary_client.rb +63 -3
- data/lib/rubino/llm/cache_breakpoint_middleware.rb +194 -0
- data/lib/rubino/llm/credential_check.rb +61 -4
- data/lib/rubino/llm/error_classifier.rb +142 -121
- data/lib/rubino/llm/fake_provider.rb +3 -3
- data/lib/rubino/llm/inline_think_filter.rb +34 -3
- data/lib/rubino/llm/reasoning_manager.rb +3 -26
- data/lib/rubino/llm/request.rb +0 -16
- data/lib/rubino/llm/ruby_llm_adapter.rb +233 -25
- data/lib/rubino/llm/scenario_loader.rb +10 -17
- data/lib/rubino/llm/scenarios/glued-table-prose.yml +36 -0
- data/lib/rubino/llm/scenarios/growing-table.yml +49 -0
- data/lib/rubino/llm/scenarios/narrow-terminal-table.yml +47 -0
- data/lib/rubino/llm/scenarios/streamed-table.yml +55 -0
- data/lib/rubino/llm/scenarios/table-then-prose.yml +34 -0
- data/lib/rubino/llm/scenarios/too-wide-table.yml +47 -0
- data/lib/rubino/llm/scenarios/wide-table.yml +1 -1
- data/lib/rubino/llm/thinking_support.rb +17 -12
- data/lib/rubino/llm/tool_bridge.rb +101 -37
- data/lib/rubino/mcp/manager.rb +53 -9
- data/lib/rubino/mcp/mcp_tool_wrapper.rb +24 -0
- data/lib/rubino/memory/backends/sqlite.rb +43 -35
- data/lib/rubino/memory/backends.rb +3 -3
- data/lib/rubino/memory/deduplicator.rb +22 -0
- data/lib/rubino/memory/flusher.rb +35 -1
- data/lib/rubino/memory/salience_gate.rb +26 -0
- data/lib/rubino/memory/sqlite_extraction_prompt.rb +5 -1
- data/lib/rubino/memory/store.rb +29 -29
- data/lib/rubino/memory/threat_scanner.rb +8 -0
- data/lib/rubino/memory.rb +47 -0
- data/lib/rubino/oauth/provider.rb +0 -5
- data/lib/rubino/run/event_store.rb +1 -6
- data/lib/rubino/run/repository.rb +0 -14
- data/lib/rubino/security/approval_policy.rb +116 -30
- data/lib/rubino/security/command_normalizer.rb +36 -0
- data/lib/rubino/security/dangerous_patterns.rb +17 -4
- data/lib/rubino/security/hardline_guard.rb +4 -3
- data/lib/rubino/security/readonly_commands.rb +299 -15
- data/lib/rubino/security/redactor.rb +272 -0
- data/lib/rubino/security/sandbox.rb +460 -0
- data/lib/rubino/security/secret_detector.rb +110 -0
- data/lib/rubino/security/secret_path.rb +136 -7
- data/lib/rubino/session/lock.rb +91 -0
- data/lib/rubino/session/message.rb +38 -3
- data/lib/rubino/session/picker.rb +95 -0
- data/lib/rubino/session/repository.rb +57 -40
- data/lib/rubino/session/store.rb +0 -11
- data/lib/rubino/skills/registry.rb +14 -5
- data/lib/rubino/skills/skill.rb +31 -10
- data/lib/rubino/skills/skill_tool.rb +3 -18
- data/lib/rubino/skills/state_repository.rb +0 -4
- data/lib/rubino/tools/background_tasks.rb +179 -40
- data/lib/rubino/tools/base.rb +87 -73
- data/lib/rubino/tools/edit_tool.rb +50 -20
- data/lib/rubino/tools/fuzzy_match.rb +212 -0
- data/lib/rubino/tools/glob_tool.rb +5 -1
- data/lib/rubino/tools/grep_tool.rb +17 -51
- data/lib/rubino/tools/multi_edit_tool.rb +32 -19
- data/lib/rubino/tools/patch_tool.rb +51 -10
- data/lib/rubino/tools/probe_tool.rb +0 -20
- data/lib/rubino/tools/question_tool.rb +54 -2
- data/lib/rubino/tools/read_attachment_tool.rb +21 -11
- data/lib/rubino/tools/read_tool.rb +131 -25
- data/lib/rubino/tools/read_tracker.rb +36 -0
- data/lib/rubino/tools/registry.rb +63 -44
- data/lib/rubino/tools/result.rb +43 -12
- data/lib/rubino/tools/retrieve_output_tool.rb +70 -0
- data/lib/rubino/tools/ruby_tool.rb +0 -0
- data/lib/rubino/tools/shell_kill_tool.rb +6 -2
- data/lib/rubino/tools/shell_output_tool.rb +7 -1
- data/lib/rubino/tools/shell_registry.rb +169 -15
- data/lib/rubino/tools/shell_tail_tool.rb +6 -1
- data/lib/rubino/tools/shell_tool.rb +483 -53
- data/lib/rubino/tools/steer_tool.rb +2 -21
- data/lib/rubino/tools/subagent_probe.rb +1 -1
- data/lib/rubino/tools/summarize_file_tool.rb +6 -0
- data/lib/rubino/tools/task_result_tool.rb +8 -2
- data/lib/rubino/tools/task_stop_tool.rb +5 -6
- data/lib/rubino/tools/task_tool.rb +200 -103
- data/lib/rubino/tools/vision_tool.rb +32 -4
- data/lib/rubino/tools/webfetch_tool.rb +145 -0
- data/lib/rubino/tools/write_tool.rb +1 -1
- data/lib/rubino/ui/agent_menu.rb +179 -0
- data/lib/rubino/ui/api.rb +2 -2
- data/lib/rubino/ui/base.rb +2 -2
- data/lib/rubino/ui/bottom_composer.rb +1112 -140
- data/lib/rubino/ui/cli.rb +898 -262
- data/lib/rubino/ui/completion_menu.rb +24 -43
- data/lib/rubino/ui/composer/input_line.rb +131 -0
- data/lib/rubino/ui/composer/subagent_panel.rb +35 -0
- data/lib/rubino/ui/headless_trace.rb +1 -1
- data/lib/rubino/ui/input_history.rb +90 -5
- data/lib/rubino/ui/live_region.rb +12 -0
- data/lib/rubino/ui/markdown_renderer.rb +103 -41
- data/lib/rubino/ui/menu_view.rb +117 -0
- data/lib/rubino/ui/null.rb +1 -1
- data/lib/rubino/ui/paste_store.rb +33 -1
- data/lib/rubino/ui/printer_base.rb +135 -8
- data/lib/rubino/ui/streaming_markdown.rb +89 -0
- data/lib/rubino/ui/subagent_cards.rb +126 -25
- data/lib/rubino/util/atomic_file.rb +12 -0
- data/lib/rubino/util/duration.rb +8 -5
- data/lib/rubino/util/output.rb +55 -10
- data/lib/rubino/version.rb +7 -1
- data/lib/rubino/workspace.rb +65 -2
- data/lib/rubino.rb +29 -22
- data/rubino-agent.gemspec +27 -1
- metadata +78 -20
- data/docs/plugins.md +0 -195
- data/lib/rubino/interaction/state.rb +0 -56
- data/lib/rubino/memory/backends/default.rb +0 -101
- data/lib/rubino/memory/extractor.rb +0 -85
- data/lib/rubino/memory/retriever.rb +0 -50
- data/lib/rubino/plugins/registry.rb +0 -75
- data/lib/rubino/plugins.rb +0 -86
- data/lib/rubino/tools/answer_child_tool.rb +0 -83
- data/lib/rubino/tools/ask_parent_tool.rb +0 -232
- data/lib/rubino/tools/git_tool.rb +0 -71
- data/lib/rubino/tools/github_tool.rb +0 -233
- data/lib/rubino/tools/test_tool.rb +0 -454
- data/lib/rubino/ui/subagent_view.rb +0 -280
data/docs/commands.md
CHANGED
|
@@ -85,6 +85,7 @@ Pasting **text** into the chat input goes through the file-backed paste pipeline
|
|
|
85
85
|
- `--new` forces a fresh session; `--continue`/`-c` resumes the latest; `--resume`/`-r <id|title>` resumes a specific one.
|
|
86
86
|
- `--resume` matches an ID prefix first, then a case-insensitive substring of the session title **or its full first prompt** — so a memorable phrase from the tail of a long first message works even though the stored title is truncated. More than one match is an error listing the candidates; no match exits non-zero with a pointer to `rubino sessions list`.
|
|
87
87
|
- One-shot mode (`-q` / `prompt`) does **not** auto-resume — automation isn't silently hijacked onto a past session; pass `--resume`/`--continue` explicitly if you want it.
|
|
88
|
+
- A bare **`rubino sessions`** on a real terminal opens an arrow-key resume picker over this directory's sessions (`--all` for every dir); ↑↓ select, Enter loads the chosen session into the chat REPL (the same as `rubino chat --session <id>`), Esc cancels. Off a TTY (piped/redirected) it prints the static `list` table so scripts stay deterministic; `rubino sessions list` is always the table.
|
|
88
89
|
- One-shot output: when stdout is a **terminal** the answer renders through the same markdown pipeline as interactive chat (styled text, fitted tables, wrapping); when stdout is **piped/redirected** the answer stays plain raw text and diagnostics go to stderr, so `$(rubino prompt …)` stays clean.
|
|
89
90
|
- Sessions are marked ended on clean exit, terminal close (SIGHUP), or kill (SIGTERM), so a closed window doesn't leave a session looking active.
|
|
90
91
|
|
|
@@ -134,6 +135,7 @@ rubino memory show ID
|
|
|
134
135
|
rubino memory delete ID
|
|
135
136
|
rubino memory backend [NAME] # show the active memory backend, or switch to NAME
|
|
136
137
|
|
|
138
|
+
rubino sessions # on a TTY: arrow-key resume picker (Enter loads, Esc cancels); piped: lists
|
|
137
139
|
rubino sessions list
|
|
138
140
|
rubino sessions show ID
|
|
139
141
|
rubino sessions compact ID
|
|
@@ -175,9 +177,9 @@ Type these inside `rubino chat`. Generated from `BuiltIns::DESCRIPTIONS` (drift-
|
|
|
175
177
|
| `/export` | Write the session transcript as markdown (/export [path]) |
|
|
176
178
|
| `/memory` | Inspect/search/forget what the agent remembers (show ID, backend, --all) |
|
|
177
179
|
| `/agent` | Switch the primary agent (/agent <name>; a bare /<name> or Tab cycles) |
|
|
178
|
-
| `/agents` | List background subagents;
|
|
180
|
+
| `/agents` | List background subagents; ↓+Enter to attach & steer one live, or steer/probe/view by id |
|
|
179
181
|
| `/tasks` | Alias for /agents |
|
|
180
|
-
| `/reply` | Answer a subagent that is blocked waiting on you (
|
|
182
|
+
| `/reply` | Answer a subagent that is blocked waiting on you (e.g. an approval) |
|
|
181
183
|
| `/stop` | Stop a running subagent (/stop <id>; alias for /agents <id> --stop) |
|
|
182
184
|
| `/jobs` | List the background job queue (status counts); /jobs <id> for detail |
|
|
183
185
|
| `/skills` | List skills; activate one ('none' clears), or enable/disable NAME |
|
|
@@ -202,9 +204,10 @@ Type these inside `rubino chat`. Generated from `BuiltIns::DESCRIPTIONS` (drift-
|
|
|
202
204
|
|
|
203
205
|
You can keep typing while a turn is running — the pinned input stays live:
|
|
204
206
|
|
|
205
|
-
- **Enter**
|
|
206
|
-
- **
|
|
207
|
-
- **`/queued <message>`**
|
|
207
|
+
- **Enter** queues the line **without** interrupting (the queue-by-default / type-ahead model, #421): the current turn keeps running, the line waits behind any earlier-queued items (FIFO) with a live `⏳ queued:` indicator above the input, and it is committed as a normal message when its turn runs. At idle (no turn running) Enter submits immediately.
|
|
208
|
+
- **Esc** interrupts the current turn (the partial answer is kept and marked `⎿ interrupted`); any queued lines then run as the next turns.
|
|
209
|
+
- **`/queued <message>`** queues a message explicitly — the terminal-independent way to enqueue without typing it into the live input.
|
|
210
|
+
- **Read-only meta-commands run immediately, mid-turn.** A small set of non-mutating slash commands — `/agents` (and `/tasks`), `/stop`, `/status`, `/jobs`, `/help`, `/commands`, `/dirs` — execute **right away** while a turn is running, so you can drill into a sub-agent, stop the run, or check status without interrupting. State-mutating commands (`/model`, `/clear`, `/new`, `/config`, `/mode`, `/reasoning`, `/think`, …) are not available mid-turn: they show a transient `⚠ <cmd> is not available during an active turn — press Esc to interrupt first` notice instead of running.
|
|
208
211
|
|
|
209
212
|
### Keys: `Esc Esc` — rewind to an earlier message
|
|
210
213
|
|
|
@@ -324,12 +327,21 @@ The agent spawns background subagents with its `task` tool; these commands are t
|
|
|
324
327
|
/agents <id> --stop # cancel a running subagent (blocked descendants unwind too)
|
|
325
328
|
/agents <id> steer "note" # park a note folded into the child's context at its next turn
|
|
326
329
|
/agents <id> probe "question" # ephemeral read-only peek — nothing is saved to the child
|
|
327
|
-
/reply <id> <answer> # answer a subagent blocked on an
|
|
330
|
+
/reply <id> <answer> # answer a subagent blocked on you (e.g. an approval)
|
|
328
331
|
/reply # bare: list the subagents currently blocked on you
|
|
329
332
|
```
|
|
330
333
|
|
|
331
334
|
`/tasks` is an alias for `/agents`.
|
|
332
335
|
|
|
336
|
+
**Attach to a subagent (agent-view).** Instead of typing ids, press `↓` at the
|
|
337
|
+
idle prompt to open the subagent picker, arrow to one, and `Enter` to **attach**:
|
|
338
|
+
the screen switches to that agent's own full timeline (its tool calls and what it
|
|
339
|
+
said, replayed) and the prompt becomes scoped — `sa_xxxx ❯`. While attached, just
|
|
340
|
+
type to steer the running child (or answer it if it's blocked on you); `←` on the
|
|
341
|
+
empty prompt (or `/detach`) returns to the main timeline. The scoped prompt makes
|
|
342
|
+
the global `/agents <id> steer/probe` and `/reply <id>` forms redundant — they're
|
|
343
|
+
the same operations, by id, from anywhere.
|
|
344
|
+
|
|
333
345
|
### Workspace roots: `/add-dir` and `/dirs`
|
|
334
346
|
|
|
335
347
|
The workspace sandbox confines write/edit/delete tools to the workspace roots. `/add-dir <path>` adds an extra allowed root mid-session (and runs the one-time folder-trust gate, so the new root's `AGENTS.md`/skills are only honored once vouched for); `/dirs` lists the current roots and their trust state. Typing `/add-dir ` opens a directory-path dropdown (relative, absolute, and `~` paths complete as you type).
|
data/docs/configuration.md
CHANGED
|
@@ -65,10 +65,27 @@ providers:
|
|
|
65
65
|
assume_model_exists: true
|
|
66
66
|
base_url: null
|
|
67
67
|
request_timeout_seconds: 600
|
|
68
|
+
extra_body: {} # free-form body merged into /v1/chat/completions
|
|
68
69
|
```
|
|
69
70
|
|
|
70
71
|
Per-provider you may also set `api_key`, and for custom gateways `anthropic_compatible: true` (MiniMax) or `openai_compatible: true`. See [models-and-keys.md](models-and-keys.md).
|
|
71
72
|
|
|
73
|
+
#### `extra_body` — OpenAI-compatible request passthrough
|
|
74
|
+
|
|
75
|
+
`providers.<name>.extra_body` is a free-form hash deep-merged verbatim into the OpenAI-style `/v1/chat/completions` request body. It is honored **only on the OpenAI-compatible request path** (`openai_compatible: true`, or the native `openai` provider) and is never applied on the anthropic-family path, nor does it touch the thinking-budget logic. Adapter-routed keys (`max_tokens`, `thinking`) win on conflict. Left unset (the default `{}`) the request is byte-identical to before.
|
|
76
|
+
|
|
77
|
+
Use it to pass provider-specific knobs the adapter does not model natively. The canonical case is suppressing chain-of-thought leakage on oMLX / Qwen-style backends that emit `<think>` text instead of native `tool_calls` unless the request carries `chat_template_kwargs: { enable_thinking: false }`:
|
|
78
|
+
|
|
79
|
+
```yaml
|
|
80
|
+
providers:
|
|
81
|
+
gateway:
|
|
82
|
+
openai_compatible: true
|
|
83
|
+
base_url: "http://localhost:8000/v1"
|
|
84
|
+
extra_body:
|
|
85
|
+
chat_template_kwargs:
|
|
86
|
+
enable_thinking: false
|
|
87
|
+
```
|
|
88
|
+
|
|
72
89
|
Per-provider `supports_thinking: true | false` declares whether the backend handles an Anthropic-style thinking budget correctly; `false` means no budget is ever sent to it, regardless of `thinking.effort`. Unset, MiniMax-family model ids default to `false`, everything else to `true` — see [reasoning & thinking](#reasoning--thinking).
|
|
73
90
|
|
|
74
91
|
### auxiliary
|
|
@@ -97,6 +114,14 @@ auxiliary:
|
|
|
97
114
|
timeout: 300
|
|
98
115
|
```
|
|
99
116
|
|
|
117
|
+
Each block routes through `LLM::AuxiliaryClient`, so `provider`/`model`/`base_url`
|
|
118
|
+
are all honored: `provider: "main"` (or empty) reuses the primary provider, an empty
|
|
119
|
+
`model` falls back to `model.default`, and a `base_url` points that task at a
|
|
120
|
+
different endpoint. `auxiliary.compression` is the **context-compaction summary**
|
|
121
|
+
model — at the defaults it is the primary model (e.g. MiniMax-M3), unchanged; set
|
|
122
|
+
`provider`/`model`/`base_url` to run compaction summaries on a different
|
|
123
|
+
(OpenAI-compatible) endpoint.
|
|
124
|
+
|
|
100
125
|
### agent
|
|
101
126
|
|
|
102
127
|
```yaml
|
|
@@ -154,7 +179,7 @@ ui:
|
|
|
154
179
|
|
|
155
180
|
### notifications
|
|
156
181
|
|
|
157
|
-
Attention signals for the moments the agent needs human eyes: a long turn finishing, an approval prompt parking the run on a decision
|
|
182
|
+
Attention signals for the moments the agent needs human eyes: a long turn finishing, or an approval prompt parking the run on a decision (the main agent's card or a background subagent flipping to `needs_approval`).
|
|
158
183
|
|
|
159
184
|
```yaml
|
|
160
185
|
notifications:
|
|
@@ -164,7 +189,7 @@ notifications:
|
|
|
164
189
|
min_turn_seconds: 10 # a turn must run at least this long before its completion notifies; quick turns stay silent
|
|
165
190
|
```
|
|
166
191
|
|
|
167
|
-
- **Events**: `turn_finished` (only when the turn ran ≥ `min_turn_seconds`), `needs_approval` (the main agent's approval card or a background child flipping to `needs approval`), `blocked
|
|
192
|
+
- **Events**: `turn_finished` (only when the turn ran ≥ `min_turn_seconds`), `needs_approval` (the main agent's approval card or a background child flipping to `needs approval`). A third event, `blocked`, exists in the enum for a child parked on the human; with subagents now non-blocking it is not raised in normal operation.
|
|
168
193
|
- **Bell hygiene**: the BEL byte is only ever written to a real terminal — never into a pipe — and is routed to the real terminal IO even while the bottom composer owns the screen (BEL doesn't move the cursor).
|
|
169
194
|
- **`command` hook**: runs detached and best-effort (stdio nulled, errors swallowed to the log) with `RUBINO_EVENT` (`turn_finished` | `needs_approval` | `blocked`) and `RUBINO_MESSAGE` in its environment — the seam for `osascript` (macOS), `notify-send` (Linux), or any custom notifier.
|
|
170
195
|
- **Spam control**: events within ~1s of the last emitted one coalesce into a single signal.
|
|
@@ -212,7 +237,6 @@ streaming:
|
|
|
212
237
|
transport: "off"
|
|
213
238
|
edit_interval: 0.3
|
|
214
239
|
buffer_threshold: 40
|
|
215
|
-
cursor: " ▉"
|
|
216
240
|
|
|
217
241
|
context:
|
|
218
242
|
engine: "compressor"
|
|
@@ -244,7 +268,7 @@ compression:
|
|
|
244
268
|
```yaml
|
|
245
269
|
memory:
|
|
246
270
|
enabled: true
|
|
247
|
-
backend: "sqlite" #
|
|
271
|
+
backend: "sqlite" # SQLite FTS5/BM25 + graph-lite recall (default). "default" = legacy non-ranked store
|
|
248
272
|
auto_extract: true
|
|
249
273
|
auto_save: true
|
|
250
274
|
user_profile_enabled: true
|
|
@@ -279,7 +303,7 @@ tasks:
|
|
|
279
303
|
max_children_per_node: 3 # max LIVE direct children per node
|
|
280
304
|
max_concurrent_total: 8 # hard ceiling on total LIVE subagents across the tree
|
|
281
305
|
max_live_probes_per_child: 5 # per-child budget for billed live probes (probe(live: true))
|
|
282
|
-
ask_parent_timeout: 900 #
|
|
306
|
+
ask_parent_timeout: 900 # vestigial: governed the removed child→parent ask channel; no effect now
|
|
283
307
|
```
|
|
284
308
|
|
|
285
309
|
### tools
|
|
@@ -291,7 +315,7 @@ tools:
|
|
|
291
315
|
shell: true # ON by default (the agent ships to run inside an isolated VM);
|
|
292
316
|
# dangerous commands are still gated by security.confirm_policy
|
|
293
317
|
ruby: true
|
|
294
|
-
web:
|
|
318
|
+
web: true # ON by default (keyless DuckDuckGo backend); gates BOTH the webfetch and websearch tools
|
|
295
319
|
memory: true
|
|
296
320
|
```
|
|
297
321
|
|
|
@@ -315,6 +339,107 @@ file_read:
|
|
|
315
339
|
max_chars: 100000
|
|
316
340
|
```
|
|
317
341
|
|
|
342
|
+
### tool_output_compression
|
|
343
|
+
|
|
344
|
+
Deterministic (no-LLM) compression of a tool's output **before it reaches the
|
|
345
|
+
model**, to spend fewer context tokens on high-volume, low-signal output. This is
|
|
346
|
+
distinct from [`compression`](#compression) (which summarises the *conversation
|
|
347
|
+
history* when the window fills) and from `display.tool_output_preview_lines`
|
|
348
|
+
(scrollback-only). It runs at a single seam — every tool's output passes through
|
|
349
|
+
`Agent::ToolExecutor` — so a content **router** picks the strategy by what the
|
|
350
|
+
output *is*, not by which tool produced it:
|
|
351
|
+
|
|
352
|
+
| Output detected as | Strategy | Effect |
|
|
353
|
+
| --- | --- | --- |
|
|
354
|
+
| test / build / lint / shell logs (rspec, pytest, jest, cargo, npm, make, generic) | `LogCompressor` | keep every error/failure + the summary tally + context, drop passing/info noise (≈97% fewer tokens on a failing suite) |
|
|
355
|
+
| a **whole-file** source read (Ruby) | code `skeleton` | keep signatures, elide large method bodies behind a `read offset:/limit:` pointer |
|
|
356
|
+
| a unified diff (`git diff`, `diff`) | `DiffCompressor` | keep every `+`/`-` line and every file/hunk header; trim far unchanged context to ±N lines; collapse a generated/lock file to a one-line summary. A small/tight diff (the "show me the diff" case) passes through **byte-identical** via the saving guard. The human view is the tool's separate scrollback diff (`body`), which is **never** compressed |
|
|
357
|
+
| a **whole-output** JSON dump (`curl \| jq`, `kubectl get -o json`, `gh api`, `docker inspect`, `aws --output json`, MCP/custom-tool JSON) | `JsonCompressor` | an array of **uniform** objects folds **losslessly** to a schema header + one compact row per item (repeated key names emitted once); a large array whose fold is too thin falls back to lossy row selection where **error-bearing rows and statistical outliers always survive** and dropped rows collapse to an `{"_elided": N}` sentinel; a single large object elides only **big string values** (never drops a key). Detected **before** the log channel, so a JSON shell dump folds as a table and is never log-compressed. Small JSON passes through **byte-identical** via the saving guard |
|
|
358
|
+
| grep / search results (`path:line:`) | passthrough | **byte-identical** |
|
|
359
|
+
| short output | passthrough | unchanged |
|
|
360
|
+
|
|
361
|
+
```yaml
|
|
362
|
+
tool_output_compression:
|
|
363
|
+
enabled: false # MASTER switch — off ships by default; the whole
|
|
364
|
+
# router is bypassed when false. `rubino setup`
|
|
365
|
+
# offers to turn this (and logs.enabled) on.
|
|
366
|
+
code: # whole-file source reads → skeleton
|
|
367
|
+
strategy: skeleton # only "skeleton" is implemented; any other value = passthrough
|
|
368
|
+
min_lines: 150 # files shorter than this are never skeletonised
|
|
369
|
+
keep_method_body_max_lines: 8 # bodies up to N lines are kept inline; larger ones are elided
|
|
370
|
+
languages: [ruby] # source languages to skeletonise (see note below); `rubino setup` lets you pick
|
|
371
|
+
logs:
|
|
372
|
+
enabled: false # sub-gate: log compression only runs when BOTH this and the master are on
|
|
373
|
+
min_lines: 40 # outputs shorter than this pass through unchanged
|
|
374
|
+
max_total_lines: 100 # cap on kept lines
|
|
375
|
+
max_errors: 10 # keep up to N errors/failures (first and last always kept)
|
|
376
|
+
max_warnings: 5
|
|
377
|
+
max_stack_traces: 3
|
|
378
|
+
context_lines: 4 # lines of surrounding context kept around each failure
|
|
379
|
+
diff: # unified diffs (git diff / diff) — model copy only
|
|
380
|
+
context_lines: 3 # unchanged context kept on each side of a change; far context → `… N unchanged lines`
|
|
381
|
+
min_lines: 40 # diffs shorter than this pass through unchanged ("show me the diff")
|
|
382
|
+
min_saving: 0.25 # only apply when ≥25% smaller; else byte-identical passthrough
|
|
383
|
+
generated_patterns: # changed files matching these collapse to a one-line summary
|
|
384
|
+
- "*.lock"
|
|
385
|
+
- Gemfile.lock
|
|
386
|
+
- package-lock.json
|
|
387
|
+
- yarn.lock
|
|
388
|
+
- pnpm-lock.yaml
|
|
389
|
+
- composer.lock
|
|
390
|
+
- "*.min.js"
|
|
391
|
+
- "*.min.css"
|
|
392
|
+
- dist/
|
|
393
|
+
- build/
|
|
394
|
+
- "*.snap"
|
|
395
|
+
- vendor/
|
|
396
|
+
json: # whole-output JSON dumps (kubectl/gh/docker/aws/jq)
|
|
397
|
+
min_items: 8 # arrays with fewer items (and < min_lines) pass through unchanged
|
|
398
|
+
min_lines: 40 # objects / text shorter than this pass through unchanged
|
|
399
|
+
min_saving: 0.25 # only apply when ≥25% smaller; else byte-identical passthrough
|
|
400
|
+
outlier_sigma: 3.0 # a numeric field > N σ from its column mean = a kept outlier row (lossy)
|
|
401
|
+
max_string_chars: 400 # in a single object, string values longer than this collapse to `<elided N chars>` (key kept)
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
> **`code.languages`** (default `["ruby"]`) selects which source languages get
|
|
405
|
+
> whole-file skeletonisation; a read whose language isn't listed passes through
|
|
406
|
+
> verbatim, so removing a language disables compression for it. Values: `ruby`
|
|
407
|
+
> (built-in Prism parser, always available), `python` (stdlib `ast` via your
|
|
408
|
+
> `python3` — a no-op if `python3` isn't on PATH), and `javascript` /
|
|
409
|
+
> `typescript` / `tsx` (need the optional `tree_sitter_language_pack` gem — a
|
|
410
|
+
> no-op until it's installed). `rubino setup` offers a language picker and, if you
|
|
411
|
+
> choose JS/TS, asks before installing the parser gem.
|
|
412
|
+
|
|
413
|
+
> `diff` and `json` have **no** own `enabled` sub-gate (like `code`): they are
|
|
414
|
+
> active whenever the master flag is on, and the saving guard (`min_lines`/
|
|
415
|
+
> `min_items` + `min_saving`) is the real gate — small/tight diffs and small JSON
|
|
416
|
+
> the user wants to see stay verbatim automatically.
|
|
417
|
+
|
|
418
|
+
**Reversibility.** When the router compresses, the executor spills the *full
|
|
419
|
+
original* to `<home>/tool-results/<call_id>.txt` and the compressed output ends
|
|
420
|
+
with a passive pointer carrying the call's **id** (`… N line(s) hidden …
|
|
421
|
+
retrieve_output id=<id> only if a hidden line is specifically needed`). The model
|
|
422
|
+
recovers the original by calling the **`retrieve_output`** tool with that id —
|
|
423
|
+
registered **only** while compression is enabled (so the default registry/tool
|
|
424
|
+
count is unchanged). The pointer deliberately prints **no cat-able filesystem
|
|
425
|
+
path**: recovery is an id behind a dedicated tool (headroom-style), so a small
|
|
426
|
+
model can't `sed`/`grep`/`cat` a printed spill path and re-inflate the output the
|
|
427
|
+
compressor just shrank. If the spill failed the pointer says *full output
|
|
428
|
+
unavailable (spill failed)* with no id. **Fidelity:** a failure or summary line
|
|
429
|
+
is never dropped; only passing/info noise is.
|
|
430
|
+
|
|
431
|
+
**Per-call opt-out.** When the feature is on, `read` and `shell` advertise a
|
|
432
|
+
`compress` boolean parameter (default `true`); the model can pass `compress:false`
|
|
433
|
+
to receive the verbatim output for that one call (returned byte-identical).
|
|
434
|
+
|
|
435
|
+
**Telemetry.** Compression events are logged as `compression.applied` /
|
|
436
|
+
`compression.drill_in` / `compression.failed`. `compression.drill_in` is emitted
|
|
437
|
+
on every `retrieve_output` call (a deliberate recovery, carrying the `id`) and on
|
|
438
|
+
a `read`'s targeted offset-read into an elided `:code` skeleton body — so the
|
|
439
|
+
counter measures real recoveries and is **not** bypassable by a shell `sed`/
|
|
440
|
+
`grep`/`cat` (there is no path to cat). A strategy error always falls back to the
|
|
441
|
+
uncompressed text, so compression can never break a tool call.
|
|
442
|
+
|
|
318
443
|
### terminal
|
|
319
444
|
|
|
320
445
|
```yaml
|
|
@@ -366,10 +491,18 @@ attachments:
|
|
|
366
491
|
inline_text_budget_bytes: 100000
|
|
367
492
|
allow_kinds: [image, text, document, archive, binary]
|
|
368
493
|
auto_extract_documents: false
|
|
369
|
-
aux_vision_egress: true
|
|
494
|
+
aux_vision_egress: true # allow the `vision` tool to send an image to an EXTERNAL aux model (data egress; see below)
|
|
370
495
|
archive: { max_entries: 2000, max_uncompressed_bytes: 268435456, max_entry_ratio: 100, max_total_ratio: 50, max_nesting_depth: 1 }
|
|
371
496
|
```
|
|
372
497
|
|
|
498
|
+
`aux_vision_egress` (default `true`) gates the **`vision` tool**: routing an
|
|
499
|
+
image to an external auxiliary vision model is data egress, so set it to `false`
|
|
500
|
+
to refuse — the tool then returns a clean error instead of sending the bytes
|
|
501
|
+
(#578). Independently, before any egress the tool **content-sniffs** the file
|
|
502
|
+
(magic bytes win over the extension, fail-closed): a path that isn't actually an
|
|
503
|
+
image is rejected, so a mislabelled or non-image file can't be smuggled to the
|
|
504
|
+
external host (#579).
|
|
505
|
+
|
|
373
506
|
### security
|
|
374
507
|
|
|
375
508
|
```yaml
|
|
@@ -537,8 +670,22 @@ api:
|
|
|
537
670
|
rate_limit_enabled: true
|
|
538
671
|
rate_limit_unauth_per_minute: 60
|
|
539
672
|
rate_limit_auth_per_minute: 600
|
|
673
|
+
allow_public_bind: false # gate for a non-loopback bind (see below)
|
|
540
674
|
```
|
|
541
675
|
|
|
676
|
+
`allow_public_bind` is **false by default (safe)**. The API can execute shell
|
|
677
|
+
tools, so binding it to a non-loopback address (`--host 0.0.0.0`,
|
|
678
|
+
`RUBINO_API_HOST` set to anything other than `127.0.0.1` / `::1` / `localhost`)
|
|
679
|
+
publishes a remote-code-execution surface to the network — and with TLS off the
|
|
680
|
+
bearer token and all traffic travel in cleartext. While this is `false`, the
|
|
681
|
+
server **refuses to boot** on a non-loopback host with an actionable error.
|
|
682
|
+
Loopback binds (the default) are unaffected and need no opt-in.
|
|
683
|
+
|
|
684
|
+
To deliberately expose the listener, set `allow_public_bind: true`. The server
|
|
685
|
+
then boots on the routable host but prints a one-time exposure **WARNING** at
|
|
686
|
+
startup. When you opt in, enable TLS (`RUBINO_TLS=1`) and a strong
|
|
687
|
+
`RUBINO_API_KEY`, and prefer a reverse proxy over a direct bind.
|
|
688
|
+
|
|
542
689
|
---
|
|
543
690
|
|
|
544
691
|
## Environment Variables
|
data/docs/mcp.md
CHANGED
|
@@ -44,10 +44,10 @@ mcp:
|
|
|
44
44
|
|
|
45
45
|
## How It Works
|
|
46
46
|
|
|
47
|
-
1. At chat boot (and in `rubino tools`), `MCP::Manager` connects to all configured servers — best-effort: a server that fails to start prints a warning and is skipped, it never blocks the session
|
|
47
|
+
1. At chat boot (and in `rubino tools`), `MCP::Manager` connects to all configured servers **in parallel** (#576), so one hanging server no longer serializes startup — best-effort: a server that fails to start prints a warning and is skipped, it never blocks the session
|
|
48
48
|
2. Each server's tools are wrapped in `MCPToolWrapper` (adapts to `Tools::Base` interface), forwarding the server-declared input schema so the model calls them with the right argument names
|
|
49
49
|
3. Wrapped tools are registered in `Tools::Registry` with a prefix (`servername_toolname`)
|
|
50
|
-
4. The agent can use MCP tools like any built-in tool; a failed MCP call (including a server-side argument rejection) surfaces as an `Error: …` tool result and renders ✗ like any failed built-in tool
|
|
50
|
+
4. The agent can use MCP tools like any built-in tool; a failed MCP call (including a server-side argument rejection) surfaces as an `Error: …` tool result and renders ✗ like any failed built-in tool. To keep external code visible, an MCP tool's **display label** is suffixed with its source — the live tool card and the approval card both show `<bare> (mcp:<server>)` (e.g. `echo (mcp:chaos)`), so you can tell at a glance that an out-of-process server is running (#582). The model-facing tool name is unchanged.
|
|
51
51
|
5. `ruby_llm-mcp`'s own log lines (including everything a stdio server prints on its stderr) go to `<home>/logs/mcp.log`, never to stdout — one-shot `rubino prompt` output stays machine-readable
|
|
52
52
|
|
|
53
53
|
MCP tools are dynamic — they come from whatever servers you configure — so they are not part of the drift-checked built-in tool list in [tools.md](tools.md) and have no `tools.<key>` config gate; disable a server (`/mcp <server> off` for the session, or set `mcp.enabled: false`) to remove its tools.
|
|
@@ -95,7 +95,7 @@ An `oauth` hash on a `streamable` server is forwarded verbatim to `ruby_llm-mcp`
|
|
|
95
95
|
/mcp reload # re-read config.yml and reconnect every server (no chat restart needed)
|
|
96
96
|
```
|
|
97
97
|
|
|
98
|
-
`off`/`on` are session-scoped — config is untouched. `/mcp reload` is how a server added to `config.yml` mid-session becomes usable. When servers are configured, `/status` includes an `mcp` line (`2 servers · 1 reachable · 14 tools`).
|
|
98
|
+
`/mcp`'s server list glyphs each server by health: green `●` **reachable**, yellow `⚠` **degraded** (#575 — the process is alive but a protocol call such as `tools/list` failed, so it's up but not fully serving), and a stopped/failed server shows its last start error in the drill-in. `off`/`on` are session-scoped — config is untouched. `/mcp reload` is how a server added to `config.yml` mid-session becomes usable. When servers are configured, `/status` includes an `mcp` line (`2 servers · 1 reachable · 14 tools`).
|
|
99
99
|
|
|
100
100
|
## Manual Management
|
|
101
101
|
|
data/docs/memory.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Memory
|
|
2
2
|
|
|
3
|
-
rubino remembers facts about you and the project across sessions. The default backend is a small SQLite
|
|
3
|
+
rubino remembers facts about you and the project across sessions. The default backend is a small SQLite fact store — an LLM-extracted, bi-temporal fact store with hybrid recall, minus the graph database, the server, and the multi-call pipeline.
|
|
4
4
|
|
|
5
5
|
## Backends
|
|
6
6
|
|
|
@@ -8,7 +8,7 @@ Memory backends are pluggable (registered like tools). Two ship:
|
|
|
8
8
|
|
|
9
9
|
| `memory.backend` | What it is |
|
|
10
10
|
|---|---|
|
|
11
|
-
| `sqlite` (**default**) |
|
|
11
|
+
| `sqlite` (**default**) | LLM-extracted atomic facts, bi-temporal supersession, hybrid FTS5/BM25 (+ optional vector) ranked recall, graph-lite 1-hop blend |
|
|
12
12
|
| `default` | the legacy non-ranked store (kept for back-compat) |
|
|
13
13
|
|
|
14
14
|
Switch backends:
|
|
@@ -20,7 +20,7 @@ rubino memory backend sqlite # switch (writes memory.backend to config.yml)
|
|
|
20
20
|
|
|
21
21
|
The agent loop, the in-chat `/memory` view, the `/status` panel, the `rubino memory` CLI, and the HTTP `/v1/memory` operations all use the **active** backend (fixed in #94/#106/#83 — these surfaces previously read a hardwired legacy table and never saw the facts the agent actually persists).
|
|
22
22
|
|
|
23
|
-
## The sqlite
|
|
23
|
+
## The sqlite memory backend
|
|
24
24
|
|
|
25
25
|
### What's stored
|
|
26
26
|
|
data/docs/security.md
CHANGED
|
@@ -145,7 +145,7 @@ The fake LLM provider can short-circuit tool decisions, so `chat` and `server` r
|
|
|
145
145
|
|
|
146
146
|
## TLS for the HTTP API
|
|
147
147
|
|
|
148
|
-
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:
|
|
148
|
+
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. Because the API can execute shell tools, a non-loopback bind is **config-gated and refused by default** (#577): the server will not boot on a routable host unless `api.allow_public_bind: true` is set in `config.yml`, and when it is, it prints a one-time exposure warning at startup. Loopback binds (`127.0.0.1` / `::1` / `localhost`) are unaffected. 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:
|
|
149
149
|
|
|
150
150
|
```bash
|
|
151
151
|
rubino tls-cert # prints $RUBINO_HOME/tls/cert.pem (generating it if absent)
|
data/docs/tools.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Tools Reference
|
|
2
2
|
|
|
3
|
-
rubino ships **
|
|
3
|
+
rubino ships **29 built-in tools** plus dynamic MCP tools (started at boot when `mcp.servers` is configured — see [mcp.md](mcp.md); being server-dependent they are excluded from the drift-checked list below) and custom user-defined tools. Each tool is gated by a `tools.<key>` config flag (opt-out: absent key = enabled, only an explicit `false` disables) and the approval model. The count and list below are drift-checked against the live registry by `spec/docs/tools_doc_drift_spec.rb`.
|
|
4
4
|
|
|
5
|
-
The full list (registration order): `read`, `summarize_file`, `write`, `edit`, `multi_edit`, `grep`, `glob`, `
|
|
5
|
+
The full list (registration order): `read`, `summarize_file`, `write`, `edit`, `multi_edit`, `grep`, `glob`, `shell`, `shell_output`, `shell_tail`, `shell_input`, `shell_kill`, `ruby`, `apply_patch`, `webfetch`, `websearch`, `question`, `todowrite`, `memory`, `session_search`, `attach_file`, `read_attachment`, `vision`, `skill`, `task`, `task_result`, `task_stop`, `steer`, `probe`.
|
|
6
6
|
|
|
7
|
-
Several tools share one config gate, so `rubino tools` shows **
|
|
7
|
+
Several tools share one config gate, so `rubino tools` shows **24 rows** (config groups), not 29: `webfetch` + `websearch` share `tools.web`, and the whole delegation family (`task`, `task_result`, `task_stop`, `steer`, `probe`) rides on `tools.task` — disabling delegation disables them all.
|
|
8
8
|
|
|
9
9
|
## How tools are gated
|
|
10
10
|
|
|
@@ -13,6 +13,27 @@ Several tools share one config gate, so `rubino tools` shows **27 rows** (config
|
|
|
13
13
|
- **Approval** — see [security.md](security.md). Shell commands are confirmation-gated by default; a non-bypassable hardline floor blocks catastrophic commands regardless of mode.
|
|
14
14
|
- **Workspace sandbox** — with `tools.workspace_strict: true` (default), write/edit/delete tools are confined to the workspace root (`terminal.cwd` or `Dir.pwd`).
|
|
15
15
|
|
|
16
|
+
## Output compression
|
|
17
|
+
|
|
18
|
+
When `tool_output_compression.enabled` is on (off by default; `rubino setup`
|
|
19
|
+
offers it), every tool's output passes through a deterministic content router
|
|
20
|
+
before it reaches the model: test/build/lint **logs** are reduced to their
|
|
21
|
+
failures + summary, a **whole-file source read** can come back as a skeleton, and
|
|
22
|
+
**diffs / grep results / JSON / short output pass through byte-identical**. The
|
|
23
|
+
full original is always recoverable: the compressed view ends with a passive
|
|
24
|
+
pointer carrying an `id` (`retrieve_output id=…`), and the model recovers the
|
|
25
|
+
verbatim original by calling the `retrieve_output` tool with that id — there is
|
|
26
|
+
**no cat-able filesystem path** in the pointer, so a small model can't `sed`/
|
|
27
|
+
`grep`/`cat` a spill path and re-inflate the very output compression just shrank.
|
|
28
|
+
While enabled, `read` and `shell` advertise an extra `compress` boolean parameter
|
|
29
|
+
(default `true`) so the model can pass `compress:false` to get one call's output
|
|
30
|
+
verbatim, and the registry adds the `retrieve_output` recovery tool (present
|
|
31
|
+
**only** while compression is enabled — it is absent from the default registry,
|
|
32
|
+
so the count below is unchanged). See
|
|
33
|
+
[configuration.md](configuration.md#tool_output_compression) for the full key
|
|
34
|
+
reference. Compression is OFF in the default registry, so the parameter lists
|
|
35
|
+
below describe the shipped (uncompressed) behaviour.
|
|
36
|
+
|
|
16
37
|
## Built-in Tools
|
|
17
38
|
|
|
18
39
|
### read
|
|
@@ -78,25 +99,6 @@ Risk: low
|
|
|
78
99
|
Parameters: pattern, path, max_results
|
|
79
100
|
```
|
|
80
101
|
|
|
81
|
-
### git
|
|
82
|
-
|
|
83
|
-
Git operations: status, diff, log, branch, show.
|
|
84
|
-
|
|
85
|
-
```
|
|
86
|
-
Risk: low (read-only operations)
|
|
87
|
-
Parameters: command, args
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
### github
|
|
91
|
-
|
|
92
|
-
GitHub integration: PRs, issues, reviews. Uses gh CLI or REST API.
|
|
93
|
-
|
|
94
|
-
```
|
|
95
|
-
Risk: medium
|
|
96
|
-
Parameters: action, title, body, number, repo, base, labels
|
|
97
|
-
Actions: pr_create, pr_list, pr_view, pr_checks, pr_diff, issue_create, issue_list, issue_view, repo_view, release_list
|
|
98
|
-
```
|
|
99
|
-
|
|
100
102
|
### shell
|
|
101
103
|
|
|
102
104
|
Execute a shell command. Foreground blocks until exit or `timeout`; pass `run_in_background: true` to fire-and-forget and get a `run_id`.
|
|
@@ -155,15 +157,6 @@ Risk: medium
|
|
|
155
157
|
Parameters: code
|
|
156
158
|
```
|
|
157
159
|
|
|
158
|
-
### run_tests
|
|
159
|
-
|
|
160
|
-
Run the workspace project's test suite and return a **structured** result instead of the raw toolchain firehose. Auto-detects RSpec / Minitest / a Rakefile default task, prefers `bundle exec` when a Gemfile is present (falls back to the bare runner if the bundle is broken), and returns pass/fail counts plus the failing examples (name + file:line + short message) and a short raw tail. Distinguishes "the suite couldn't start" (toolchain error) from "the suite ran and N failed". Use this instead of driving `shell` by hand to run tests. (issue #101)
|
|
161
|
-
|
|
162
|
-
```
|
|
163
|
-
Risk: low
|
|
164
|
-
Parameters: path (optional file/pattern), framework (optional: rspec|minitest|rake)
|
|
165
|
-
```
|
|
166
|
-
|
|
167
160
|
### apply_patch
|
|
168
161
|
|
|
169
162
|
Apply unified diff patches to files.
|
|
@@ -182,6 +175,27 @@ Risk: low
|
|
|
182
175
|
Parameters: url, format (text|html)
|
|
183
176
|
```
|
|
184
177
|
|
|
178
|
+
`format: "text"` (default) runs a readability-style **main-content extraction**
|
|
179
|
+
(nokogiri): page chrome — `script`, `style`, `noscript`, `nav`, `header`,
|
|
180
|
+
`footer`, `aside`, `form`, `svg`, `iframe`, `button`, plus ARIA landmark roles
|
|
181
|
+
(`navigation`, `banner`, `contentinfo`, `search`, `complementary`) — is dropped,
|
|
182
|
+
the main container is preferred (`<main>` → `[role=main]` → `<article>` →
|
|
183
|
+
`<body>`), and the kept subtree is serialized to markdown-ish text (`## `
|
|
184
|
+
headings, `- ` list items, blank-line-separated paragraphs, entities decoded).
|
|
185
|
+
This strips nav menus/footers/cookie banners and typically cuts tokens
|
|
186
|
+
substantially on article and docs pages.
|
|
187
|
+
|
|
188
|
+
Two guarantees so capability is never lost:
|
|
189
|
+
|
|
190
|
+
- **Safety fallback** — if the extracted text is under ~30% of the full page
|
|
191
|
+
text (or below a small char floor), the tool returns the full-page strip
|
|
192
|
+
instead, so a page whose content isn't in a clean `<main>`/`<article>` is never
|
|
193
|
+
over-trimmed. Malformed HTML that nokogiri can't parse also falls back (a fetch
|
|
194
|
+
never crashes). When extraction trims a lot, a one-line note points back at the
|
|
195
|
+
raw escape hatch.
|
|
196
|
+
- **Raw escape hatch** — `format: "html"` returns the full raw HTML **verbatim**,
|
|
197
|
+
completely unprocessed, for when the model wants the original page.
|
|
198
|
+
|
|
185
199
|
### websearch
|
|
186
200
|
|
|
187
201
|
Search the web. Supports Tavily (best), SearXNG, or DuckDuckGo fallback.
|
|
@@ -302,15 +316,6 @@ Risk: medium
|
|
|
302
316
|
Parameters: task_id
|
|
303
317
|
```
|
|
304
318
|
|
|
305
|
-
### ask_parent
|
|
306
|
-
|
|
307
|
-
Child→parent escalation: a subagent asks its parent a question it cannot resolve from its sealed prompt. `blocking: true` pauses the child until the answer arrives; `blocking: false` (default) lets it keep working and folds the answer in later as a note. The parent (agent or human) answers via `answer_child` / `/reply`. Only available to subagents — a top-level agent has no parent to ask. Gated by `tools.task`.
|
|
308
|
-
|
|
309
|
-
```
|
|
310
|
-
Risk: low
|
|
311
|
-
Parameters: question, blocking
|
|
312
|
-
```
|
|
313
|
-
|
|
314
319
|
### steer
|
|
315
320
|
|
|
316
321
|
Parent→child steering note: park a short note on one of YOUR OWN running subagents; it is folded into the child's context at its next turn boundary and persists (it changes the child's trajectory). Ownership-scoped at call time — only your direct children. The model counterpart of the human `/agents <id> steer "…"`. Gated by `tools.task`.
|
|
@@ -329,15 +334,6 @@ Risk: low
|
|
|
329
334
|
Parameters: task_id, question, live
|
|
330
335
|
```
|
|
331
336
|
|
|
332
|
-
### answer_child
|
|
333
|
-
|
|
334
|
-
Parent→child answer to an `ask_parent` question: delivers the answer into the asking child's context (unblocks a blocking ask; folds in for a non-blocking one). Ownership-scoped — only a direct child that is actually waiting. The model counterpart of the human `/reply <id> <answer>`. Gated by `tools.task`.
|
|
335
|
-
|
|
336
|
-
```
|
|
337
|
-
Risk: low
|
|
338
|
-
Parameters: task_id, answer
|
|
339
|
-
```
|
|
340
|
-
|
|
341
337
|
---
|
|
342
338
|
|
|
343
339
|
## MCP Tools
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Build step for the standalone `rubino-landlock` helper.
|
|
4
|
+
#
|
|
5
|
+
# This is NOT a Ruby C extension in the usual sense — the gem has no native
|
|
6
|
+
# Ruby bindings. We (ab)use the extension mechanism purely so RubyGems compiles
|
|
7
|
+
# a tiny standalone executable at install time: a Landlock-applying exec-wrapper
|
|
8
|
+
# the Linux sandbox path (Security::Sandbox) launches in front of `bash`.
|
|
9
|
+
#
|
|
10
|
+
# CRITICAL: this must DEGRADE GRACEFULLY. The gem must install on macOS, on
|
|
11
|
+
# Windows, and on a Linux kernel/toolchain without Landlock headers. So we never
|
|
12
|
+
# fail the build: on any non-Linux host, a missing compiler, or missing Landlock
|
|
13
|
+
# headers we write a no-op Makefile and exit 0. The sandbox then reports the
|
|
14
|
+
# Linux mechanism as unavailable and fails OPEN with a loud banner (by design).
|
|
15
|
+
require "mkmf"
|
|
16
|
+
|
|
17
|
+
# A Makefile that satisfies `make` / `make install` with no work, so
|
|
18
|
+
# `gem install` always succeeds even when we can't build the helper.
|
|
19
|
+
def write_noop_makefile(reason)
|
|
20
|
+
warn "rubino-landlock: skipping native helper build (#{reason}); " \
|
|
21
|
+
"the Linux OS write-sandbox will be unavailable (fail-open)."
|
|
22
|
+
File.write("Makefile", <<~MAKE)
|
|
23
|
+
all:
|
|
24
|
+
\t@true
|
|
25
|
+
install:
|
|
26
|
+
\t@true
|
|
27
|
+
clean:
|
|
28
|
+
\t@true
|
|
29
|
+
MAKE
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Only Linux has Landlock. Everywhere else the helper is irrelevant.
|
|
33
|
+
unless RUBY_PLATFORM.include?("linux")
|
|
34
|
+
write_noop_makefile("not Linux")
|
|
35
|
+
exit 0
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# The helper needs the Landlock UAPI header to know the struct/flag layout.
|
|
39
|
+
# (The syscall itself is invoked by number, so no libc wrapper is required.)
|
|
40
|
+
unless have_header("linux/landlock.h")
|
|
41
|
+
write_noop_makefile("linux/landlock.h not found")
|
|
42
|
+
exit 0
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# The compiled helper lands in the gem's exe/ dir as `rubino-landlock`, on PATH
|
|
46
|
+
# of the installed gem. extconf builds into the extension dir; we install it to
|
|
47
|
+
# the gem's bin via a custom rule appended after create_makefile.
|
|
48
|
+
target = "rubino-landlock"
|
|
49
|
+
|
|
50
|
+
# create_makefile expects a Ruby-loadable .so; we instead emit our own Makefile
|
|
51
|
+
# that compiles a freestanding executable. Bypass the .so machinery entirely.
|
|
52
|
+
cc = RbConfig::CONFIG["CC"] || "cc"
|
|
53
|
+
srcdir = __dir__
|
|
54
|
+
|
|
55
|
+
File.write("Makefile", <<~MAKE)
|
|
56
|
+
CC = #{cc}
|
|
57
|
+
TARGET = #{target}
|
|
58
|
+
SRC = #{File.join(srcdir, "landlock.c")}
|
|
59
|
+
|
|
60
|
+
all: $(TARGET)
|
|
61
|
+
|
|
62
|
+
$(TARGET): $(SRC)
|
|
63
|
+
\t$(CC) -O2 -Wall -o $(TARGET) $(SRC)
|
|
64
|
+
|
|
65
|
+
# `gem install` runs `make` then `make install`; RubyGems copies any built
|
|
66
|
+
# artifact reported in the extension dir. We additionally drop the binary into
|
|
67
|
+
# the gem's exe/ so Security::Sandbox can resolve it next to the `rubino` exe.
|
|
68
|
+
install: all
|
|
69
|
+
\t@true
|
|
70
|
+
|
|
71
|
+
clean:
|
|
72
|
+
\t-rm -f $(TARGET)
|
|
73
|
+
MAKE
|
|
74
|
+
|
|
75
|
+
# If the compile would fail (no working cc), we still don't want to break the
|
|
76
|
+
# install — but mkmf already verified a compiler via have_header above. Leave
|
|
77
|
+
# the real compile to `make`; a failure there is rare and caught by Sandbox's
|
|
78
|
+
# runtime probe (binary absent ⇒ fail-open).
|