Skip to content

feat: ShellDispatcher, ExternalTool wrappers, and pluggable tool registry#1848

Open
aboimpinto wants to merge 27 commits into
Hmbown:mainfrom
aboimpinto:feat/pluggable-tool-registry-pr
Open

feat: ShellDispatcher, ExternalTool wrappers, and pluggable tool registry#1848
aboimpinto wants to merge 27 commits into
Hmbown:mainfrom
aboimpinto:feat/pluggable-tool-registry-pr

Conversation

@aboimpinto
Copy link
Copy Markdown
Contributor

@aboimpinto aboimpinto commented May 20, 2026

This PR folds the previously stacked work into a single PR so it can be reviewed and merged as one coherent change.

Supersedes #2008 and #2031.

Summary

  • Adds ShellDispatcher for shell-agnostic command execution.
  • Adds ExternalTool wrappers for git, gh, and rustc command execution.
  • Adds pluggable ToolRegistry support for runtime tool registration, config.toml overrides, and user script tools.

Implementation notes

No [tools] config required

The default ~/.deepseek/tools/ directory is always scanned. A user can drop a script there with frontmatter and it becomes available as a plugin tool. The [tools] table in config.toml is only needed for custom plugin directory paths, built-in tool overrides, or disabling tools.

Script tool flow

  1. A script such as ~/.deepseek/tools/count-words.mjs declares metadata in comment frontmatter: name, description, JSON schema, and approval behavior.
  2. On session start, the engine scans the tool directory, reads frontmatter, and registers a ScriptPluginTool in the ToolRegistry.
  3. Plugin tools are non-deferred, so the model can see them immediately.
  4. When called, the engine spawns the script, writes JSON input to stdin, reads a ToolResult JSON object from stdout, and returns that result to the model.

Key design decisions

  • stdin/stdout protocol: JSON input in, ToolResult JSON out.
  • Comment frontmatter: supports #, //, and -- prefixes so scripts can be written in multiple languages.
  • Plugin tools are immediately visible to the model.
  • Shebang resolution handles #!/usr/bin/env node by resolving node directly, which works better on Windows.

Paulo Aboim Pinto

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a plugin tool system that allows users to register custom scripts and commands as model-visible tools, alongside a new .NET execution runtime. It also adds a unified ExternalTool trait for subprocess management and a ShellDispatcher to handle cross-platform shell execution safely. Several debugging logs were identified in the TUI rendering and event loop logic that should be removed, and a potential bug in script argument handling in the plugin system was highlighted.

Comment thread crates/tui/src/tools/plugin.rs
Comment thread crates/tui/src/tools/plugin.rs
Comment thread crates/tui/src/tools/plugin.rs
Comment thread crates/tui/src/tui/ui.rs
Comment thread crates/tui/src/tui/ui.rs
Comment thread crates/tui/src/tui/ui.rs Outdated
Comment thread crates/tui/src/tui/ui.rs Outdated
Comment thread crates/tui/src/tui/ui.rs
Comment thread crates/tui/src/tui/ui.rs Outdated
Comment thread crates/tui/src/tui/ui.rs
@aboimpinto aboimpinto force-pushed the feat/pluggable-tool-registry-pr branch 6 times, most recently from 329b648 to 3bb6a6a Compare May 20, 2026 22:57
@Hmbown Hmbown added this to the v0.8.46 milestone May 21, 2026
@aboimpinto aboimpinto force-pushed the feat/pluggable-tool-registry-pr branch from 3bb6a6a to 4fc533a Compare May 24, 2026 21:44
@aboimpinto
Copy link
Copy Markdown
Contributor Author

Stacked on top of #2008 (ShellDispatcher) + ExternalTool wrappers. All 4 commits applied cleanly with conflict resolution. The full stack: main ← #2008 ← ExternalTool ← #1848.

@aboimpinto aboimpinto changed the title feat: Pluggable Tool Registry -- runtime registration, config.toml overrides, and plugin tools (#1847) feat: ShellDispatcher, ExternalTool wrappers, and pluggable tool registry May 24, 2026
aboimpinto added 16 commits May 25, 2026 00:22
Introduces a central shell abstraction that replaces hardcoded
Command::new("cmd") / Command::new("sh") across the execution path.

- Shell detection at startup (pwsh -> powershell -> cmd -> sh)
- Correct quoting per shell (PowerShell uses -NoProfile -Command)
- Scope guards restore crossterm raw mode on all spawn paths (Hmbown#1690)
- Direct program+args builder for sandbox ExecEnv integration

Files:
- crates/tui/src/shell_dispatcher.rs (new, 12 tests)
- crates/tui/src/main.rs (register module)
- crates/tui/src/eval.rs (exec_shell delegates to dispatcher)
- crates/tui/src/sandbox/mod.rs (CommandSpec::shell uses dispatcher)
- crates/tui/src/tools/shell.rs (raw mode guards on all spawn paths)

Closes Hmbown#1690
Refs Hmbown#1779
… logging

- find_exe(): fall back to known install dirs when PATH lookup fails
  (C:\Program Files\PowerShell\7, System32\WindowsPowerShell\v1.0)
- Encoding prefix: use idiomatic [Console]::OutputEncoding for PowerShell
  instead of cmd.exe chcp 65001 >NUL
- Execution logging: write exec via <ShellKind> entries to
  SHELL_DISPATCHER_LOG when set
- System prompt: use ShellDispatcher detection instead of raw $SHELL
  so model knows it has PowerShell and generates native cmdlets
- display_command(): strip PowerShell encoding prefix for display
- Add tests for find_exe PATH + known-dir fallback

Refs Hmbown#1779
- Fixed sandbox tests to accept sh/bash/zsh on non-Windows
- Added cfg_shell_program() helper that uses ShellDispatcher detection
- Marked find_exe_finds_cmd_on_path as #[cfg(windows)]
- Fixed shell::tests for issue_1691 to accept sh/bash/zsh
- Added #[cfg(windows)] to cmd-specific tests
ShellDispatcher may detect pwsh, powershell, or cmd on Windows.
Tests now verify the command is passed as the last arg instead of
hardcoding cmd.exe with chcp prefix.
…tection

Gh, RustC, Cargo, DotNet, and Go resolve() implementations hardcoded a single binary name instead of iterating over Self::candidates(). This broke .cmd detection on Windows for npm/wrapper-installed tools. Aligned all resolve() implementations to the pattern used by Git and TypeScript: iterate over candidates, log the resolved candidate name, and return None only if all probes fail.

Only Go is a new tool in this PR; the other four are pre-existing but flagged in code review as the same anti-pattern. Fixed them in a single pass since the pattern is identical.
…lerplate

DOTNET and RuntimeTool tool execution dispatch blocks had identical started_at -> await -> ToolCallComplete -> ToolExecOutcome -> continue patterns. Extracted into Engine::emit_tool_outcome() to reduce code duplication. Responds to code review feedback.
…asks.rs

Both call sites used tokio::task::spawn_blocking just to run Gh::command() or Git::command(). Switched to Gh::tokio_command() and Git::tokio_command() respectively — these return tokio::process::Command so the output can be awaited directly without spawn_blocking. Pre-existing code improved per boy-scout rule (leave the place better than found it). Responds to code review feedback on PR Hmbown#1845.
@aboimpinto aboimpinto force-pushed the feat/pluggable-tool-registry-pr branch from 4fc533a to 6adc99e Compare May 24, 2026 22:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants