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                raw_response: None,
129            }))
130        }
131    }
132
133    fn make_call(tool_id: &str) -> ToolCall {
134        ToolCall {
135            tool_id: tool_id.into(),
136            params: serde_json::Map::new(),
137        }
138    }
139
140    fn make_call_with_cmd(tool_id: &str, cmd: &str) -> ToolCall {
141        let mut params = serde_json::Map::new();
142        params.insert("command".into(), serde_json::Value::String(cmd.into()));
143        ToolCall {
144            tool_id: tool_id.into(),
145            params,
146        }
147    }
148
149    #[tokio::test]
150    async fn trusted_allows_all() {
151        let mut gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
152        gate.set_effective_trust(TrustLevel::Trusted);
153
154        let result = gate.execute_tool_call(&make_call("bash")).await;
155        // Default policy returns Ask for unknown tools
156        assert!(matches!(
157            result,
158            Err(ToolError::ConfirmationRequired { .. })
159        ));
160    }
161
162    #[tokio::test]
163    async fn quarantined_denies_bash() {
164        let mut gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
165        gate.set_effective_trust(TrustLevel::Quarantined);
166
167        let result = gate.execute_tool_call(&make_call("bash")).await;
168        assert!(matches!(result, Err(ToolError::Blocked { .. })));
169    }
170
171    #[tokio::test]
172    async fn quarantined_denies_file_write() {
173        let mut gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
174        gate.set_effective_trust(TrustLevel::Quarantined);
175
176        let result = gate.execute_tool_call(&make_call("file_write")).await;
177        assert!(matches!(result, Err(ToolError::Blocked { .. })));
178    }
179
180    #[tokio::test]
181    async fn quarantined_allows_file_read() {
182        let policy = crate::permissions::PermissionPolicy::from_legacy(&[], &[]);
183        let mut gate = TrustGateExecutor::new(MockExecutor, policy);
184        gate.set_effective_trust(TrustLevel::Quarantined);
185
186        let result = gate.execute_tool_call(&make_call("file_read")).await;
187        // file_read is not in quarantine denied list, and policy has no rules for file_read => Ask
188        assert!(matches!(
189            result,
190            Err(ToolError::ConfirmationRequired { .. })
191        ));
192    }
193
194    #[tokio::test]
195    async fn blocked_denies_everything() {
196        let mut gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
197        gate.set_effective_trust(TrustLevel::Blocked);
198
199        let result = gate.execute_tool_call(&make_call("file_read")).await;
200        assert!(matches!(result, Err(ToolError::Blocked { .. })));
201    }
202
203    #[tokio::test]
204    async fn policy_deny_overrides_trust() {
205        let policy = crate::permissions::PermissionPolicy::from_legacy(&["sudo".into()], &[]);
206        let mut gate = TrustGateExecutor::new(MockExecutor, policy);
207        gate.set_effective_trust(TrustLevel::Trusted);
208
209        let result = gate
210            .execute_tool_call(&make_call_with_cmd("bash", "sudo rm"))
211            .await;
212        assert!(matches!(result, Err(ToolError::Blocked { .. })));
213    }
214
215    #[tokio::test]
216    async fn blocked_denies_execute() {
217        let mut gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
218        gate.set_effective_trust(TrustLevel::Blocked);
219
220        let result = gate.execute("some response").await;
221        assert!(matches!(result, Err(ToolError::Blocked { .. })));
222    }
223
224    #[tokio::test]
225    async fn blocked_denies_execute_confirmed() {
226        let mut gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
227        gate.set_effective_trust(TrustLevel::Blocked);
228
229        let result = gate.execute_confirmed("some response").await;
230        assert!(matches!(result, Err(ToolError::Blocked { .. })));
231    }
232
233    #[tokio::test]
234    async fn trusted_allows_execute() {
235        let mut gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
236        gate.set_effective_trust(TrustLevel::Trusted);
237
238        let result = gate.execute("some response").await;
239        assert!(result.is_ok());
240    }
241
242    #[tokio::test]
243    async fn verified_with_allow_policy_succeeds() {
244        let policy = crate::permissions::PermissionPolicy::from_legacy(&[], &[]);
245        let mut gate = TrustGateExecutor::new(MockExecutor, policy);
246        gate.set_effective_trust(TrustLevel::Verified);
247
248        let result = gate
249            .execute_tool_call(&make_call_with_cmd("bash", "echo hi"))
250            .await
251            .unwrap();
252        assert!(result.is_some());
253    }
254}