Skip to main content

zeph_tools/
tool_filter.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use crate::executor::{ToolCall, ToolError, ToolExecutor, ToolOutput};
5use crate::registry::ToolDef;
6
7/// Wraps a `ToolExecutor` and suppresses specified tool ids from both
8/// `tool_definitions` and `execute_tool_call`.
9///
10/// Used to hide `FileExecutor` tools (e.g. `read`, `glob`) when
11/// `AcpFileExecutor` provides equivalent IDE-proxied alternatives.
12#[derive(Debug)]
13pub struct ToolFilter<E: ToolExecutor> {
14    inner: E,
15    suppressed: &'static [&'static str],
16}
17
18impl<E: ToolExecutor> ToolFilter<E> {
19    #[must_use]
20    pub fn new(inner: E, suppressed: &'static [&'static str]) -> Self {
21        Self { inner, suppressed }
22    }
23}
24
25impl<E: ToolExecutor> ToolExecutor for ToolFilter<E> {
26    async fn execute(&self, response: &str) -> Result<Option<ToolOutput>, ToolError> {
27        self.inner.execute(response).await
28    }
29
30    async fn execute_confirmed(&self, response: &str) -> Result<Option<ToolOutput>, ToolError> {
31        self.inner.execute_confirmed(response).await
32    }
33
34    fn tool_definitions(&self) -> Vec<ToolDef> {
35        self.inner
36            .tool_definitions()
37            .into_iter()
38            .filter(|d| !self.suppressed.contains(&d.id.as_ref()))
39            .collect()
40    }
41
42    async fn execute_tool_call(&self, call: &ToolCall) -> Result<Option<ToolOutput>, ToolError> {
43        if self.suppressed.contains(&call.tool_id.as_str()) {
44            return Ok(None);
45        }
46        self.inner.execute_tool_call(call).await
47    }
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53    use crate::ToolName;
54
55    #[derive(Debug)]
56    struct StubExecutor;
57    impl ToolExecutor for StubExecutor {
58        async fn execute(&self, _: &str) -> Result<Option<ToolOutput>, ToolError> {
59            Ok(None)
60        }
61        fn tool_definitions(&self) -> Vec<ToolDef> {
62            vec![
63                ToolDef {
64                    id: "read".into(),
65                    description: "read a file".into(),
66                    schema: schemars::schema_for!(String),
67                    invocation: crate::registry::InvocationHint::ToolCall,
68                    output_schema: None,
69                    server_id: None,
70                },
71                ToolDef {
72                    id: "glob".into(),
73                    description: "find files".into(),
74                    schema: schemars::schema_for!(String),
75                    invocation: crate::registry::InvocationHint::ToolCall,
76                    output_schema: None,
77                    server_id: None,
78                },
79                ToolDef {
80                    id: "edit".into(),
81                    description: "edit a file".into(),
82                    schema: schemars::schema_for!(String),
83                    invocation: crate::registry::InvocationHint::ToolCall,
84                    output_schema: None,
85                    server_id: None,
86                },
87            ]
88        }
89        async fn execute_tool_call(
90            &self,
91            call: &ToolCall,
92        ) -> Result<Option<ToolOutput>, ToolError> {
93            Ok(Some(ToolOutput {
94                tool_name: call.tool_id.clone(),
95                summary: "stub".to_owned(),
96                blocks_executed: 1,
97                filter_stats: None,
98                diff: None,
99                streamed: false,
100                terminal_id: None,
101                locations: None,
102                raw_response: None,
103                claim_source: None,
104            }))
105        }
106    }
107
108    #[test]
109    fn suppressed_tools_hidden_from_definitions() {
110        let filter = ToolFilter::new(StubExecutor, &["read", "glob"]);
111        let defs = filter.tool_definitions();
112        let ids: Vec<&str> = defs.iter().map(|d| d.id.as_ref()).collect();
113        assert!(!ids.contains(&"read"));
114        assert!(!ids.contains(&"glob"));
115        assert!(ids.contains(&"edit"));
116    }
117
118    #[tokio::test]
119    async fn suppressed_tool_call_returns_none() {
120        let filter = ToolFilter::new(StubExecutor, &["read", "glob"]);
121        let call = ToolCall {
122            tool_id: ToolName::new("read"),
123            params: serde_json::Map::new(),
124            caller_id: None,
125            context: None,
126
127            tool_call_id: String::new(),
128            skill_name: None,
129        };
130        let result = filter.execute_tool_call(&call).await.unwrap();
131        assert!(result.is_none());
132    }
133
134    #[tokio::test]
135    async fn allowed_tool_call_passes_through() {
136        let filter = ToolFilter::new(StubExecutor, &["read", "glob"]);
137        let call = ToolCall {
138            tool_id: ToolName::new("edit"),
139            params: serde_json::Map::new(),
140            caller_id: None,
141            context: None,
142
143            tool_call_id: String::new(),
144            skill_name: None,
145        };
146        let result = filter.execute_tool_call(&call).await.unwrap();
147        assert!(result.is_some());
148    }
149}