Merge remote-tracking branch 'origin/rcc/plugins' into integration/dori-cleanroom

# Conflicts:
#	rust/crates/commands/src/lib.rs
#	rust/crates/rusty-claude-cli/src/main.rs
This commit is contained in:
YeonGyu-Kim
2026-04-01 19:13:53 +09:00
4 changed files with 2277 additions and 470 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -2578,6 +2578,9 @@ mod tests {
}, },
); );
manager.store_registry(&registry).expect("store registry"); manager.store_registry(&registry).expect("store registry");
manager
.write_enabled_state("stale@bundled", Some(true))
.expect("seed bundled enabled state");
let installed = manager let installed = manager
.list_installed_plugins() .list_installed_plugins()
@@ -2635,6 +2638,9 @@ mod tests {
}, },
); );
manager.store_registry(&registry).expect("store registry"); manager.store_registry(&registry).expect("store registry");
manager
.write_enabled_state("stale-external@external", Some(true))
.expect("seed stale external enabled state");
let installed = manager let installed = manager
.list_installed_plugins() .list_installed_plugins()

File diff suppressed because it is too large Load Diff

View File

@@ -8,6 +8,7 @@ use api::{
MessageRequest, MessageResponse, OutputContentBlock, ProviderClient, MessageRequest, MessageResponse, OutputContentBlock, ProviderClient,
StreamEvent as ApiStreamEvent, ToolChoice, ToolDefinition, ToolResultContentBlock, StreamEvent as ApiStreamEvent, ToolChoice, ToolDefinition, ToolResultContentBlock,
}; };
use plugins::PluginTool;
use reqwest::blocking::Client; use reqwest::blocking::Client;
use runtime::{ use runtime::{
edit_file, execute_bash, glob_search, grep_search, load_system_prompt, read_file, write_file, edit_file, execute_bash, glob_search, grep_search, load_system_prompt, read_file, write_file,
@@ -55,6 +56,161 @@ pub struct ToolSpec {
pub required_permission: PermissionMode, pub required_permission: PermissionMode,
} }
#[derive(Debug, Clone, PartialEq)]
pub struct GlobalToolRegistry {
plugin_tools: Vec<PluginTool>,
}
impl GlobalToolRegistry {
#[must_use]
pub fn builtin() -> Self {
Self {
plugin_tools: Vec::new(),
}
}
pub fn with_plugin_tools(plugin_tools: Vec<PluginTool>) -> Result<Self, String> {
let builtin_names = mvp_tool_specs()
.into_iter()
.map(|spec| spec.name.to_string())
.collect::<BTreeSet<_>>();
let mut seen_plugin_names = BTreeSet::new();
for tool in &plugin_tools {
let name = tool.definition().name.clone();
if builtin_names.contains(&name) {
return Err(format!(
"plugin tool `{name}` conflicts with a built-in tool name"
));
}
if !seen_plugin_names.insert(name.clone()) {
return Err(format!("duplicate plugin tool name `{name}`"));
}
}
Ok(Self { plugin_tools })
}
pub fn normalize_allowed_tools(&self, values: &[String]) -> Result<Option<BTreeSet<String>>, String> {
if values.is_empty() {
return Ok(None);
}
let builtin_specs = mvp_tool_specs();
let canonical_names = builtin_specs
.iter()
.map(|spec| spec.name.to_string())
.chain(self.plugin_tools.iter().map(|tool| tool.definition().name.clone()))
.collect::<Vec<_>>();
let mut name_map = canonical_names
.iter()
.map(|name| (normalize_tool_name(name), name.clone()))
.collect::<BTreeMap<_, _>>();
for (alias, canonical) in [
("read", "read_file"),
("write", "write_file"),
("edit", "edit_file"),
("glob", "glob_search"),
("grep", "grep_search"),
] {
name_map.insert(alias.to_string(), canonical.to_string());
}
let mut allowed = BTreeSet::new();
for value in values {
for token in value
.split(|ch: char| ch == ',' || ch.is_whitespace())
.filter(|token| !token.is_empty())
{
let normalized = normalize_tool_name(token);
let canonical = name_map.get(&normalized).ok_or_else(|| {
format!(
"unsupported tool in --allowedTools: {token} (expected one of: {})",
canonical_names.join(", ")
)
})?;
allowed.insert(canonical.clone());
}
}
Ok(Some(allowed))
}
#[must_use]
pub fn definitions(&self, allowed_tools: Option<&BTreeSet<String>>) -> Vec<ToolDefinition> {
let builtin = mvp_tool_specs()
.into_iter()
.filter(|spec| allowed_tools.is_none_or(|allowed| allowed.contains(spec.name)))
.map(|spec| ToolDefinition {
name: spec.name.to_string(),
description: Some(spec.description.to_string()),
input_schema: spec.input_schema,
});
let plugin = self
.plugin_tools
.iter()
.filter(|tool| {
allowed_tools.is_none_or(|allowed| allowed.contains(tool.definition().name.as_str()))
})
.map(|tool| ToolDefinition {
name: tool.definition().name.clone(),
description: tool.definition().description.clone(),
input_schema: tool.definition().input_schema.clone(),
});
builtin.chain(plugin).collect()
}
#[must_use]
pub fn permission_specs(
&self,
allowed_tools: Option<&BTreeSet<String>>,
) -> Vec<(String, PermissionMode)> {
let builtin = mvp_tool_specs()
.into_iter()
.filter(|spec| allowed_tools.is_none_or(|allowed| allowed.contains(spec.name)))
.map(|spec| (spec.name.to_string(), spec.required_permission));
let plugin = self
.plugin_tools
.iter()
.filter(|tool| {
allowed_tools.is_none_or(|allowed| allowed.contains(tool.definition().name.as_str()))
})
.map(|tool| {
(
tool.definition().name.clone(),
permission_mode_from_plugin(tool.required_permission()),
)
});
builtin.chain(plugin).collect()
}
pub fn execute(&self, name: &str, input: &Value) -> Result<String, String> {
if mvp_tool_specs().iter().any(|spec| spec.name == name) {
return execute_tool(name, input);
}
self.plugin_tools
.iter()
.find(|tool| tool.definition().name == name)
.ok_or_else(|| format!("unsupported tool: {name}"))?
.execute(input)
.map_err(|error| error.to_string())
}
}
fn normalize_tool_name(value: &str) -> String {
value.trim().replace('-', "_").to_ascii_lowercase()
}
fn permission_mode_from_plugin(value: &str) -> PermissionMode {
match value {
"read-only" => PermissionMode::ReadOnly,
"workspace-write" => PermissionMode::WorkspaceWrite,
"danger-full-access" => PermissionMode::DangerFullAccess,
other => panic!("unsupported plugin permission: {other}"),
}
}
#[must_use] #[must_use]
#[allow(clippy::too_many_lines)] #[allow(clippy::too_many_lines)]
pub fn mvp_tool_specs() -> Vec<ToolSpec> { pub fn mvp_tool_specs() -> Vec<ToolSpec> {