1use std::collections::HashSet;
7use std::sync::{
8 Arc, RwLock,
9 atomic::{AtomicU8, Ordering},
10};
11
12use crate::TrustLevel;
13
14use crate::executor::{ToolCall, ToolError, ToolExecutor, ToolOutput};
15use crate::permissions::{AutonomyLevel, PermissionAction, PermissionPolicy};
16use crate::registry::ToolDef;
17
18pub const QUARANTINE_DENIED: &[&str] = &[
32 "bash",
34 "write",
36 "edit",
37 "delete_path",
38 "move_path",
39 "copy_path",
40 "create_directory",
41 "web_scrape",
43 "fetch",
44 "memory_save",
46];
47
48fn is_quarantine_denied(tool_id: &str) -> bool {
49 QUARANTINE_DENIED
50 .iter()
51 .any(|denied| tool_id == *denied || tool_id.ends_with(&format!("_{denied}")))
52}
53
54fn trust_to_u8(level: TrustLevel) -> u8 {
55 match level {
56 TrustLevel::Trusted => 0,
57 TrustLevel::Verified => 1,
58 TrustLevel::Quarantined => 2,
59 TrustLevel::Blocked => 3,
60 }
61}
62
63fn u8_to_trust(v: u8) -> TrustLevel {
64 match v {
65 0 => TrustLevel::Trusted,
66 1 => TrustLevel::Verified,
67 2 => TrustLevel::Quarantined,
68 _ => TrustLevel::Blocked,
69 }
70}
71
72pub struct TrustGateExecutor<T: ToolExecutor> {
74 inner: T,
75 policy: PermissionPolicy,
76 effective_trust: AtomicU8,
77 mcp_tool_ids: Arc<RwLock<HashSet<String>>>,
82}
83
84impl<T: ToolExecutor + std::fmt::Debug> std::fmt::Debug for TrustGateExecutor<T> {
85 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86 f.debug_struct("TrustGateExecutor")
87 .field("inner", &self.inner)
88 .field("policy", &self.policy)
89 .field("effective_trust", &self.effective_trust())
90 .field("mcp_tool_ids", &self.mcp_tool_ids)
91 .finish()
92 }
93}
94
95impl<T: ToolExecutor> TrustGateExecutor<T> {
96 #[must_use]
97 pub fn new(inner: T, policy: PermissionPolicy) -> Self {
98 Self {
99 inner,
100 policy,
101 effective_trust: AtomicU8::new(trust_to_u8(TrustLevel::Trusted)),
102 mcp_tool_ids: Arc::new(RwLock::new(HashSet::new())),
103 }
104 }
105
106 #[must_use]
110 pub fn mcp_tool_ids_handle(&self) -> Arc<RwLock<HashSet<String>>> {
111 Arc::clone(&self.mcp_tool_ids)
112 }
113
114 pub fn set_effective_trust(&self, level: TrustLevel) {
115 self.effective_trust
116 .store(trust_to_u8(level), Ordering::Relaxed);
117 }
118
119 #[must_use]
120 pub fn effective_trust(&self) -> TrustLevel {
121 u8_to_trust(self.effective_trust.load(Ordering::Relaxed))
122 }
123
124 fn is_mcp_tool(&self, tool_id: &str) -> bool {
125 self.mcp_tool_ids
126 .read()
127 .unwrap_or_else(std::sync::PoisonError::into_inner)
128 .contains(tool_id)
129 }
130
131 fn check_trust(&self, tool_id: &str, input: &str) -> Result<(), ToolError> {
132 match self.effective_trust() {
133 TrustLevel::Blocked => {
134 return Err(ToolError::Blocked {
135 command: "all tools blocked (trust=blocked)".to_owned(),
136 });
137 }
138 TrustLevel::Quarantined => {
139 if is_quarantine_denied(tool_id) || self.is_mcp_tool(tool_id) {
140 return Err(ToolError::Blocked {
141 command: format!("{tool_id} denied (trust=quarantined)"),
142 });
143 }
144 }
145 TrustLevel::Trusted | TrustLevel::Verified => {}
146 }
147
148 if self.policy.autonomy_level() == AutonomyLevel::Supervised
153 && self.policy.rules().get(tool_id).is_none()
154 {
155 return Ok(());
156 }
157
158 match self.policy.check(tool_id, input) {
159 PermissionAction::Allow => Ok(()),
160 PermissionAction::Ask => Err(ToolError::ConfirmationRequired {
161 command: input.to_owned(),
162 }),
163 PermissionAction::Deny => Err(ToolError::Blocked {
164 command: input.to_owned(),
165 }),
166 }
167 }
168}
169
170impl<T: ToolExecutor> ToolExecutor for TrustGateExecutor<T> {
171 async fn execute(&self, response: &str) -> Result<Option<ToolOutput>, ToolError> {
172 match self.effective_trust() {
176 TrustLevel::Blocked | TrustLevel::Quarantined => {
177 return Err(ToolError::Blocked {
178 command: format!(
179 "tool execution denied (trust={})",
180 format!("{:?}", self.effective_trust()).to_lowercase()
181 ),
182 });
183 }
184 TrustLevel::Trusted | TrustLevel::Verified => {}
185 }
186 self.inner.execute(response).await
187 }
188
189 async fn execute_confirmed(&self, response: &str) -> Result<Option<ToolOutput>, ToolError> {
190 match self.effective_trust() {
192 TrustLevel::Blocked | TrustLevel::Quarantined => {
193 return Err(ToolError::Blocked {
194 command: format!(
195 "tool execution denied (trust={})",
196 format!("{:?}", self.effective_trust()).to_lowercase()
197 ),
198 });
199 }
200 TrustLevel::Trusted | TrustLevel::Verified => {}
201 }
202 self.inner.execute_confirmed(response).await
203 }
204
205 fn tool_definitions(&self) -> Vec<ToolDef> {
206 self.inner.tool_definitions()
207 }
208
209 async fn execute_tool_call(&self, call: &ToolCall) -> Result<Option<ToolOutput>, ToolError> {
210 let input = call
211 .params
212 .get("command")
213 .or_else(|| call.params.get("file_path"))
214 .or_else(|| call.params.get("query"))
215 .or_else(|| call.params.get("url"))
216 .or_else(|| call.params.get("uri"))
217 .and_then(|v| v.as_str())
218 .unwrap_or("");
219 self.check_trust(&call.tool_id, input)?;
220 self.inner.execute_tool_call(call).await
221 }
222
223 async fn execute_tool_call_confirmed(
224 &self,
225 call: &ToolCall,
226 ) -> Result<Option<ToolOutput>, ToolError> {
227 match self.effective_trust() {
230 TrustLevel::Blocked => {
231 return Err(ToolError::Blocked {
232 command: "all tools blocked (trust=blocked)".to_owned(),
233 });
234 }
235 TrustLevel::Quarantined => {
236 if is_quarantine_denied(&call.tool_id) || self.is_mcp_tool(&call.tool_id) {
237 return Err(ToolError::Blocked {
238 command: format!("{} denied (trust=quarantined)", call.tool_id),
239 });
240 }
241 }
242 TrustLevel::Trusted | TrustLevel::Verified => {}
243 }
244 self.inner.execute_tool_call_confirmed(call).await
245 }
246
247 fn set_skill_env(&self, env: Option<std::collections::HashMap<String, String>>) {
248 self.inner.set_skill_env(env);
249 }
250
251 fn is_tool_retryable(&self, tool_id: &str) -> bool {
252 self.inner.is_tool_retryable(tool_id)
253 }
254
255 fn set_effective_trust(&self, level: crate::TrustLevel) {
256 self.effective_trust
257 .store(trust_to_u8(level), Ordering::Relaxed);
258 }
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264
265 #[derive(Debug)]
266 struct MockExecutor;
267 impl ToolExecutor for MockExecutor {
268 async fn execute(&self, _: &str) -> Result<Option<ToolOutput>, ToolError> {
269 Ok(None)
270 }
271 async fn execute_tool_call(
272 &self,
273 call: &ToolCall,
274 ) -> Result<Option<ToolOutput>, ToolError> {
275 Ok(Some(ToolOutput {
276 tool_name: call.tool_id.clone(),
277 summary: "ok".into(),
278 blocks_executed: 1,
279 filter_stats: None,
280 diff: None,
281 streamed: false,
282 terminal_id: None,
283 locations: None,
284 raw_response: None,
285 claim_source: None,
286 }))
287 }
288 }
289
290 fn make_call(tool_id: &str) -> ToolCall {
291 ToolCall {
292 tool_id: tool_id.into(),
293 params: serde_json::Map::new(),
294 }
295 }
296
297 fn make_call_with_cmd(tool_id: &str, cmd: &str) -> ToolCall {
298 let mut params = serde_json::Map::new();
299 params.insert("command".into(), serde_json::Value::String(cmd.into()));
300 ToolCall {
301 tool_id: tool_id.into(),
302 params,
303 }
304 }
305
306 #[tokio::test]
307 async fn trusted_allows_all() {
308 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
309 gate.set_effective_trust(TrustLevel::Trusted);
310
311 let result = gate.execute_tool_call(&make_call("bash")).await;
312 assert!(result.is_ok());
314 }
315
316 #[tokio::test]
317 async fn quarantined_denies_bash() {
318 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
319 gate.set_effective_trust(TrustLevel::Quarantined);
320
321 let result = gate.execute_tool_call(&make_call("bash")).await;
322 assert!(matches!(result, Err(ToolError::Blocked { .. })));
323 }
324
325 #[tokio::test]
326 async fn quarantined_denies_write() {
327 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
328 gate.set_effective_trust(TrustLevel::Quarantined);
329
330 let result = gate.execute_tool_call(&make_call("write")).await;
331 assert!(matches!(result, Err(ToolError::Blocked { .. })));
332 }
333
334 #[tokio::test]
335 async fn quarantined_denies_edit() {
336 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
337 gate.set_effective_trust(TrustLevel::Quarantined);
338
339 let result = gate.execute_tool_call(&make_call("edit")).await;
340 assert!(matches!(result, Err(ToolError::Blocked { .. })));
341 }
342
343 #[tokio::test]
344 async fn quarantined_denies_delete_path() {
345 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
346 gate.set_effective_trust(TrustLevel::Quarantined);
347
348 let result = gate.execute_tool_call(&make_call("delete_path")).await;
349 assert!(matches!(result, Err(ToolError::Blocked { .. })));
350 }
351
352 #[tokio::test]
353 async fn quarantined_denies_fetch() {
354 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
355 gate.set_effective_trust(TrustLevel::Quarantined);
356
357 let result = gate.execute_tool_call(&make_call("fetch")).await;
358 assert!(matches!(result, Err(ToolError::Blocked { .. })));
359 }
360
361 #[tokio::test]
362 async fn quarantined_denies_memory_save() {
363 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
364 gate.set_effective_trust(TrustLevel::Quarantined);
365
366 let result = gate.execute_tool_call(&make_call("memory_save")).await;
367 assert!(matches!(result, Err(ToolError::Blocked { .. })));
368 }
369
370 #[tokio::test]
371 async fn quarantined_allows_read() {
372 let policy = crate::permissions::PermissionPolicy::from_legacy(&[], &[]);
373 let gate = TrustGateExecutor::new(MockExecutor, policy);
374 gate.set_effective_trust(TrustLevel::Quarantined);
375
376 let result = gate.execute_tool_call(&make_call("read")).await;
378 assert!(result.is_ok());
379 }
380
381 #[tokio::test]
382 async fn quarantined_allows_file_read() {
383 let policy = crate::permissions::PermissionPolicy::from_legacy(&[], &[]);
384 let gate = TrustGateExecutor::new(MockExecutor, policy);
385 gate.set_effective_trust(TrustLevel::Quarantined);
386
387 let result = gate.execute_tool_call(&make_call("file_read")).await;
388 assert!(result.is_ok());
390 }
391
392 #[tokio::test]
393 async fn blocked_denies_everything() {
394 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
395 gate.set_effective_trust(TrustLevel::Blocked);
396
397 let result = gate.execute_tool_call(&make_call("file_read")).await;
398 assert!(matches!(result, Err(ToolError::Blocked { .. })));
399 }
400
401 #[tokio::test]
402 async fn policy_deny_overrides_trust() {
403 let policy = crate::permissions::PermissionPolicy::from_legacy(&["sudo".into()], &[]);
404 let gate = TrustGateExecutor::new(MockExecutor, policy);
405 gate.set_effective_trust(TrustLevel::Trusted);
406
407 let result = gate
408 .execute_tool_call(&make_call_with_cmd("bash", "sudo rm"))
409 .await;
410 assert!(matches!(result, Err(ToolError::Blocked { .. })));
411 }
412
413 #[tokio::test]
414 async fn blocked_denies_execute() {
415 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
416 gate.set_effective_trust(TrustLevel::Blocked);
417
418 let result = gate.execute("some response").await;
419 assert!(matches!(result, Err(ToolError::Blocked { .. })));
420 }
421
422 #[tokio::test]
423 async fn blocked_denies_execute_confirmed() {
424 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
425 gate.set_effective_trust(TrustLevel::Blocked);
426
427 let result = gate.execute_confirmed("some response").await;
428 assert!(matches!(result, Err(ToolError::Blocked { .. })));
429 }
430
431 #[tokio::test]
432 async fn trusted_allows_execute() {
433 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
434 gate.set_effective_trust(TrustLevel::Trusted);
435
436 let result = gate.execute("some response").await;
437 assert!(result.is_ok());
438 }
439
440 #[tokio::test]
441 async fn verified_with_allow_policy_succeeds() {
442 let policy = crate::permissions::PermissionPolicy::from_legacy(&[], &[]);
443 let gate = TrustGateExecutor::new(MockExecutor, policy);
444 gate.set_effective_trust(TrustLevel::Verified);
445
446 let result = gate
447 .execute_tool_call(&make_call_with_cmd("bash", "echo hi"))
448 .await
449 .unwrap();
450 assert!(result.is_some());
451 }
452
453 #[tokio::test]
454 async fn quarantined_denies_web_scrape() {
455 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
456 gate.set_effective_trust(TrustLevel::Quarantined);
457
458 let result = gate.execute_tool_call(&make_call("web_scrape")).await;
459 assert!(matches!(result, Err(ToolError::Blocked { .. })));
460 }
461
462 #[derive(Debug)]
463 struct EnvCapture {
464 captured: std::sync::Mutex<Option<std::collections::HashMap<String, String>>>,
465 }
466 impl EnvCapture {
467 fn new() -> Self {
468 Self {
469 captured: std::sync::Mutex::new(None),
470 }
471 }
472 }
473 impl ToolExecutor for EnvCapture {
474 async fn execute(&self, _: &str) -> Result<Option<ToolOutput>, ToolError> {
475 Ok(None)
476 }
477 async fn execute_tool_call(&self, _: &ToolCall) -> Result<Option<ToolOutput>, ToolError> {
478 Ok(None)
479 }
480 fn set_skill_env(&self, env: Option<std::collections::HashMap<String, String>>) {
481 *self.captured.lock().unwrap() = env;
482 }
483 }
484
485 #[test]
486 fn is_tool_retryable_delegated_to_inner() {
487 #[derive(Debug)]
488 struct RetryableExecutor;
489 impl ToolExecutor for RetryableExecutor {
490 async fn execute(&self, _: &str) -> Result<Option<ToolOutput>, ToolError> {
491 Ok(None)
492 }
493 async fn execute_tool_call(
494 &self,
495 _: &ToolCall,
496 ) -> Result<Option<ToolOutput>, ToolError> {
497 Ok(None)
498 }
499 fn is_tool_retryable(&self, tool_id: &str) -> bool {
500 tool_id == "fetch"
501 }
502 }
503 let gate = TrustGateExecutor::new(RetryableExecutor, PermissionPolicy::default());
504 assert!(gate.is_tool_retryable("fetch"));
505 assert!(!gate.is_tool_retryable("bash"));
506 }
507
508 #[test]
509 fn set_skill_env_forwarded_to_inner() {
510 let inner = EnvCapture::new();
511 let gate = TrustGateExecutor::new(inner, PermissionPolicy::default());
512
513 let mut env = std::collections::HashMap::new();
514 env.insert("MY_VAR".to_owned(), "42".to_owned());
515 gate.set_skill_env(Some(env.clone()));
516
517 let captured = gate.inner.captured.lock().unwrap();
518 assert_eq!(*captured, Some(env));
519 }
520
521 #[tokio::test]
522 async fn mcp_tool_supervised_no_rules_allows() {
523 let policy = crate::permissions::PermissionPolicy::from_legacy(&[], &[]);
525 let gate = TrustGateExecutor::new(MockExecutor, policy);
526 gate.set_effective_trust(TrustLevel::Trusted);
527
528 let mut params = serde_json::Map::new();
529 params.insert(
530 "file_path".into(),
531 serde_json::Value::String("/tmp/test.txt".into()),
532 );
533 let call = ToolCall {
534 tool_id: "mcp_filesystem__read_file".into(),
535 params,
536 };
537 let result = gate.execute_tool_call(&call).await;
538 assert!(
539 result.is_ok(),
540 "MCP tool should be allowed when no rules exist"
541 );
542 }
543
544 #[tokio::test]
545 async fn bash_with_explicit_deny_rule_blocked() {
546 let policy = crate::permissions::PermissionPolicy::from_legacy(&["sudo".into()], &[]);
548 let gate = TrustGateExecutor::new(MockExecutor, policy);
549 gate.set_effective_trust(TrustLevel::Trusted);
550
551 let result = gate
552 .execute_tool_call(&make_call_with_cmd("bash", "sudo apt install vim"))
553 .await;
554 assert!(
555 matches!(result, Err(ToolError::Blocked { .. })),
556 "bash with explicit deny rule should be blocked"
557 );
558 }
559
560 #[tokio::test]
561 async fn bash_with_explicit_allow_rule_succeeds() {
562 let policy = crate::permissions::PermissionPolicy::from_legacy(&[], &[]);
564 let gate = TrustGateExecutor::new(MockExecutor, policy);
565 gate.set_effective_trust(TrustLevel::Trusted);
566
567 let result = gate
568 .execute_tool_call(&make_call_with_cmd("bash", "echo hello"))
569 .await;
570 assert!(
571 result.is_ok(),
572 "bash with explicit allow rule should succeed"
573 );
574 }
575
576 #[tokio::test]
577 async fn readonly_denies_mcp_tool_not_in_allowlist() {
578 let policy =
580 crate::permissions::PermissionPolicy::default().with_autonomy(AutonomyLevel::ReadOnly);
581 let gate = TrustGateExecutor::new(MockExecutor, policy);
582 gate.set_effective_trust(TrustLevel::Trusted);
583
584 let result = gate
585 .execute_tool_call(&make_call("mcpls_get_diagnostics"))
586 .await;
587 assert!(
588 matches!(result, Err(ToolError::Blocked { .. })),
589 "ReadOnly mode must deny non-allowlisted tools"
590 );
591 }
592
593 #[test]
594 fn set_effective_trust_interior_mutability() {
595 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
596 assert_eq!(gate.effective_trust(), TrustLevel::Trusted);
597
598 gate.set_effective_trust(TrustLevel::Quarantined);
599 assert_eq!(gate.effective_trust(), TrustLevel::Quarantined);
600
601 gate.set_effective_trust(TrustLevel::Blocked);
602 assert_eq!(gate.effective_trust(), TrustLevel::Blocked);
603
604 gate.set_effective_trust(TrustLevel::Trusted);
605 assert_eq!(gate.effective_trust(), TrustLevel::Trusted);
606 }
607
608 #[test]
611 fn is_quarantine_denied_exact_match() {
612 assert!(is_quarantine_denied("bash"));
613 assert!(is_quarantine_denied("write"));
614 assert!(is_quarantine_denied("fetch"));
615 assert!(is_quarantine_denied("memory_save"));
616 assert!(is_quarantine_denied("delete_path"));
617 assert!(is_quarantine_denied("create_directory"));
618 }
619
620 #[test]
621 fn is_quarantine_denied_suffix_match_mcp_write() {
622 assert!(is_quarantine_denied("filesystem_write"));
624 assert!(!is_quarantine_denied("filesystem_write_file"));
626 }
627
628 #[test]
629 fn is_quarantine_denied_suffix_mcp_bash() {
630 assert!(is_quarantine_denied("shell_bash"));
631 assert!(is_quarantine_denied("mcp_shell_bash"));
632 }
633
634 #[test]
635 fn is_quarantine_denied_suffix_mcp_fetch() {
636 assert!(is_quarantine_denied("http_fetch"));
637 assert!(!is_quarantine_denied("server_prefetch"));
639 }
640
641 #[test]
642 fn is_quarantine_denied_suffix_mcp_memory_save() {
643 assert!(is_quarantine_denied("server_memory_save"));
644 assert!(!is_quarantine_denied("server_save"));
646 }
647
648 #[test]
649 fn is_quarantine_denied_suffix_mcp_delete_path() {
650 assert!(is_quarantine_denied("fs_delete_path"));
651 assert!(is_quarantine_denied("fs_not_delete_path"));
653 }
654
655 #[test]
656 fn is_quarantine_denied_substring_not_suffix() {
657 assert!(!is_quarantine_denied("write_log"));
659 }
660
661 #[test]
662 fn is_quarantine_denied_read_only_tools_allowed() {
663 assert!(!is_quarantine_denied("filesystem_read_file"));
664 assert!(!is_quarantine_denied("filesystem_list_dir"));
665 assert!(!is_quarantine_denied("read"));
666 assert!(!is_quarantine_denied("file_read"));
667 }
668
669 #[tokio::test]
670 async fn quarantined_denies_mcp_write_tool() {
671 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
672 gate.set_effective_trust(TrustLevel::Quarantined);
673
674 let result = gate.execute_tool_call(&make_call("filesystem_write")).await;
675 assert!(matches!(result, Err(ToolError::Blocked { .. })));
676 }
677
678 #[tokio::test]
679 async fn quarantined_allows_mcp_read_file() {
680 let policy = crate::permissions::PermissionPolicy::from_legacy(&[], &[]);
681 let gate = TrustGateExecutor::new(MockExecutor, policy);
682 gate.set_effective_trust(TrustLevel::Quarantined);
683
684 let result = gate
685 .execute_tool_call(&make_call("filesystem_read_file"))
686 .await;
687 assert!(result.is_ok());
688 }
689
690 #[tokio::test]
691 async fn quarantined_denies_mcp_bash_tool() {
692 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
693 gate.set_effective_trust(TrustLevel::Quarantined);
694
695 let result = gate.execute_tool_call(&make_call("shell_bash")).await;
696 assert!(matches!(result, Err(ToolError::Blocked { .. })));
697 }
698
699 #[tokio::test]
700 async fn quarantined_denies_mcp_memory_save() {
701 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
702 gate.set_effective_trust(TrustLevel::Quarantined);
703
704 let result = gate
705 .execute_tool_call(&make_call("server_memory_save"))
706 .await;
707 assert!(matches!(result, Err(ToolError::Blocked { .. })));
708 }
709
710 #[tokio::test]
711 async fn quarantined_denies_mcp_confirmed_path() {
712 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
714 gate.set_effective_trust(TrustLevel::Quarantined);
715
716 let result = gate
717 .execute_tool_call_confirmed(&make_call("filesystem_write"))
718 .await;
719 assert!(matches!(result, Err(ToolError::Blocked { .. })));
720 }
721
722 fn gate_with_mcp_ids(ids: &[&str]) -> TrustGateExecutor<MockExecutor> {
725 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
726 let handle = gate.mcp_tool_ids_handle();
727 let set: std::collections::HashSet<String> = ids.iter().map(ToString::to_string).collect();
728 *handle.write().unwrap() = set;
729 gate
730 }
731
732 #[tokio::test]
733 async fn quarantined_denies_registered_mcp_tool_novel_name() {
734 let gate = gate_with_mcp_ids(&["github_run_command"]);
736 gate.set_effective_trust(TrustLevel::Quarantined);
737
738 let result = gate
739 .execute_tool_call(&make_call("github_run_command"))
740 .await;
741 assert!(matches!(result, Err(ToolError::Blocked { .. })));
742 }
743
744 #[tokio::test]
745 async fn quarantined_denies_registered_mcp_tool_execute() {
746 let gate = gate_with_mcp_ids(&["shell_execute"]);
748 gate.set_effective_trust(TrustLevel::Quarantined);
749
750 let result = gate.execute_tool_call(&make_call("shell_execute")).await;
751 assert!(matches!(result, Err(ToolError::Blocked { .. })));
752 }
753
754 #[tokio::test]
755 async fn quarantined_allows_unregistered_tool_not_in_denied_list() {
756 let gate = gate_with_mcp_ids(&["other_tool"]);
758 gate.set_effective_trust(TrustLevel::Quarantined);
759
760 let result = gate.execute_tool_call(&make_call("read")).await;
761 assert!(result.is_ok());
762 }
763
764 #[tokio::test]
765 async fn trusted_allows_registered_mcp_tool() {
766 let gate = gate_with_mcp_ids(&["github_run_command"]);
768 gate.set_effective_trust(TrustLevel::Trusted);
769
770 let result = gate
771 .execute_tool_call(&make_call("github_run_command"))
772 .await;
773 assert!(result.is_ok());
774 }
775
776 #[tokio::test]
777 async fn quarantined_denies_mcp_tool_via_confirmed_path() {
778 let gate = gate_with_mcp_ids(&["docker_container_exec"]);
780 gate.set_effective_trust(TrustLevel::Quarantined);
781
782 let result = gate
783 .execute_tool_call_confirmed(&make_call("docker_container_exec"))
784 .await;
785 assert!(matches!(result, Err(ToolError::Blocked { .. })));
786 }
787
788 #[test]
789 fn mcp_tool_ids_handle_shared_arc() {
790 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
791 let handle = gate.mcp_tool_ids_handle();
792 handle.write().unwrap().insert("test_tool".to_owned());
793 assert!(gate.is_mcp_tool("test_tool"));
794 assert!(!gate.is_mcp_tool("other_tool"));
795 }
796}