1use std::collections::HashSet;
7use std::sync::{
8 Arc, RwLock,
9 atomic::{AtomicU8, Ordering},
10};
11
12use crate::SkillTrustLevel;
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: SkillTrustLevel) -> u8 {
55 match level {
56 SkillTrustLevel::Trusted => 0,
57 SkillTrustLevel::Verified => 1,
58 SkillTrustLevel::Quarantined => 2,
59 SkillTrustLevel::Blocked => 3,
60 }
61}
62
63fn u8_to_trust(v: u8) -> SkillTrustLevel {
64 match v {
65 0 => SkillTrustLevel::Trusted,
66 1 => SkillTrustLevel::Verified,
67 2 => SkillTrustLevel::Quarantined,
68 _ => SkillTrustLevel::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(SkillTrustLevel::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: SkillTrustLevel) {
115 self.effective_trust
116 .store(trust_to_u8(level), Ordering::Relaxed);
117 }
118
119 #[must_use]
120 pub fn effective_trust(&self) -> SkillTrustLevel {
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 SkillTrustLevel::Blocked => {
134 return Err(ToolError::Blocked {
135 command: "all tools blocked (trust=blocked)".to_owned(),
136 });
137 }
138 SkillTrustLevel::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 SkillTrustLevel::Trusted | SkillTrustLevel::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 SkillTrustLevel::Blocked | SkillTrustLevel::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 SkillTrustLevel::Trusted | SkillTrustLevel::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 SkillTrustLevel::Blocked | SkillTrustLevel::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 SkillTrustLevel::Trusted | SkillTrustLevel::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 SkillTrustLevel::Blocked => {
231 return Err(ToolError::Blocked {
232 command: "all tools blocked (trust=blocked)".to_owned(),
233 });
234 }
235 SkillTrustLevel::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 SkillTrustLevel::Trusted | SkillTrustLevel::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::SkillTrustLevel) {
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 caller_id: None,
295 }
296 }
297
298 fn make_call_with_cmd(tool_id: &str, cmd: &str) -> ToolCall {
299 let mut params = serde_json::Map::new();
300 params.insert("command".into(), serde_json::Value::String(cmd.into()));
301 ToolCall {
302 tool_id: tool_id.into(),
303 params,
304 caller_id: None,
305 }
306 }
307
308 #[tokio::test]
309 async fn trusted_allows_all() {
310 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
311 gate.set_effective_trust(SkillTrustLevel::Trusted);
312
313 let result = gate.execute_tool_call(&make_call("bash")).await;
314 assert!(result.is_ok());
316 }
317
318 #[tokio::test]
319 async fn quarantined_denies_bash() {
320 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
321 gate.set_effective_trust(SkillTrustLevel::Quarantined);
322
323 let result = gate.execute_tool_call(&make_call("bash")).await;
324 assert!(matches!(result, Err(ToolError::Blocked { .. })));
325 }
326
327 #[tokio::test]
328 async fn quarantined_denies_write() {
329 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
330 gate.set_effective_trust(SkillTrustLevel::Quarantined);
331
332 let result = gate.execute_tool_call(&make_call("write")).await;
333 assert!(matches!(result, Err(ToolError::Blocked { .. })));
334 }
335
336 #[tokio::test]
337 async fn quarantined_denies_edit() {
338 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
339 gate.set_effective_trust(SkillTrustLevel::Quarantined);
340
341 let result = gate.execute_tool_call(&make_call("edit")).await;
342 assert!(matches!(result, Err(ToolError::Blocked { .. })));
343 }
344
345 #[tokio::test]
346 async fn quarantined_denies_delete_path() {
347 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
348 gate.set_effective_trust(SkillTrustLevel::Quarantined);
349
350 let result = gate.execute_tool_call(&make_call("delete_path")).await;
351 assert!(matches!(result, Err(ToolError::Blocked { .. })));
352 }
353
354 #[tokio::test]
355 async fn quarantined_denies_fetch() {
356 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
357 gate.set_effective_trust(SkillTrustLevel::Quarantined);
358
359 let result = gate.execute_tool_call(&make_call("fetch")).await;
360 assert!(matches!(result, Err(ToolError::Blocked { .. })));
361 }
362
363 #[tokio::test]
364 async fn quarantined_denies_memory_save() {
365 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
366 gate.set_effective_trust(SkillTrustLevel::Quarantined);
367
368 let result = gate.execute_tool_call(&make_call("memory_save")).await;
369 assert!(matches!(result, Err(ToolError::Blocked { .. })));
370 }
371
372 #[tokio::test]
373 async fn quarantined_allows_read() {
374 let policy = crate::permissions::PermissionPolicy::from_legacy(&[], &[]);
375 let gate = TrustGateExecutor::new(MockExecutor, policy);
376 gate.set_effective_trust(SkillTrustLevel::Quarantined);
377
378 let result = gate.execute_tool_call(&make_call("read")).await;
380 assert!(result.is_ok());
381 }
382
383 #[tokio::test]
384 async fn quarantined_allows_file_read() {
385 let policy = crate::permissions::PermissionPolicy::from_legacy(&[], &[]);
386 let gate = TrustGateExecutor::new(MockExecutor, policy);
387 gate.set_effective_trust(SkillTrustLevel::Quarantined);
388
389 let result = gate.execute_tool_call(&make_call("file_read")).await;
390 assert!(result.is_ok());
392 }
393
394 #[tokio::test]
395 async fn blocked_denies_everything() {
396 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
397 gate.set_effective_trust(SkillTrustLevel::Blocked);
398
399 let result = gate.execute_tool_call(&make_call("file_read")).await;
400 assert!(matches!(result, Err(ToolError::Blocked { .. })));
401 }
402
403 #[tokio::test]
404 async fn policy_deny_overrides_trust() {
405 let policy = crate::permissions::PermissionPolicy::from_legacy(&["sudo".into()], &[]);
406 let gate = TrustGateExecutor::new(MockExecutor, policy);
407 gate.set_effective_trust(SkillTrustLevel::Trusted);
408
409 let result = gate
410 .execute_tool_call(&make_call_with_cmd("bash", "sudo rm"))
411 .await;
412 assert!(matches!(result, Err(ToolError::Blocked { .. })));
413 }
414
415 #[tokio::test]
416 async fn blocked_denies_execute() {
417 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
418 gate.set_effective_trust(SkillTrustLevel::Blocked);
419
420 let result = gate.execute("some response").await;
421 assert!(matches!(result, Err(ToolError::Blocked { .. })));
422 }
423
424 #[tokio::test]
425 async fn blocked_denies_execute_confirmed() {
426 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
427 gate.set_effective_trust(SkillTrustLevel::Blocked);
428
429 let result = gate.execute_confirmed("some response").await;
430 assert!(matches!(result, Err(ToolError::Blocked { .. })));
431 }
432
433 #[tokio::test]
434 async fn trusted_allows_execute() {
435 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
436 gate.set_effective_trust(SkillTrustLevel::Trusted);
437
438 let result = gate.execute("some response").await;
439 assert!(result.is_ok());
440 }
441
442 #[tokio::test]
443 async fn verified_with_allow_policy_succeeds() {
444 let policy = crate::permissions::PermissionPolicy::from_legacy(&[], &[]);
445 let gate = TrustGateExecutor::new(MockExecutor, policy);
446 gate.set_effective_trust(SkillTrustLevel::Verified);
447
448 let result = gate
449 .execute_tool_call(&make_call_with_cmd("bash", "echo hi"))
450 .await
451 .unwrap();
452 assert!(result.is_some());
453 }
454
455 #[tokio::test]
456 async fn quarantined_denies_web_scrape() {
457 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
458 gate.set_effective_trust(SkillTrustLevel::Quarantined);
459
460 let result = gate.execute_tool_call(&make_call("web_scrape")).await;
461 assert!(matches!(result, Err(ToolError::Blocked { .. })));
462 }
463
464 #[derive(Debug)]
465 struct EnvCapture {
466 captured: std::sync::Mutex<Option<std::collections::HashMap<String, String>>>,
467 }
468 impl EnvCapture {
469 fn new() -> Self {
470 Self {
471 captured: std::sync::Mutex::new(None),
472 }
473 }
474 }
475 impl ToolExecutor for EnvCapture {
476 async fn execute(&self, _: &str) -> Result<Option<ToolOutput>, ToolError> {
477 Ok(None)
478 }
479 async fn execute_tool_call(&self, _: &ToolCall) -> Result<Option<ToolOutput>, ToolError> {
480 Ok(None)
481 }
482 fn set_skill_env(&self, env: Option<std::collections::HashMap<String, String>>) {
483 *self.captured.lock().unwrap() = env;
484 }
485 }
486
487 #[test]
488 fn is_tool_retryable_delegated_to_inner() {
489 #[derive(Debug)]
490 struct RetryableExecutor;
491 impl ToolExecutor for RetryableExecutor {
492 async fn execute(&self, _: &str) -> Result<Option<ToolOutput>, ToolError> {
493 Ok(None)
494 }
495 async fn execute_tool_call(
496 &self,
497 _: &ToolCall,
498 ) -> Result<Option<ToolOutput>, ToolError> {
499 Ok(None)
500 }
501 fn is_tool_retryable(&self, tool_id: &str) -> bool {
502 tool_id == "fetch"
503 }
504 }
505 let gate = TrustGateExecutor::new(RetryableExecutor, PermissionPolicy::default());
506 assert!(gate.is_tool_retryable("fetch"));
507 assert!(!gate.is_tool_retryable("bash"));
508 }
509
510 #[test]
511 fn set_skill_env_forwarded_to_inner() {
512 let inner = EnvCapture::new();
513 let gate = TrustGateExecutor::new(inner, PermissionPolicy::default());
514
515 let mut env = std::collections::HashMap::new();
516 env.insert("MY_VAR".to_owned(), "42".to_owned());
517 gate.set_skill_env(Some(env.clone()));
518
519 let captured = gate.inner.captured.lock().unwrap();
520 assert_eq!(*captured, Some(env));
521 }
522
523 #[tokio::test]
524 async fn mcp_tool_supervised_no_rules_allows() {
525 let policy = crate::permissions::PermissionPolicy::from_legacy(&[], &[]);
527 let gate = TrustGateExecutor::new(MockExecutor, policy);
528 gate.set_effective_trust(SkillTrustLevel::Trusted);
529
530 let mut params = serde_json::Map::new();
531 params.insert(
532 "file_path".into(),
533 serde_json::Value::String("/tmp/test.txt".into()),
534 );
535 let call = ToolCall {
536 tool_id: "mcp_filesystem__read_file".into(),
537 params,
538 caller_id: None,
539 };
540 let result = gate.execute_tool_call(&call).await;
541 assert!(
542 result.is_ok(),
543 "MCP tool should be allowed when no rules exist"
544 );
545 }
546
547 #[tokio::test]
548 async fn bash_with_explicit_deny_rule_blocked() {
549 let policy = crate::permissions::PermissionPolicy::from_legacy(&["sudo".into()], &[]);
551 let gate = TrustGateExecutor::new(MockExecutor, policy);
552 gate.set_effective_trust(SkillTrustLevel::Trusted);
553
554 let result = gate
555 .execute_tool_call(&make_call_with_cmd("bash", "sudo apt install vim"))
556 .await;
557 assert!(
558 matches!(result, Err(ToolError::Blocked { .. })),
559 "bash with explicit deny rule should be blocked"
560 );
561 }
562
563 #[tokio::test]
564 async fn bash_with_explicit_allow_rule_succeeds() {
565 let policy = crate::permissions::PermissionPolicy::from_legacy(&[], &[]);
567 let gate = TrustGateExecutor::new(MockExecutor, policy);
568 gate.set_effective_trust(SkillTrustLevel::Trusted);
569
570 let result = gate
571 .execute_tool_call(&make_call_with_cmd("bash", "echo hello"))
572 .await;
573 assert!(
574 result.is_ok(),
575 "bash with explicit allow rule should succeed"
576 );
577 }
578
579 #[tokio::test]
580 async fn readonly_denies_mcp_tool_not_in_allowlist() {
581 let policy =
583 crate::permissions::PermissionPolicy::default().with_autonomy(AutonomyLevel::ReadOnly);
584 let gate = TrustGateExecutor::new(MockExecutor, policy);
585 gate.set_effective_trust(SkillTrustLevel::Trusted);
586
587 let result = gate
588 .execute_tool_call(&make_call("mcpls_get_diagnostics"))
589 .await;
590 assert!(
591 matches!(result, Err(ToolError::Blocked { .. })),
592 "ReadOnly mode must deny non-allowlisted tools"
593 );
594 }
595
596 #[test]
597 fn set_effective_trust_interior_mutability() {
598 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
599 assert_eq!(gate.effective_trust(), SkillTrustLevel::Trusted);
600
601 gate.set_effective_trust(SkillTrustLevel::Quarantined);
602 assert_eq!(gate.effective_trust(), SkillTrustLevel::Quarantined);
603
604 gate.set_effective_trust(SkillTrustLevel::Blocked);
605 assert_eq!(gate.effective_trust(), SkillTrustLevel::Blocked);
606
607 gate.set_effective_trust(SkillTrustLevel::Trusted);
608 assert_eq!(gate.effective_trust(), SkillTrustLevel::Trusted);
609 }
610
611 #[test]
614 fn is_quarantine_denied_exact_match() {
615 assert!(is_quarantine_denied("bash"));
616 assert!(is_quarantine_denied("write"));
617 assert!(is_quarantine_denied("fetch"));
618 assert!(is_quarantine_denied("memory_save"));
619 assert!(is_quarantine_denied("delete_path"));
620 assert!(is_quarantine_denied("create_directory"));
621 }
622
623 #[test]
624 fn is_quarantine_denied_suffix_match_mcp_write() {
625 assert!(is_quarantine_denied("filesystem_write"));
627 assert!(!is_quarantine_denied("filesystem_write_file"));
629 }
630
631 #[test]
632 fn is_quarantine_denied_suffix_mcp_bash() {
633 assert!(is_quarantine_denied("shell_bash"));
634 assert!(is_quarantine_denied("mcp_shell_bash"));
635 }
636
637 #[test]
638 fn is_quarantine_denied_suffix_mcp_fetch() {
639 assert!(is_quarantine_denied("http_fetch"));
640 assert!(!is_quarantine_denied("server_prefetch"));
642 }
643
644 #[test]
645 fn is_quarantine_denied_suffix_mcp_memory_save() {
646 assert!(is_quarantine_denied("server_memory_save"));
647 assert!(!is_quarantine_denied("server_save"));
649 }
650
651 #[test]
652 fn is_quarantine_denied_suffix_mcp_delete_path() {
653 assert!(is_quarantine_denied("fs_delete_path"));
654 assert!(is_quarantine_denied("fs_not_delete_path"));
656 }
657
658 #[test]
659 fn is_quarantine_denied_substring_not_suffix() {
660 assert!(!is_quarantine_denied("write_log"));
662 }
663
664 #[test]
665 fn is_quarantine_denied_read_only_tools_allowed() {
666 assert!(!is_quarantine_denied("filesystem_read_file"));
667 assert!(!is_quarantine_denied("filesystem_list_dir"));
668 assert!(!is_quarantine_denied("read"));
669 assert!(!is_quarantine_denied("file_read"));
670 }
671
672 #[tokio::test]
673 async fn quarantined_denies_mcp_write_tool() {
674 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
675 gate.set_effective_trust(SkillTrustLevel::Quarantined);
676
677 let result = gate.execute_tool_call(&make_call("filesystem_write")).await;
678 assert!(matches!(result, Err(ToolError::Blocked { .. })));
679 }
680
681 #[tokio::test]
682 async fn quarantined_allows_mcp_read_file() {
683 let policy = crate::permissions::PermissionPolicy::from_legacy(&[], &[]);
684 let gate = TrustGateExecutor::new(MockExecutor, policy);
685 gate.set_effective_trust(SkillTrustLevel::Quarantined);
686
687 let result = gate
688 .execute_tool_call(&make_call("filesystem_read_file"))
689 .await;
690 assert!(result.is_ok());
691 }
692
693 #[tokio::test]
694 async fn quarantined_denies_mcp_bash_tool() {
695 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
696 gate.set_effective_trust(SkillTrustLevel::Quarantined);
697
698 let result = gate.execute_tool_call(&make_call("shell_bash")).await;
699 assert!(matches!(result, Err(ToolError::Blocked { .. })));
700 }
701
702 #[tokio::test]
703 async fn quarantined_denies_mcp_memory_save() {
704 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
705 gate.set_effective_trust(SkillTrustLevel::Quarantined);
706
707 let result = gate
708 .execute_tool_call(&make_call("server_memory_save"))
709 .await;
710 assert!(matches!(result, Err(ToolError::Blocked { .. })));
711 }
712
713 #[tokio::test]
714 async fn quarantined_denies_mcp_confirmed_path() {
715 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
717 gate.set_effective_trust(SkillTrustLevel::Quarantined);
718
719 let result = gate
720 .execute_tool_call_confirmed(&make_call("filesystem_write"))
721 .await;
722 assert!(matches!(result, Err(ToolError::Blocked { .. })));
723 }
724
725 fn gate_with_mcp_ids(ids: &[&str]) -> TrustGateExecutor<MockExecutor> {
728 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
729 let handle = gate.mcp_tool_ids_handle();
730 let set: std::collections::HashSet<String> = ids.iter().map(ToString::to_string).collect();
731 *handle.write().unwrap() = set;
732 gate
733 }
734
735 #[tokio::test]
736 async fn quarantined_denies_registered_mcp_tool_novel_name() {
737 let gate = gate_with_mcp_ids(&["github_run_command"]);
739 gate.set_effective_trust(SkillTrustLevel::Quarantined);
740
741 let result = gate
742 .execute_tool_call(&make_call("github_run_command"))
743 .await;
744 assert!(matches!(result, Err(ToolError::Blocked { .. })));
745 }
746
747 #[tokio::test]
748 async fn quarantined_denies_registered_mcp_tool_execute() {
749 let gate = gate_with_mcp_ids(&["shell_execute"]);
751 gate.set_effective_trust(SkillTrustLevel::Quarantined);
752
753 let result = gate.execute_tool_call(&make_call("shell_execute")).await;
754 assert!(matches!(result, Err(ToolError::Blocked { .. })));
755 }
756
757 #[tokio::test]
758 async fn quarantined_allows_unregistered_tool_not_in_denied_list() {
759 let gate = gate_with_mcp_ids(&["other_tool"]);
761 gate.set_effective_trust(SkillTrustLevel::Quarantined);
762
763 let result = gate.execute_tool_call(&make_call("read")).await;
764 assert!(result.is_ok());
765 }
766
767 #[tokio::test]
768 async fn trusted_allows_registered_mcp_tool() {
769 let gate = gate_with_mcp_ids(&["github_run_command"]);
771 gate.set_effective_trust(SkillTrustLevel::Trusted);
772
773 let result = gate
774 .execute_tool_call(&make_call("github_run_command"))
775 .await;
776 assert!(result.is_ok());
777 }
778
779 #[tokio::test]
780 async fn quarantined_denies_mcp_tool_via_confirmed_path() {
781 let gate = gate_with_mcp_ids(&["docker_container_exec"]);
783 gate.set_effective_trust(SkillTrustLevel::Quarantined);
784
785 let result = gate
786 .execute_tool_call_confirmed(&make_call("docker_container_exec"))
787 .await;
788 assert!(matches!(result, Err(ToolError::Blocked { .. })));
789 }
790
791 #[test]
792 fn mcp_tool_ids_handle_shared_arc() {
793 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
794 let handle = gate.mcp_tool_ids_handle();
795 handle.write().unwrap().insert("test_tool".to_owned());
796 assert!(gate.is_mcp_tool("test_tool"));
797 assert!(!gate.is_mcp_tool("other_tool"));
798 }
799}