Skip to main content

zeph_tools/
trust_gate.rs

1//! Trust-level enforcement layer for tool execution.
2
3use zeph_skills::TrustLevel;
4
5use crate::executor::{ToolCall, ToolError, ToolExecutor, ToolOutput};
6use crate::permissions::{PermissionAction, PermissionPolicy};
7use crate::registry::ToolDef;
8
9/// Tools denied when a Quarantined skill is active.
10const QUARANTINE_DENIED: &[&str] = &["bash", "file_write", "web_scrape"];
11
12/// Wraps an inner `ToolExecutor` and applies trust-level permission overlays.
13#[derive(Debug)]
14pub struct TrustGateExecutor<T: ToolExecutor> {
15    inner: T,
16    policy: PermissionPolicy,
17    effective_trust: TrustLevel,
18}
19
20impl<T: ToolExecutor> TrustGateExecutor<T> {
21    #[must_use]
22    pub fn new(inner: T, policy: PermissionPolicy) -> Self {
23        Self {
24            inner,
25            policy,
26            effective_trust: TrustLevel::Trusted,
27        }
28    }
29
30    pub fn set_effective_trust(&mut self, level: TrustLevel) {
31        self.effective_trust = level;
32    }
33
34    #[must_use]
35    pub fn effective_trust(&self) -> TrustLevel {
36        self.effective_trust
37    }
38
39    fn check_trust(&self, tool_id: &str, input: &str) -> Result<(), ToolError> {
40        match self.effective_trust {
41            TrustLevel::Blocked => {
42                return Err(ToolError::Blocked {
43                    command: "all tools blocked (trust=blocked)".to_owned(),
44                });
45            }
46            TrustLevel::Quarantined => {
47                if QUARANTINE_DENIED.contains(&tool_id) {
48                    return Err(ToolError::Blocked {
49                        command: format!("{tool_id} denied (trust=quarantined)"),
50                    });
51                }
52            }
53            TrustLevel::Trusted | TrustLevel::Verified => {}
54        }
55
56        match self.policy.check(tool_id, input) {
57            PermissionAction::Allow => Ok(()),
58            PermissionAction::Ask => Err(ToolError::ConfirmationRequired {
59                command: input.to_owned(),
60            }),
61            PermissionAction::Deny => Err(ToolError::Blocked {
62                command: input.to_owned(),
63            }),
64        }
65    }
66}
67
68impl<T: ToolExecutor> ToolExecutor for TrustGateExecutor<T> {
69    async fn execute(&self, response: &str) -> Result<Option<ToolOutput>, ToolError> {
70        if self.effective_trust == TrustLevel::Blocked {
71            return Err(ToolError::Blocked {
72                command: "all tools blocked (trust=blocked)".to_owned(),
73            });
74        }
75        self.inner.execute(response).await
76    }
77
78    async fn execute_confirmed(&self, response: &str) -> Result<Option<ToolOutput>, ToolError> {
79        if self.effective_trust == TrustLevel::Blocked {
80            return Err(ToolError::Blocked {
81                command: "all tools blocked (trust=blocked)".to_owned(),
82            });
83        }
84        self.inner.execute_confirmed(response).await
85    }
86
87    fn tool_definitions(&self) -> Vec<ToolDef> {
88        self.inner.tool_definitions()
89    }
90
91    async fn execute_tool_call(&self, call: &ToolCall) -> Result<Option<ToolOutput>, ToolError> {
92        let input = call
93            .params
94            .get("command")
95            .and_then(|v| v.as_str())
96            .unwrap_or("");
97        self.check_trust(&call.tool_id, input)?;
98        self.inner.execute_tool_call(call).await
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[derive(Debug)]
107    struct MockExecutor;
108    impl ToolExecutor for MockExecutor {
109        async fn execute(&self, _: &str) -> Result<Option<ToolOutput>, ToolError> {
110            Ok(None)
111        }
112        async fn execute_tool_call(
113            &self,
114            call: &ToolCall,
115        ) -> Result<Option<ToolOutput>, ToolError> {
116            Ok(Some(ToolOutput {
117                tool_name: call.tool_id.clone(),
118                summary: "ok".into(),
119                blocks_executed: 1,
120                filter_stats: None,
121                diff: None,
122                streamed: false,
123            }))
124        }
125    }
126
127    fn make_call(tool_id: &str) -> ToolCall {
128        ToolCall {
129            tool_id: tool_id.into(),
130            params: serde_json::Map::new(),
131        }
132    }
133
134    fn make_call_with_cmd(tool_id: &str, cmd: &str) -> ToolCall {
135        let mut params = serde_json::Map::new();
136        params.insert("command".into(), serde_json::Value::String(cmd.into()));
137        ToolCall {
138            tool_id: tool_id.into(),
139            params,
140        }
141    }
142
143    #[tokio::test]
144    async fn trusted_allows_all() {
145        let mut gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
146        gate.set_effective_trust(TrustLevel::Trusted);
147
148        let result = gate.execute_tool_call(&make_call("bash")).await;
149        // Default policy returns Ask for unknown tools
150        assert!(matches!(
151            result,
152            Err(ToolError::ConfirmationRequired { .. })
153        ));
154    }
155
156    #[tokio::test]
157    async fn quarantined_denies_bash() {
158        let mut gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
159        gate.set_effective_trust(TrustLevel::Quarantined);
160
161        let result = gate.execute_tool_call(&make_call("bash")).await;
162        assert!(matches!(result, Err(ToolError::Blocked { .. })));
163    }
164
165    #[tokio::test]
166    async fn quarantined_denies_file_write() {
167        let mut gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
168        gate.set_effective_trust(TrustLevel::Quarantined);
169
170        let result = gate.execute_tool_call(&make_call("file_write")).await;
171        assert!(matches!(result, Err(ToolError::Blocked { .. })));
172    }
173
174    #[tokio::test]
175    async fn quarantined_allows_file_read() {
176        let policy = crate::permissions::PermissionPolicy::from_legacy(&[], &[]);
177        let mut gate = TrustGateExecutor::new(MockExecutor, policy);
178        gate.set_effective_trust(TrustLevel::Quarantined);
179
180        let result = gate.execute_tool_call(&make_call("file_read")).await;
181        // file_read is not in quarantine denied list, and policy has no rules for file_read => Ask
182        assert!(matches!(
183            result,
184            Err(ToolError::ConfirmationRequired { .. })
185        ));
186    }
187
188    #[tokio::test]
189    async fn blocked_denies_everything() {
190        let mut gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
191        gate.set_effective_trust(TrustLevel::Blocked);
192
193        let result = gate.execute_tool_call(&make_call("file_read")).await;
194        assert!(matches!(result, Err(ToolError::Blocked { .. })));
195    }
196
197    #[tokio::test]
198    async fn policy_deny_overrides_trust() {
199        let policy = crate::permissions::PermissionPolicy::from_legacy(&["sudo".into()], &[]);
200        let mut gate = TrustGateExecutor::new(MockExecutor, policy);
201        gate.set_effective_trust(TrustLevel::Trusted);
202
203        let result = gate
204            .execute_tool_call(&make_call_with_cmd("bash", "sudo rm"))
205            .await;
206        assert!(matches!(result, Err(ToolError::Blocked { .. })));
207    }
208
209    #[tokio::test]
210    async fn blocked_denies_execute() {
211        let mut gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
212        gate.set_effective_trust(TrustLevel::Blocked);
213
214        let result = gate.execute("some response").await;
215        assert!(matches!(result, Err(ToolError::Blocked { .. })));
216    }
217
218    #[tokio::test]
219    async fn blocked_denies_execute_confirmed() {
220        let mut gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
221        gate.set_effective_trust(TrustLevel::Blocked);
222
223        let result = gate.execute_confirmed("some response").await;
224        assert!(matches!(result, Err(ToolError::Blocked { .. })));
225    }
226
227    #[tokio::test]
228    async fn trusted_allows_execute() {
229        let mut gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
230        gate.set_effective_trust(TrustLevel::Trusted);
231
232        let result = gate.execute("some response").await;
233        assert!(result.is_ok());
234    }
235
236    #[tokio::test]
237    async fn verified_with_allow_policy_succeeds() {
238        let policy = crate::permissions::PermissionPolicy::from_legacy(&[], &[]);
239        let mut gate = TrustGateExecutor::new(MockExecutor, policy);
240        gate.set_effective_trust(TrustLevel::Verified);
241
242        let result = gate
243            .execute_tool_call(&make_call_with_cmd("bash", "echo hi"))
244            .await
245            .unwrap();
246        assert!(result.is_some());
247    }
248}