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