Skip to content

feat: plugin system — auto-discover and execute custom tools#16

Merged
kienbui1995 merged 1 commit intomainfrom
feat/plugin-system
Apr 11, 2026
Merged

feat: plugin system — auto-discover and execute custom tools#16
kienbui1995 merged 1 commit intomainfrom
feat/plugin-system

Conversation

@kienbui1995
Copy link
Copy Markdown
Owner

@kienbui1995 kienbui1995 commented Apr 11, 2026

What

Plugin system that auto-discovers scripts in .magic-code/tools/ and registers them as LLM-callable tools.

Supported languages

  • Bash (.sh)
  • Python (.py)
  • JavaScript (.js) — new

How it works

  1. Place scripts in .magic-code/tools/
  2. Scripts are auto-discovered on startup
  3. Registered as plugin_<filename> tools
  4. LLM calls them like any other tool
  5. Input passed via PLUGIN_INPUT env var
  6. First comment line used as tool description

Tested

  • plugin_hello (bash): ✅
  • plugin_timestamp (python): ✅
  • plugin_upper (js): ✅

152 tests pass.

Summary by CodeRabbit

  • New Features
    • Added JavaScript plugin support alongside existing Python and shell scripts
    • Plugin discovery and registration now occur automatically when initializing a workspace
    • Script execution automatically selects the appropriate interpreter based on file type

Plugins are scripts in .magic-code/tools/ (sh, py, js).
Auto-discovered on startup, registered as plugin_<name> tools.
LLM can call them like any other tool.

- Added JS support (node) alongside existing sh/py
- Plugin specs included in tool_specs sent to LLM
- Scripts receive input via PLUGIN_INPUT env var
- First comment line used as tool description
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 11, 2026

📝 Walkthrough

Walkthrough

The plugin system now supports JavaScript scripts (.js files) in addition to existing .sh and .py formats. Script execution automatically selects the appropriate interpreter based on file extension: node for JavaScript, python3 for Python, and sh as default. The ToolRegistry integrates plugin discovery by populating its new plugin_specs field during initialization.

Changes

Cohort / File(s) Summary
Plugin Discovery and Execution
mc/crates/mc-tools/src/plugin.rs
Extended plugin discovery to accept .js files. Updated script execution to select interpreters based on file extension (python3 for .py, node for .js, sh for others). Script lookup now searches for {name}.js alongside existing extensions.
Registry Integration
mc/crates/mc-tools/src/registry.rs
Added plugin_specs: Vec<ToolSpec> field to ToolRegistry. Initialize plugins via discover_plugins() when constructing with workspace root. Updated all_specs() to include plugin specs in returned tool specifications.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 Aha! A treasure chest of .js files to find,
With node as the interpreter, perfectly aligned,
Alongside the .py and .sh of old,
The plugin registry now holds something bold,
JavaScript joins the magic, bright and true!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main feature added: a plugin system that auto-discovers and executes custom tools, which aligns directly with the core changes in both plugin.rs and registry.rs.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/plugin-system

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
mc/crates/mc-tools/src/plugin.rs (1)

83-89: ⚠️ Potential issue | 🔴 Critical

Block path traversal in plugin script resolution.

name is interpolated directly into a filesystem path. A crafted tool name like plugin_../../tmp/pwn can escape .magic-code/tools and execute unintended scripts.

🔒 Proposed hardening
 fn find_plugin_script(workspace: &Path, name: &str) -> Option<PathBuf> {
     let dir = workspace.join(".magic-code/tools");
+    if name.contains('/') || name.contains('\\') || name.contains("..") {
+        return None;
+    }
+    let dir_canon = dir.canonicalize().ok()?;
     for ext in ["sh", "py", "js"] {
         let path = dir.join(format!("{name}.{ext}"));
-        if path.exists() {
-            return Some(path);
+        if let Ok(path_canon) = path.canonicalize() {
+            if path_canon.starts_with(&dir_canon) && path_canon.is_file() {
+                return Some(path_canon);
+            }
         }
     }
     None
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mc/crates/mc-tools/src/plugin.rs` around lines 83 - 89, The plugin lookup
currently interpolates an unvalidated tool name into a filesystem path in
find_plugin_script, allowing path traversal; validate/sanitize the name before
constructing dir.join by rejecting any names that contain path separators, dots,
or ".." (or better, require a strict whitelist like /^[A-Za-z0-9_-]+$/) and
return None on invalid input so only safe filenames are checked in the ext loop
(the validation should occur at the start of find_plugin_script, referencing the
name parameter and the same dir/.magic-code/tools resolution).
🧹 Nitpick comments (1)
mc/crates/mc-tools/src/registry.rs (1)

109-114: Consider guarding tool-spec growth from plugins.

Because all_specs() feeds prompt/tool-schema construction (mc/crates/mc-core/src/runtime.rs:180-220), many plugins or long descriptions can consume context budget quickly. Consider a cap or truncation strategy for plugin descriptions/spec count.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mc/crates/mc-tools/src/registry.rs` around lines 109 - 114, The all_specs()
accumulator (cached_specs.get_or_init -> all_tool_specs, mcp_tool_specs,
plugin_specs) can grow unbounded and inflate prompt/schema context; modify
all_specs (or the cached_specs initializer) to enforce a configurable cap and/or
truncate plugin_specs descriptions: limit the total number of ToolSpec entries
merged (e.g., MAX_TOOL_SPECS) and for entries from plugin_specs trim long
description fields to a max length (e.g., MAX_DESCRIPTION_CHARS) or replace
excess text with an ellipsis, and make both limits configurable constants so
downstream code in runtime.rs receives a bounded spec list.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@mc/crates/mc-tools/src/registry.rs`:
- Around line 54-59: with_workspace_root updates self.plugin_specs but does not
clear the cached aggregate used by all_specs(), causing newly discovered plugins
to remain invisible; after assigning self.plugin_specs in with_workspace_root
(and before returning self), invalidate or clear the cache (e.g. set
self.cached_specs to None or an empty value) so all_specs() will rebuild with
the new plugin_specs on next access.

---

Outside diff comments:
In `@mc/crates/mc-tools/src/plugin.rs`:
- Around line 83-89: The plugin lookup currently interpolates an unvalidated
tool name into a filesystem path in find_plugin_script, allowing path traversal;
validate/sanitize the name before constructing dir.join by rejecting any names
that contain path separators, dots, or ".." (or better, require a strict
whitelist like /^[A-Za-z0-9_-]+$/) and return None on invalid input so only safe
filenames are checked in the ext loop (the validation should occur at the start
of find_plugin_script, referencing the name parameter and the same
dir/.magic-code/tools resolution).

---

Nitpick comments:
In `@mc/crates/mc-tools/src/registry.rs`:
- Around line 109-114: The all_specs() accumulator (cached_specs.get_or_init ->
all_tool_specs, mcp_tool_specs, plugin_specs) can grow unbounded and inflate
prompt/schema context; modify all_specs (or the cached_specs initializer) to
enforce a configurable cap and/or truncate plugin_specs descriptions: limit the
total number of ToolSpec entries merged (e.g., MAX_TOOL_SPECS) and for entries
from plugin_specs trim long description fields to a max length (e.g.,
MAX_DESCRIPTION_CHARS) or replace excess text with an ellipsis, and make both
limits configurable constants so downstream code in runtime.rs receives a
bounded spec list.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7f182ec2-b5dd-47cb-94b0-63da78ba3f12

📥 Commits

Reviewing files that changed from the base of the PR and between 64a0298 and 627a1cd.

📒 Files selected for processing (2)
  • mc/crates/mc-tools/src/plugin.rs
  • mc/crates/mc-tools/src/registry.rs

Comment on lines +54 to 59
self.plugin_specs = crate::plugin::discover_plugins(&root);
if !self.plugin_specs.is_empty() {
tracing::info!(count = self.plugin_specs.len(), "plugins discovered");
}
self.sandbox = Some(Sandbox::new(root));
self
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Invalidate cached_specs after plugin discovery.

with_workspace_root mutates plugin_specs but does not reset cached_specs. If all_specs() was initialized earlier, plugin tools never become visible from this registry instance.

🩹 Proposed fix
 pub fn with_workspace_root(mut self, root: PathBuf) -> Self {
     self.plugin_specs = crate::plugin::discover_plugins(&root);
     if !self.plugin_specs.is_empty() {
         tracing::info!(count = self.plugin_specs.len(), "plugins discovered");
     }
+    self.cached_specs = std::sync::OnceLock::new(); // invalidate cache
     self.sandbox = Some(Sandbox::new(root));
     self
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
self.plugin_specs = crate::plugin::discover_plugins(&root);
if !self.plugin_specs.is_empty() {
tracing::info!(count = self.plugin_specs.len(), "plugins discovered");
}
self.sandbox = Some(Sandbox::new(root));
self
self.plugin_specs = crate::plugin::discover_plugins(&root);
if !self.plugin_specs.is_empty() {
tracing::info!(count = self.plugin_specs.len(), "plugins discovered");
}
self.cached_specs = std::sync::OnceLock::new(); // invalidate cache
self.sandbox = Some(Sandbox::new(root));
self
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mc/crates/mc-tools/src/registry.rs` around lines 54 - 59, with_workspace_root
updates self.plugin_specs but does not clear the cached aggregate used by
all_specs(), causing newly discovered plugins to remain invisible; after
assigning self.plugin_specs in with_workspace_root (and before returning self),
invalidate or clear the cache (e.g. set self.cached_specs to None or an empty
value) so all_specs() will rebuild with the new plugin_specs on next access.

@kienbui1995 kienbui1995 merged commit 5e56044 into main Apr 11, 2026
9 checks passed
@kienbui1995 kienbui1995 deleted the feat/plugin-system branch April 11, 2026 09:25
@kienbui1995 kienbui1995 mentioned this pull request Apr 11, 2026
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.

1 participant