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 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 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 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}