1use zeph_skills::TrustLevel;
7
8use crate::executor::{ToolCall, ToolError, ToolExecutor, ToolOutput};
9use crate::permissions::{PermissionAction, PermissionPolicy};
10use crate::registry::ToolDef;
11
12const QUARANTINE_DENIED: &[&str] = &["bash", "file_write", "web_scrape"];
14
15#[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 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 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}