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