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