Просмотр исходного кода

Allow subagent tool flows to reach plugin-provided tools

The subagent runtime still advertised and executed only built-in tools, which left plugin-provided tools outside the Agent execution path. This change loads the same plugin-aware registry used by the CLI for subagent tool definitions, permission policy, and execution lookup so delegated runs can resolve plugin tools consistently.

Constraint: Plugin tools must respect the existing runtime plugin config and enabled-plugin state

Rejected: Thread plugin-specific exceptions through execute_tool directly | would bypass registry validation and duplicate lookup rules

Confidence: medium

Scope-risk: moderate

Reversibility: clean

Directive: Keep CLI and subagent registry construction aligned when plugin tool loading rules change

Tested: cargo test -p tools -p claw-cli

Not-tested: Live Anthropic subagent runs invoking plugin tools end-to-end
Yeachan-Heo 2 месяцев назад
Родитель
Сommit
aea2adb9c8
1 измененных файлов с 133 добавлено и 33 удалено
  1. 133 33
      rust/crates/tools/src/lib.rs

+ 133 - 33
rust/crates/tools/src/lib.rs

@@ -8,13 +8,13 @@ use api::{
     MessageRequest, MessageResponse, OutputContentBlock, StreamEvent as ApiStreamEvent, ToolChoice,
     MessageRequest, MessageResponse, OutputContentBlock, StreamEvent as ApiStreamEvent, ToolChoice,
     ToolDefinition, ToolResultContentBlock,
     ToolDefinition, ToolResultContentBlock,
 };
 };
-use plugins::PluginTool;
+use plugins::{PluginManager, PluginManagerConfig, 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,
-    ApiClient, ApiRequest, AssistantEvent, BashCommandInput, ContentBlock, ConversationMessage,
-    ConversationRuntime, GrepSearchInput, MessageRole, PermissionMode, PermissionPolicy,
-    RuntimeError, Session, TokenUsage, ToolError, ToolExecutor,
+    ApiClient, ApiRequest, AssistantEvent, BashCommandInput, ConfigLoader, ContentBlock,
+    ConversationMessage, ConversationRuntime, GrepSearchInput, MessageRole, PermissionMode,
+    PermissionPolicy, RuntimeConfig, RuntimeError, Session, TokenUsage, ToolError, ToolExecutor,
 };
 };
 use serde::{Deserialize, Serialize};
 use serde::{Deserialize, Serialize};
 use serde_json::{json, Value};
 use serde_json::{json, Value};
@@ -1700,13 +1700,15 @@ fn build_agent_runtime(
         .clone()
         .clone()
         .unwrap_or_else(|| DEFAULT_AGENT_MODEL.to_string());
         .unwrap_or_else(|| DEFAULT_AGENT_MODEL.to_string());
     let allowed_tools = job.allowed_tools.clone();
     let allowed_tools = job.allowed_tools.clone();
-    let api_client = AnthropicRuntimeClient::new(model, allowed_tools.clone())?;
-    let tool_executor = SubagentToolExecutor::new(allowed_tools);
+    let tool_registry = current_tool_registry()?;
+    let api_client =
+        AnthropicRuntimeClient::new(model, allowed_tools.clone(), tool_registry.clone())?;
+    let tool_executor = SubagentToolExecutor::new(allowed_tools, tool_registry.clone());
     Ok(ConversationRuntime::new(
     Ok(ConversationRuntime::new(
         Session::new(),
         Session::new(),
         api_client,
         api_client,
         tool_executor,
         tool_executor,
-        agent_permission_policy(),
+        agent_permission_policy(&tool_registry),
         job.system_prompt.clone(),
         job.system_prompt.clone(),
     ))
     ))
 }
 }
@@ -1815,10 +1817,12 @@ fn allowed_tools_for_subagent(subagent_type: &str) -> BTreeSet<String> {
     tools.into_iter().map(str::to_string).collect()
     tools.into_iter().map(str::to_string).collect()
 }
 }
 
 
-fn agent_permission_policy() -> PermissionPolicy {
-    mvp_tool_specs().into_iter().fold(
+fn agent_permission_policy(tool_registry: &GlobalToolRegistry) -> PermissionPolicy {
+    tool_registry.permission_specs(None).into_iter().fold(
         PermissionPolicy::new(PermissionMode::DangerFullAccess),
         PermissionPolicy::new(PermissionMode::DangerFullAccess),
-        |policy, spec| policy.with_tool_requirement(spec.name, spec.required_permission),
+        |policy, (name, required_permission)| {
+            policy.with_tool_requirement(name, required_permission)
+        },
     )
     )
 }
 }
 
 
@@ -1874,10 +1878,15 @@ struct AnthropicRuntimeClient {
     client: AnthropicClient,
     client: AnthropicClient,
     model: String,
     model: String,
     allowed_tools: BTreeSet<String>,
     allowed_tools: BTreeSet<String>,
+    tool_registry: GlobalToolRegistry,
 }
 }
 
 
 impl AnthropicRuntimeClient {
 impl AnthropicRuntimeClient {
-    fn new(model: String, allowed_tools: BTreeSet<String>) -> Result<Self, String> {
+    fn new(
+        model: String,
+        allowed_tools: BTreeSet<String>,
+        tool_registry: GlobalToolRegistry,
+    ) -> Result<Self, String> {
         let client = AnthropicClient::from_env()
         let client = AnthropicClient::from_env()
             .map_err(|error| error.to_string())?
             .map_err(|error| error.to_string())?
             .with_base_url(read_base_url());
             .with_base_url(read_base_url());
@@ -1886,20 +1895,14 @@ impl AnthropicRuntimeClient {
             client,
             client,
             model,
             model,
             allowed_tools,
             allowed_tools,
+            tool_registry,
         })
         })
     }
     }
 }
 }
 
 
 impl ApiClient for AnthropicRuntimeClient {
 impl ApiClient for AnthropicRuntimeClient {
     fn stream(&mut self, request: ApiRequest) -> Result<Vec<AssistantEvent>, RuntimeError> {
     fn stream(&mut self, request: ApiRequest) -> Result<Vec<AssistantEvent>, RuntimeError> {
-        let tools = tool_specs_for_allowed_tools(Some(&self.allowed_tools))
-            .into_iter()
-            .map(|spec| ToolDefinition {
-                name: spec.name.to_string(),
-                description: Some(spec.description.to_string()),
-                input_schema: spec.input_schema,
-            })
-            .collect::<Vec<_>>();
+        let tools = self.tool_registry.definitions(Some(&self.allowed_tools));
         let message_request = MessageRequest {
         let message_request = MessageRequest {
             model: self.model.clone(),
             model: self.model.clone(),
             max_tokens: 32_000,
             max_tokens: 32_000,
@@ -2002,32 +2005,82 @@ impl ApiClient for AnthropicRuntimeClient {
 
 
 struct SubagentToolExecutor {
 struct SubagentToolExecutor {
     allowed_tools: BTreeSet<String>,
     allowed_tools: BTreeSet<String>,
+    tool_registry: GlobalToolRegistry,
 }
 }
 
 
 impl SubagentToolExecutor {
 impl SubagentToolExecutor {
-    fn new(allowed_tools: BTreeSet<String>) -> Self {
-        Self { allowed_tools }
+    fn new(allowed_tools: BTreeSet<String>, tool_registry: GlobalToolRegistry) -> Self {
+        Self {
+            allowed_tools,
+            tool_registry,
+        }
     }
     }
 }
 }
 
 
 impl ToolExecutor for SubagentToolExecutor {
 impl ToolExecutor for SubagentToolExecutor {
     fn execute(&mut self, tool_name: &str, input: &str) -> Result<String, ToolError> {
     fn execute(&mut self, tool_name: &str, input: &str) -> Result<String, ToolError> {
-        if !self.allowed_tools.contains(tool_name) {
+        let entry = self
+            .tool_registry
+            .find_entry(tool_name)
+            .ok_or_else(|| ToolError::new(format!("unsupported tool: {tool_name}")))?;
+        if !self.allowed_tools.contains(entry.definition.name.as_str()) {
             return Err(ToolError::new(format!(
             return Err(ToolError::new(format!(
                 "tool `{tool_name}` is not enabled for this sub-agent"
                 "tool `{tool_name}` is not enabled for this sub-agent"
             )));
             )));
         }
         }
         let value = serde_json::from_str(input)
         let value = serde_json::from_str(input)
             .map_err(|error| ToolError::new(format!("invalid tool input JSON: {error}")))?;
             .map_err(|error| ToolError::new(format!("invalid tool input JSON: {error}")))?;
-        execute_tool(tool_name, &value).map_err(ToolError::new)
+        self.tool_registry
+            .execute(tool_name, &value)
+            .map_err(ToolError::new)
     }
     }
 }
 }
 
 
-fn tool_specs_for_allowed_tools(allowed_tools: Option<&BTreeSet<String>>) -> Vec<ToolSpec> {
-    mvp_tool_specs()
-        .into_iter()
-        .filter(|spec| allowed_tools.is_none_or(|allowed| allowed.contains(spec.name)))
-        .collect()
+fn current_tool_registry() -> Result<GlobalToolRegistry, String> {
+    let cwd = std::env::current_dir().map_err(|error| error.to_string())?;
+    let loader = ConfigLoader::default_for(&cwd);
+    let runtime_config = loader.load().map_err(|error| error.to_string())?;
+    let plugin_manager = build_plugin_manager(&cwd, &loader, &runtime_config);
+    let plugin_tools = plugin_manager
+        .aggregated_tools()
+        .map_err(|error| error.to_string())?;
+    GlobalToolRegistry::with_plugin_tools(plugin_tools)
+}
+
+fn build_plugin_manager(
+    cwd: &Path,
+    loader: &ConfigLoader,
+    runtime_config: &RuntimeConfig,
+) -> PluginManager {
+    let plugin_settings = runtime_config.plugins();
+    let mut plugin_config = PluginManagerConfig::new(loader.config_home().to_path_buf());
+    plugin_config.enabled_plugins = plugin_settings.enabled_plugins().clone();
+    plugin_config.external_dirs = plugin_settings
+        .external_directories()
+        .iter()
+        .map(|path| resolve_plugin_path(cwd, loader.config_home(), path))
+        .collect();
+    plugin_config.install_root = plugin_settings
+        .install_root()
+        .map(|path| resolve_plugin_path(cwd, loader.config_home(), path));
+    plugin_config.registry_path = plugin_settings
+        .registry_path()
+        .map(|path| resolve_plugin_path(cwd, loader.config_home(), path));
+    plugin_config.bundled_root = plugin_settings
+        .bundled_root()
+        .map(|path| resolve_plugin_path(cwd, loader.config_home(), path));
+    PluginManager::new(plugin_config)
+}
+
+fn resolve_plugin_path(cwd: &Path, config_home: &Path, value: &str) -> PathBuf {
+    let path = PathBuf::from(value);
+    if path.is_absolute() {
+        path
+    } else if value.starts_with('.') {
+        cwd.join(path)
+    } else {
+        config_home.join(path)
+    }
 }
 }
 
 
 fn convert_messages(messages: &[ConversationMessage]) -> Vec<InputMessage> {
 fn convert_messages(messages: &[ConversationMessage]) -> Vec<InputMessage> {
@@ -3142,7 +3195,9 @@ mod tests {
         AgentInput, AgentJob, GlobalToolRegistry, SubagentToolExecutor,
         AgentInput, AgentJob, GlobalToolRegistry, SubagentToolExecutor,
     };
     };
     use plugins::{PluginTool, PluginToolDefinition, PluginToolPermission};
     use plugins::{PluginTool, PluginToolDefinition, PluginToolPermission};
-    use runtime::{ApiRequest, AssistantEvent, ConversationRuntime, RuntimeError, Session};
+    use runtime::{
+        ApiRequest, AssistantEvent, ConversationRuntime, RuntimeError, Session, ToolExecutor,
+    };
     use serde_json::json;
     use serde_json::json;
 
 
     fn env_lock() -> &'static Mutex<()> {
     fn env_lock() -> &'static Mutex<()> {
@@ -3221,8 +3276,8 @@ mod tests {
                     "additionalProperties": false
                     "additionalProperties": false
                 }),
                 }),
             },
             },
-            script.display().to_string(),
-            Vec::new(),
+            "sh".to_string(),
+            vec![script.display().to_string()],
             PluginToolPermission::WorkspaceWrite,
             PluginToolPermission::WorkspaceWrite,
             script.parent().map(PathBuf::from),
             script.parent().map(PathBuf::from),
         )])
         )])
@@ -3300,6 +3355,48 @@ mod tests {
         let _ = std::fs::remove_file(script);
         let _ = std::fs::remove_file(script);
     }
     }
 
 
+    #[test]
+    fn subagent_executor_executes_allowed_plugin_tools() {
+        let script = temp_path("subagent-plugin-tool.sh");
+        std::fs::write(
+            &script,
+            "#!/bin/sh\nINPUT=$(cat)\nprintf '{\"tool\":\"%s\",\"input\":%s}\\n' \"$CLAWD_TOOL_NAME\" \"$INPUT\"\n",
+        )
+        .expect("write script");
+        make_executable(&script);
+
+        let registry = GlobalToolRegistry::with_plugin_tools(vec![PluginTool::new(
+            "demo@external",
+            "demo",
+            PluginToolDefinition {
+                name: "plugin_echo".to_string(),
+                description: Some("Echo plugin input".to_string()),
+                input_schema: json!({
+                    "type": "object",
+                    "properties": { "message": { "type": "string" } },
+                    "required": ["message"],
+                    "additionalProperties": false
+                }),
+            },
+            script.display().to_string(),
+            Vec::new(),
+            PluginToolPermission::WorkspaceWrite,
+            script.parent().map(PathBuf::from),
+        )])
+        .expect("registry should build");
+
+        let mut executor =
+            SubagentToolExecutor::new(BTreeSet::from([String::from("plugin_echo")]), registry);
+        let output = executor
+            .execute("plugin-echo", r#"{"message":"hello"}"#)
+            .expect("plugin tool should execute for subagent");
+        let payload: serde_json::Value = serde_json::from_str(&output).expect("valid json");
+        assert_eq!(payload["tool"], "plugin_echo");
+        assert_eq!(payload["input"]["message"], "hello");
+
+        let _ = std::fs::remove_file(script);
+    }
+
     #[test]
     #[test]
     fn global_registry_rejects_conflicting_plugin_tool_names() {
     fn global_registry_rejects_conflicting_plugin_tool_names() {
         let error = GlobalToolRegistry::with_plugin_tools(vec![PluginTool::new(
         let error = GlobalToolRegistry::with_plugin_tools(vec![PluginTool::new(
@@ -3899,8 +3996,11 @@ mod tests {
                 calls: 0,
                 calls: 0,
                 input_path: path.display().to_string(),
                 input_path: path.display().to_string(),
             },
             },
-            SubagentToolExecutor::new(BTreeSet::from([String::from("read_file")])),
-            agent_permission_policy(),
+            SubagentToolExecutor::new(
+                BTreeSet::from([String::from("read_file")]),
+                GlobalToolRegistry::builtin(),
+            ),
+            agent_permission_policy(&GlobalToolRegistry::builtin()),
             vec![String::from("system prompt")],
             vec![String::from("system prompt")],
         );
         );