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