Skip to main content

zeph_tools/
trust_gate.rs

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