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