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