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 set_effective_trust(&self, level: crate::TrustLevel) {
249 self.effective_trust
250 .store(trust_to_u8(level), Ordering::Relaxed);
251 }
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257
258 #[derive(Debug)]
259 struct MockExecutor;
260 impl ToolExecutor for MockExecutor {
261 async fn execute(&self, _: &str) -> Result<Option<ToolOutput>, ToolError> {
262 Ok(None)
263 }
264 async fn execute_tool_call(
265 &self,
266 call: &ToolCall,
267 ) -> Result<Option<ToolOutput>, ToolError> {
268 Ok(Some(ToolOutput {
269 tool_name: call.tool_id.clone(),
270 summary: "ok".into(),
271 blocks_executed: 1,
272 filter_stats: None,
273 diff: None,
274 streamed: false,
275 terminal_id: None,
276 locations: None,
277 raw_response: None,
278 }))
279 }
280 }
281
282 fn make_call(tool_id: &str) -> ToolCall {
283 ToolCall {
284 tool_id: tool_id.into(),
285 params: serde_json::Map::new(),
286 }
287 }
288
289 fn make_call_with_cmd(tool_id: &str, cmd: &str) -> ToolCall {
290 let mut params = serde_json::Map::new();
291 params.insert("command".into(), serde_json::Value::String(cmd.into()));
292 ToolCall {
293 tool_id: tool_id.into(),
294 params,
295 }
296 }
297
298 #[tokio::test]
299 async fn trusted_allows_all() {
300 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
301 gate.set_effective_trust(TrustLevel::Trusted);
302
303 let result = gate.execute_tool_call(&make_call("bash")).await;
304 assert!(result.is_ok());
306 }
307
308 #[tokio::test]
309 async fn quarantined_denies_bash() {
310 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
311 gate.set_effective_trust(TrustLevel::Quarantined);
312
313 let result = gate.execute_tool_call(&make_call("bash")).await;
314 assert!(matches!(result, Err(ToolError::Blocked { .. })));
315 }
316
317 #[tokio::test]
318 async fn quarantined_denies_write() {
319 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
320 gate.set_effective_trust(TrustLevel::Quarantined);
321
322 let result = gate.execute_tool_call(&make_call("write")).await;
323 assert!(matches!(result, Err(ToolError::Blocked { .. })));
324 }
325
326 #[tokio::test]
327 async fn quarantined_denies_edit() {
328 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
329 gate.set_effective_trust(TrustLevel::Quarantined);
330
331 let result = gate.execute_tool_call(&make_call("edit")).await;
332 assert!(matches!(result, Err(ToolError::Blocked { .. })));
333 }
334
335 #[tokio::test]
336 async fn quarantined_denies_delete_path() {
337 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
338 gate.set_effective_trust(TrustLevel::Quarantined);
339
340 let result = gate.execute_tool_call(&make_call("delete_path")).await;
341 assert!(matches!(result, Err(ToolError::Blocked { .. })));
342 }
343
344 #[tokio::test]
345 async fn quarantined_denies_fetch() {
346 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
347 gate.set_effective_trust(TrustLevel::Quarantined);
348
349 let result = gate.execute_tool_call(&make_call("fetch")).await;
350 assert!(matches!(result, Err(ToolError::Blocked { .. })));
351 }
352
353 #[tokio::test]
354 async fn quarantined_denies_memory_save() {
355 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
356 gate.set_effective_trust(TrustLevel::Quarantined);
357
358 let result = gate.execute_tool_call(&make_call("memory_save")).await;
359 assert!(matches!(result, Err(ToolError::Blocked { .. })));
360 }
361
362 #[tokio::test]
363 async fn quarantined_allows_read() {
364 let policy = crate::permissions::PermissionPolicy::from_legacy(&[], &[]);
365 let gate = TrustGateExecutor::new(MockExecutor, policy);
366 gate.set_effective_trust(TrustLevel::Quarantined);
367
368 let result = gate.execute_tool_call(&make_call("read")).await;
370 assert!(result.is_ok());
371 }
372
373 #[tokio::test]
374 async fn quarantined_allows_file_read() {
375 let policy = crate::permissions::PermissionPolicy::from_legacy(&[], &[]);
376 let gate = TrustGateExecutor::new(MockExecutor, policy);
377 gate.set_effective_trust(TrustLevel::Quarantined);
378
379 let result = gate.execute_tool_call(&make_call("file_read")).await;
380 assert!(result.is_ok());
382 }
383
384 #[tokio::test]
385 async fn blocked_denies_everything() {
386 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
387 gate.set_effective_trust(TrustLevel::Blocked);
388
389 let result = gate.execute_tool_call(&make_call("file_read")).await;
390 assert!(matches!(result, Err(ToolError::Blocked { .. })));
391 }
392
393 #[tokio::test]
394 async fn policy_deny_overrides_trust() {
395 let policy = crate::permissions::PermissionPolicy::from_legacy(&["sudo".into()], &[]);
396 let gate = TrustGateExecutor::new(MockExecutor, policy);
397 gate.set_effective_trust(TrustLevel::Trusted);
398
399 let result = gate
400 .execute_tool_call(&make_call_with_cmd("bash", "sudo rm"))
401 .await;
402 assert!(matches!(result, Err(ToolError::Blocked { .. })));
403 }
404
405 #[tokio::test]
406 async fn blocked_denies_execute() {
407 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
408 gate.set_effective_trust(TrustLevel::Blocked);
409
410 let result = gate.execute("some response").await;
411 assert!(matches!(result, Err(ToolError::Blocked { .. })));
412 }
413
414 #[tokio::test]
415 async fn blocked_denies_execute_confirmed() {
416 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
417 gate.set_effective_trust(TrustLevel::Blocked);
418
419 let result = gate.execute_confirmed("some response").await;
420 assert!(matches!(result, Err(ToolError::Blocked { .. })));
421 }
422
423 #[tokio::test]
424 async fn trusted_allows_execute() {
425 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
426 gate.set_effective_trust(TrustLevel::Trusted);
427
428 let result = gate.execute("some response").await;
429 assert!(result.is_ok());
430 }
431
432 #[tokio::test]
433 async fn verified_with_allow_policy_succeeds() {
434 let policy = crate::permissions::PermissionPolicy::from_legacy(&[], &[]);
435 let gate = TrustGateExecutor::new(MockExecutor, policy);
436 gate.set_effective_trust(TrustLevel::Verified);
437
438 let result = gate
439 .execute_tool_call(&make_call_with_cmd("bash", "echo hi"))
440 .await
441 .unwrap();
442 assert!(result.is_some());
443 }
444
445 #[tokio::test]
446 async fn quarantined_denies_web_scrape() {
447 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
448 gate.set_effective_trust(TrustLevel::Quarantined);
449
450 let result = gate.execute_tool_call(&make_call("web_scrape")).await;
451 assert!(matches!(result, Err(ToolError::Blocked { .. })));
452 }
453
454 #[derive(Debug)]
455 struct EnvCapture {
456 captured: std::sync::Mutex<Option<std::collections::HashMap<String, String>>>,
457 }
458 impl EnvCapture {
459 fn new() -> Self {
460 Self {
461 captured: std::sync::Mutex::new(None),
462 }
463 }
464 }
465 impl ToolExecutor for EnvCapture {
466 async fn execute(&self, _: &str) -> Result<Option<ToolOutput>, ToolError> {
467 Ok(None)
468 }
469 async fn execute_tool_call(&self, _: &ToolCall) -> Result<Option<ToolOutput>, ToolError> {
470 Ok(None)
471 }
472 fn set_skill_env(&self, env: Option<std::collections::HashMap<String, String>>) {
473 *self.captured.lock().unwrap() = env;
474 }
475 }
476
477 #[test]
478 fn set_skill_env_forwarded_to_inner() {
479 let inner = EnvCapture::new();
480 let gate = TrustGateExecutor::new(inner, PermissionPolicy::default());
481
482 let mut env = std::collections::HashMap::new();
483 env.insert("MY_VAR".to_owned(), "42".to_owned());
484 gate.set_skill_env(Some(env.clone()));
485
486 let captured = gate.inner.captured.lock().unwrap();
487 assert_eq!(*captured, Some(env));
488 }
489
490 #[tokio::test]
491 async fn mcp_tool_supervised_no_rules_allows() {
492 let policy = crate::permissions::PermissionPolicy::from_legacy(&[], &[]);
494 let gate = TrustGateExecutor::new(MockExecutor, policy);
495 gate.set_effective_trust(TrustLevel::Trusted);
496
497 let mut params = serde_json::Map::new();
498 params.insert(
499 "file_path".into(),
500 serde_json::Value::String("/tmp/test.txt".into()),
501 );
502 let call = ToolCall {
503 tool_id: "mcp_filesystem__read_file".into(),
504 params,
505 };
506 let result = gate.execute_tool_call(&call).await;
507 assert!(
508 result.is_ok(),
509 "MCP tool should be allowed when no rules exist"
510 );
511 }
512
513 #[tokio::test]
514 async fn bash_with_explicit_deny_rule_blocked() {
515 let policy = crate::permissions::PermissionPolicy::from_legacy(&["sudo".into()], &[]);
517 let gate = TrustGateExecutor::new(MockExecutor, policy);
518 gate.set_effective_trust(TrustLevel::Trusted);
519
520 let result = gate
521 .execute_tool_call(&make_call_with_cmd("bash", "sudo apt install vim"))
522 .await;
523 assert!(
524 matches!(result, Err(ToolError::Blocked { .. })),
525 "bash with explicit deny rule should be blocked"
526 );
527 }
528
529 #[tokio::test]
530 async fn bash_with_explicit_allow_rule_succeeds() {
531 let policy = crate::permissions::PermissionPolicy::from_legacy(&[], &[]);
533 let gate = TrustGateExecutor::new(MockExecutor, policy);
534 gate.set_effective_trust(TrustLevel::Trusted);
535
536 let result = gate
537 .execute_tool_call(&make_call_with_cmd("bash", "echo hello"))
538 .await;
539 assert!(
540 result.is_ok(),
541 "bash with explicit allow rule should succeed"
542 );
543 }
544
545 #[tokio::test]
546 async fn readonly_denies_mcp_tool_not_in_allowlist() {
547 let policy =
549 crate::permissions::PermissionPolicy::default().with_autonomy(AutonomyLevel::ReadOnly);
550 let gate = TrustGateExecutor::new(MockExecutor, policy);
551 gate.set_effective_trust(TrustLevel::Trusted);
552
553 let result = gate
554 .execute_tool_call(&make_call("mcpls_get_diagnostics"))
555 .await;
556 assert!(
557 matches!(result, Err(ToolError::Blocked { .. })),
558 "ReadOnly mode must deny non-allowlisted tools"
559 );
560 }
561
562 #[test]
563 fn set_effective_trust_interior_mutability() {
564 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
565 assert_eq!(gate.effective_trust(), TrustLevel::Trusted);
566
567 gate.set_effective_trust(TrustLevel::Quarantined);
568 assert_eq!(gate.effective_trust(), TrustLevel::Quarantined);
569
570 gate.set_effective_trust(TrustLevel::Blocked);
571 assert_eq!(gate.effective_trust(), TrustLevel::Blocked);
572
573 gate.set_effective_trust(TrustLevel::Trusted);
574 assert_eq!(gate.effective_trust(), TrustLevel::Trusted);
575 }
576
577 #[test]
580 fn is_quarantine_denied_exact_match() {
581 assert!(is_quarantine_denied("bash"));
582 assert!(is_quarantine_denied("write"));
583 assert!(is_quarantine_denied("fetch"));
584 assert!(is_quarantine_denied("memory_save"));
585 assert!(is_quarantine_denied("delete_path"));
586 assert!(is_quarantine_denied("create_directory"));
587 }
588
589 #[test]
590 fn is_quarantine_denied_suffix_match_mcp_write() {
591 assert!(is_quarantine_denied("filesystem_write"));
593 assert!(!is_quarantine_denied("filesystem_write_file"));
595 }
596
597 #[test]
598 fn is_quarantine_denied_suffix_mcp_bash() {
599 assert!(is_quarantine_denied("shell_bash"));
600 assert!(is_quarantine_denied("mcp_shell_bash"));
601 }
602
603 #[test]
604 fn is_quarantine_denied_suffix_mcp_fetch() {
605 assert!(is_quarantine_denied("http_fetch"));
606 assert!(!is_quarantine_denied("server_prefetch"));
608 }
609
610 #[test]
611 fn is_quarantine_denied_suffix_mcp_memory_save() {
612 assert!(is_quarantine_denied("server_memory_save"));
613 assert!(!is_quarantine_denied("server_save"));
615 }
616
617 #[test]
618 fn is_quarantine_denied_suffix_mcp_delete_path() {
619 assert!(is_quarantine_denied("fs_delete_path"));
620 assert!(is_quarantine_denied("fs_not_delete_path"));
622 }
623
624 #[test]
625 fn is_quarantine_denied_substring_not_suffix() {
626 assert!(!is_quarantine_denied("write_log"));
628 }
629
630 #[test]
631 fn is_quarantine_denied_read_only_tools_allowed() {
632 assert!(!is_quarantine_denied("filesystem_read_file"));
633 assert!(!is_quarantine_denied("filesystem_list_dir"));
634 assert!(!is_quarantine_denied("read"));
635 assert!(!is_quarantine_denied("file_read"));
636 }
637
638 #[tokio::test]
639 async fn quarantined_denies_mcp_write_tool() {
640 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
641 gate.set_effective_trust(TrustLevel::Quarantined);
642
643 let result = gate.execute_tool_call(&make_call("filesystem_write")).await;
644 assert!(matches!(result, Err(ToolError::Blocked { .. })));
645 }
646
647 #[tokio::test]
648 async fn quarantined_allows_mcp_read_file() {
649 let policy = crate::permissions::PermissionPolicy::from_legacy(&[], &[]);
650 let gate = TrustGateExecutor::new(MockExecutor, policy);
651 gate.set_effective_trust(TrustLevel::Quarantined);
652
653 let result = gate
654 .execute_tool_call(&make_call("filesystem_read_file"))
655 .await;
656 assert!(result.is_ok());
657 }
658
659 #[tokio::test]
660 async fn quarantined_denies_mcp_bash_tool() {
661 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
662 gate.set_effective_trust(TrustLevel::Quarantined);
663
664 let result = gate.execute_tool_call(&make_call("shell_bash")).await;
665 assert!(matches!(result, Err(ToolError::Blocked { .. })));
666 }
667
668 #[tokio::test]
669 async fn quarantined_denies_mcp_memory_save() {
670 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
671 gate.set_effective_trust(TrustLevel::Quarantined);
672
673 let result = gate
674 .execute_tool_call(&make_call("server_memory_save"))
675 .await;
676 assert!(matches!(result, Err(ToolError::Blocked { .. })));
677 }
678
679 #[tokio::test]
680 async fn quarantined_denies_mcp_confirmed_path() {
681 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
683 gate.set_effective_trust(TrustLevel::Quarantined);
684
685 let result = gate
686 .execute_tool_call_confirmed(&make_call("filesystem_write"))
687 .await;
688 assert!(matches!(result, Err(ToolError::Blocked { .. })));
689 }
690
691 fn gate_with_mcp_ids(ids: &[&str]) -> TrustGateExecutor<MockExecutor> {
694 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
695 let handle = gate.mcp_tool_ids_handle();
696 let set: std::collections::HashSet<String> = ids.iter().map(|s| s.to_string()).collect();
697 *handle.write().unwrap() = set;
698 gate
699 }
700
701 #[tokio::test]
702 async fn quarantined_denies_registered_mcp_tool_novel_name() {
703 let gate = gate_with_mcp_ids(&["github_run_command"]);
705 gate.set_effective_trust(TrustLevel::Quarantined);
706
707 let result = gate
708 .execute_tool_call(&make_call("github_run_command"))
709 .await;
710 assert!(matches!(result, Err(ToolError::Blocked { .. })));
711 }
712
713 #[tokio::test]
714 async fn quarantined_denies_registered_mcp_tool_execute() {
715 let gate = gate_with_mcp_ids(&["shell_execute"]);
717 gate.set_effective_trust(TrustLevel::Quarantined);
718
719 let result = gate.execute_tool_call(&make_call("shell_execute")).await;
720 assert!(matches!(result, Err(ToolError::Blocked { .. })));
721 }
722
723 #[tokio::test]
724 async fn quarantined_allows_unregistered_tool_not_in_denied_list() {
725 let gate = gate_with_mcp_ids(&["other_tool"]);
727 gate.set_effective_trust(TrustLevel::Quarantined);
728
729 let result = gate.execute_tool_call(&make_call("read")).await;
730 assert!(result.is_ok());
731 }
732
733 #[tokio::test]
734 async fn trusted_allows_registered_mcp_tool() {
735 let gate = gate_with_mcp_ids(&["github_run_command"]);
737 gate.set_effective_trust(TrustLevel::Trusted);
738
739 let result = gate
740 .execute_tool_call(&make_call("github_run_command"))
741 .await;
742 assert!(result.is_ok());
743 }
744
745 #[tokio::test]
746 async fn quarantined_denies_mcp_tool_via_confirmed_path() {
747 let gate = gate_with_mcp_ids(&["docker_container_exec"]);
749 gate.set_effective_trust(TrustLevel::Quarantined);
750
751 let result = gate
752 .execute_tool_call_confirmed(&make_call("docker_container_exec"))
753 .await;
754 assert!(matches!(result, Err(ToolError::Blocked { .. })));
755 }
756
757 #[test]
758 fn mcp_tool_ids_handle_shared_arc() {
759 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
760 let handle = gate.mcp_tool_ids_handle();
761 handle.write().unwrap().insert("test_tool".to_owned());
762 assert!(gate.is_mcp_tool("test_tool"));
763 assert!(!gate.is_mcp_tool("other_tool"));
764 }
765}