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