1use std::sync::atomic::{AtomicU8, Ordering};
7
8use crate::TrustLevel;
9
10use crate::executor::{ToolCall, ToolError, ToolExecutor, ToolOutput};
11use crate::permissions::{AutonomyLevel, PermissionAction, PermissionPolicy};
12use crate::registry::ToolDef;
13
14const QUARANTINE_DENIED: &[&str] = &["bash", "file_write", "web_scrape"];
16
17fn trust_to_u8(level: TrustLevel) -> u8 {
18 match level {
19 TrustLevel::Trusted => 0,
20 TrustLevel::Verified => 1,
21 TrustLevel::Quarantined => 2,
22 TrustLevel::Blocked => 3,
23 }
24}
25
26fn u8_to_trust(v: u8) -> TrustLevel {
27 match v {
28 0 => TrustLevel::Trusted,
29 1 => TrustLevel::Verified,
30 2 => TrustLevel::Quarantined,
31 _ => TrustLevel::Blocked,
32 }
33}
34
35pub struct TrustGateExecutor<T: ToolExecutor> {
37 inner: T,
38 policy: PermissionPolicy,
39 effective_trust: AtomicU8,
40}
41
42impl<T: ToolExecutor + std::fmt::Debug> std::fmt::Debug for TrustGateExecutor<T> {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 f.debug_struct("TrustGateExecutor")
45 .field("inner", &self.inner)
46 .field("policy", &self.policy)
47 .field("effective_trust", &self.effective_trust())
48 .finish()
49 }
50}
51
52impl<T: ToolExecutor> TrustGateExecutor<T> {
53 #[must_use]
54 pub fn new(inner: T, policy: PermissionPolicy) -> Self {
55 Self {
56 inner,
57 policy,
58 effective_trust: AtomicU8::new(trust_to_u8(TrustLevel::Trusted)),
59 }
60 }
61
62 pub fn set_effective_trust(&self, level: TrustLevel) {
63 self.effective_trust
64 .store(trust_to_u8(level), Ordering::Relaxed);
65 }
66
67 #[must_use]
68 pub fn effective_trust(&self) -> TrustLevel {
69 u8_to_trust(self.effective_trust.load(Ordering::Relaxed))
70 }
71
72 fn check_trust(&self, tool_id: &str, input: &str) -> Result<(), ToolError> {
73 match self.effective_trust() {
74 TrustLevel::Blocked => {
75 return Err(ToolError::Blocked {
76 command: "all tools blocked (trust=blocked)".to_owned(),
77 });
78 }
79 TrustLevel::Quarantined => {
80 if QUARANTINE_DENIED.contains(&tool_id) {
81 return Err(ToolError::Blocked {
82 command: format!("{tool_id} denied (trust=quarantined)"),
83 });
84 }
85 }
86 TrustLevel::Trusted | TrustLevel::Verified => {}
87 }
88
89 if self.policy.autonomy_level() == AutonomyLevel::Supervised
94 && self.policy.rules().get(tool_id).is_none()
95 {
96 return Ok(());
97 }
98
99 match self.policy.check(tool_id, input) {
100 PermissionAction::Allow => Ok(()),
101 PermissionAction::Ask => Err(ToolError::ConfirmationRequired {
102 command: input.to_owned(),
103 }),
104 PermissionAction::Deny => Err(ToolError::Blocked {
105 command: input.to_owned(),
106 }),
107 }
108 }
109}
110
111impl<T: ToolExecutor> ToolExecutor for TrustGateExecutor<T> {
112 async fn execute(&self, response: &str) -> Result<Option<ToolOutput>, ToolError> {
113 match self.effective_trust() {
114 TrustLevel::Blocked | TrustLevel::Quarantined => {
115 return Err(ToolError::Blocked {
116 command: format!(
117 "tool execution denied (trust={})",
118 format!("{:?}", self.effective_trust()).to_lowercase()
119 ),
120 });
121 }
122 TrustLevel::Trusted | TrustLevel::Verified => {}
123 }
124 self.inner.execute(response).await
125 }
126
127 async fn execute_confirmed(&self, response: &str) -> Result<Option<ToolOutput>, ToolError> {
128 match self.effective_trust() {
129 TrustLevel::Blocked | TrustLevel::Quarantined => {
130 return Err(ToolError::Blocked {
131 command: format!(
132 "tool execution denied (trust={})",
133 format!("{:?}", self.effective_trust()).to_lowercase()
134 ),
135 });
136 }
137 TrustLevel::Trusted | TrustLevel::Verified => {}
138 }
139 self.inner.execute_confirmed(response).await
140 }
141
142 fn tool_definitions(&self) -> Vec<ToolDef> {
143 self.inner.tool_definitions()
144 }
145
146 async fn execute_tool_call(&self, call: &ToolCall) -> Result<Option<ToolOutput>, ToolError> {
147 let input = call
148 .params
149 .get("command")
150 .or_else(|| call.params.get("file_path"))
151 .or_else(|| call.params.get("query"))
152 .or_else(|| call.params.get("url"))
153 .or_else(|| call.params.get("uri"))
154 .and_then(|v| v.as_str())
155 .unwrap_or("");
156 self.check_trust(&call.tool_id, input)?;
157 self.inner.execute_tool_call(call).await
158 }
159
160 async fn execute_tool_call_confirmed(
161 &self,
162 call: &ToolCall,
163 ) -> Result<Option<ToolOutput>, ToolError> {
164 match self.effective_trust() {
167 TrustLevel::Blocked => {
168 return Err(ToolError::Blocked {
169 command: "all tools blocked (trust=blocked)".to_owned(),
170 });
171 }
172 TrustLevel::Quarantined => {
173 if QUARANTINE_DENIED.contains(&call.tool_id.as_str()) {
174 return Err(ToolError::Blocked {
175 command: format!("{} denied (trust=quarantined)", call.tool_id),
176 });
177 }
178 }
179 TrustLevel::Trusted | TrustLevel::Verified => {}
180 }
181 self.inner.execute_tool_call(call).await
182 }
183
184 fn set_skill_env(&self, env: Option<std::collections::HashMap<String, String>>) {
185 self.inner.set_skill_env(env);
186 }
187
188 fn set_effective_trust(&self, level: crate::TrustLevel) {
189 self.effective_trust
190 .store(trust_to_u8(level), Ordering::Relaxed);
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197
198 #[derive(Debug)]
199 struct MockExecutor;
200 impl ToolExecutor for MockExecutor {
201 async fn execute(&self, _: &str) -> Result<Option<ToolOutput>, ToolError> {
202 Ok(None)
203 }
204 async fn execute_tool_call(
205 &self,
206 call: &ToolCall,
207 ) -> Result<Option<ToolOutput>, ToolError> {
208 Ok(Some(ToolOutput {
209 tool_name: call.tool_id.clone(),
210 summary: "ok".into(),
211 blocks_executed: 1,
212 filter_stats: None,
213 diff: None,
214 streamed: false,
215 terminal_id: None,
216 locations: None,
217 raw_response: None,
218 }))
219 }
220 }
221
222 fn make_call(tool_id: &str) -> ToolCall {
223 ToolCall {
224 tool_id: tool_id.into(),
225 params: serde_json::Map::new(),
226 }
227 }
228
229 fn make_call_with_cmd(tool_id: &str, cmd: &str) -> ToolCall {
230 let mut params = serde_json::Map::new();
231 params.insert("command".into(), serde_json::Value::String(cmd.into()));
232 ToolCall {
233 tool_id: tool_id.into(),
234 params,
235 }
236 }
237
238 #[tokio::test]
239 async fn trusted_allows_all() {
240 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
241 gate.set_effective_trust(TrustLevel::Trusted);
242
243 let result = gate.execute_tool_call(&make_call("bash")).await;
244 assert!(result.is_ok());
246 }
247
248 #[tokio::test]
249 async fn quarantined_denies_bash() {
250 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
251 gate.set_effective_trust(TrustLevel::Quarantined);
252
253 let result = gate.execute_tool_call(&make_call("bash")).await;
254 assert!(matches!(result, Err(ToolError::Blocked { .. })));
255 }
256
257 #[tokio::test]
258 async fn quarantined_denies_file_write() {
259 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
260 gate.set_effective_trust(TrustLevel::Quarantined);
261
262 let result = gate.execute_tool_call(&make_call("file_write")).await;
263 assert!(matches!(result, Err(ToolError::Blocked { .. })));
264 }
265
266 #[tokio::test]
267 async fn quarantined_allows_file_read() {
268 let policy = crate::permissions::PermissionPolicy::from_legacy(&[], &[]);
269 let gate = TrustGateExecutor::new(MockExecutor, policy);
270 gate.set_effective_trust(TrustLevel::Quarantined);
271
272 let result = gate.execute_tool_call(&make_call("file_read")).await;
273 assert!(result.is_ok());
275 }
276
277 #[tokio::test]
278 async fn blocked_denies_everything() {
279 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
280 gate.set_effective_trust(TrustLevel::Blocked);
281
282 let result = gate.execute_tool_call(&make_call("file_read")).await;
283 assert!(matches!(result, Err(ToolError::Blocked { .. })));
284 }
285
286 #[tokio::test]
287 async fn policy_deny_overrides_trust() {
288 let policy = crate::permissions::PermissionPolicy::from_legacy(&["sudo".into()], &[]);
289 let gate = TrustGateExecutor::new(MockExecutor, policy);
290 gate.set_effective_trust(TrustLevel::Trusted);
291
292 let result = gate
293 .execute_tool_call(&make_call_with_cmd("bash", "sudo rm"))
294 .await;
295 assert!(matches!(result, Err(ToolError::Blocked { .. })));
296 }
297
298 #[tokio::test]
299 async fn blocked_denies_execute() {
300 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
301 gate.set_effective_trust(TrustLevel::Blocked);
302
303 let result = gate.execute("some response").await;
304 assert!(matches!(result, Err(ToolError::Blocked { .. })));
305 }
306
307 #[tokio::test]
308 async fn blocked_denies_execute_confirmed() {
309 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
310 gate.set_effective_trust(TrustLevel::Blocked);
311
312 let result = gate.execute_confirmed("some response").await;
313 assert!(matches!(result, Err(ToolError::Blocked { .. })));
314 }
315
316 #[tokio::test]
317 async fn trusted_allows_execute() {
318 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
319 gate.set_effective_trust(TrustLevel::Trusted);
320
321 let result = gate.execute("some response").await;
322 assert!(result.is_ok());
323 }
324
325 #[tokio::test]
326 async fn verified_with_allow_policy_succeeds() {
327 let policy = crate::permissions::PermissionPolicy::from_legacy(&[], &[]);
328 let gate = TrustGateExecutor::new(MockExecutor, policy);
329 gate.set_effective_trust(TrustLevel::Verified);
330
331 let result = gate
332 .execute_tool_call(&make_call_with_cmd("bash", "echo hi"))
333 .await
334 .unwrap();
335 assert!(result.is_some());
336 }
337
338 #[tokio::test]
339 async fn quarantined_denies_web_scrape() {
340 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
341 gate.set_effective_trust(TrustLevel::Quarantined);
342
343 let result = gate.execute_tool_call(&make_call("web_scrape")).await;
344 assert!(matches!(result, Err(ToolError::Blocked { .. })));
345 }
346
347 #[derive(Debug)]
348 struct EnvCapture {
349 captured: std::sync::Mutex<Option<std::collections::HashMap<String, String>>>,
350 }
351 impl EnvCapture {
352 fn new() -> Self {
353 Self {
354 captured: std::sync::Mutex::new(None),
355 }
356 }
357 }
358 impl ToolExecutor for EnvCapture {
359 async fn execute(&self, _: &str) -> Result<Option<ToolOutput>, ToolError> {
360 Ok(None)
361 }
362 async fn execute_tool_call(&self, _: &ToolCall) -> Result<Option<ToolOutput>, ToolError> {
363 Ok(None)
364 }
365 fn set_skill_env(&self, env: Option<std::collections::HashMap<String, String>>) {
366 *self.captured.lock().unwrap() = env;
367 }
368 }
369
370 #[test]
371 fn set_skill_env_forwarded_to_inner() {
372 let inner = EnvCapture::new();
373 let gate = TrustGateExecutor::new(inner, PermissionPolicy::default());
374
375 let mut env = std::collections::HashMap::new();
376 env.insert("MY_VAR".to_owned(), "42".to_owned());
377 gate.set_skill_env(Some(env.clone()));
378
379 let captured = gate.inner.captured.lock().unwrap();
380 assert_eq!(*captured, Some(env));
381 }
382
383 #[tokio::test]
384 async fn mcp_tool_supervised_no_rules_allows() {
385 let policy = crate::permissions::PermissionPolicy::from_legacy(&[], &[]);
387 let gate = TrustGateExecutor::new(MockExecutor, policy);
388 gate.set_effective_trust(TrustLevel::Trusted);
389
390 let mut params = serde_json::Map::new();
391 params.insert(
392 "file_path".into(),
393 serde_json::Value::String("/tmp/test.txt".into()),
394 );
395 let call = ToolCall {
396 tool_id: "mcp_filesystem__read_file".into(),
397 params,
398 };
399 let result = gate.execute_tool_call(&call).await;
400 assert!(
401 result.is_ok(),
402 "MCP tool should be allowed when no rules exist"
403 );
404 }
405
406 #[tokio::test]
407 async fn bash_with_explicit_deny_rule_blocked() {
408 let policy = crate::permissions::PermissionPolicy::from_legacy(&["sudo".into()], &[]);
410 let gate = TrustGateExecutor::new(MockExecutor, policy);
411 gate.set_effective_trust(TrustLevel::Trusted);
412
413 let result = gate
414 .execute_tool_call(&make_call_with_cmd("bash", "sudo apt install vim"))
415 .await;
416 assert!(
417 matches!(result, Err(ToolError::Blocked { .. })),
418 "bash with explicit deny rule should be blocked"
419 );
420 }
421
422 #[tokio::test]
423 async fn bash_with_explicit_allow_rule_succeeds() {
424 let policy = crate::permissions::PermissionPolicy::from_legacy(&[], &[]);
426 let gate = TrustGateExecutor::new(MockExecutor, policy);
427 gate.set_effective_trust(TrustLevel::Trusted);
428
429 let result = gate
430 .execute_tool_call(&make_call_with_cmd("bash", "echo hello"))
431 .await;
432 assert!(
433 result.is_ok(),
434 "bash with explicit allow rule should succeed"
435 );
436 }
437
438 #[tokio::test]
439 async fn readonly_denies_mcp_tool_not_in_allowlist() {
440 let policy =
442 crate::permissions::PermissionPolicy::default().with_autonomy(AutonomyLevel::ReadOnly);
443 let gate = TrustGateExecutor::new(MockExecutor, policy);
444 gate.set_effective_trust(TrustLevel::Trusted);
445
446 let result = gate
447 .execute_tool_call(&make_call("mcpls_get_diagnostics"))
448 .await;
449 assert!(
450 matches!(result, Err(ToolError::Blocked { .. })),
451 "ReadOnly mode must deny non-allowlisted tools"
452 );
453 }
454
455 #[test]
456 fn set_effective_trust_interior_mutability() {
457 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
458 assert_eq!(gate.effective_trust(), TrustLevel::Trusted);
459
460 gate.set_effective_trust(TrustLevel::Quarantined);
461 assert_eq!(gate.effective_trust(), TrustLevel::Quarantined);
462
463 gate.set_effective_trust(TrustLevel::Blocked);
464 assert_eq!(gate.effective_trust(), TrustLevel::Blocked);
465
466 gate.set_effective_trust(TrustLevel::Trusted);
467 assert_eq!(gate.effective_trust(), TrustLevel::Trusted);
468 }
469}