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 _ => 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 {
117 return Err(ToolError::Blocked {
118 command: format!("{tool_id} denied (trust=quarantined)"),
119 });
120 }
121 _ => {}
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 _ => 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 _ => {}
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 _ => {}
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 }
221 self.inner.execute_tool_call_confirmed(call).await
222 }
223
224 fn set_skill_env(&self, env: Option<std::collections::HashMap<String, String>>) {
225 self.inner.set_skill_env(env);
226 }
227
228 fn is_tool_retryable(&self, tool_id: &str) -> bool {
229 self.inner.is_tool_retryable(tool_id)
230 }
231
232 fn is_tool_speculatable(&self, tool_id: &str) -> bool {
233 self.inner.is_tool_speculatable(tool_id)
234 }
235
236 fn set_effective_trust(&self, level: crate::SkillTrustLevel) {
237 self.effective_trust
238 .store(trust_to_u8(level), Ordering::Relaxed);
239 }
240
241 fn requires_confirmation(&self, call: &crate::executor::ToolCall) -> bool {
247 let input = call
248 .params
249 .get("command")
250 .or_else(|| call.params.get("file_path"))
251 .or_else(|| call.params.get("query"))
252 .or_else(|| call.params.get("url"))
253 .or_else(|| call.params.get("uri"))
254 .and_then(|v| v.as_str())
255 .unwrap_or("");
256 matches!(
257 self.check_trust(call.tool_id.as_str(), input),
258 Err(ToolError::ConfirmationRequired { .. })
259 )
260 }
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266
267 #[derive(Debug)]
268 struct MockExecutor;
269 impl ToolExecutor for MockExecutor {
270 async fn execute(&self, _: &str) -> Result<Option<ToolOutput>, ToolError> {
271 Ok(None)
272 }
273 async fn execute_tool_call(
274 &self,
275 call: &ToolCall,
276 ) -> Result<Option<ToolOutput>, ToolError> {
277 Ok(Some(ToolOutput {
278 tool_name: call.tool_id.clone(),
279 summary: "ok".into(),
280 blocks_executed: 1,
281 filter_stats: None,
282 diff: None,
283 streamed: false,
284 terminal_id: None,
285 locations: None,
286 raw_response: None,
287 claim_source: None,
288 }))
289 }
290 }
291
292 fn make_call(tool_id: &str) -> ToolCall {
293 ToolCall {
294 tool_id: tool_id.into(),
295 params: serde_json::Map::new(),
296 caller_id: None,
297 context: None,
298
299 tool_call_id: String::new(),
300 skill_name: None,
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 skill_name: None,
315 }
316 }
317
318 #[tokio::test]
319 async fn trusted_allows_all() {
320 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
321 gate.set_effective_trust(SkillTrustLevel::Trusted);
322
323 let result = gate.execute_tool_call(&make_call("bash")).await;
324 assert!(result.is_ok());
326 }
327
328 #[tokio::test]
329 async fn quarantined_denies_bash() {
330 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
331 gate.set_effective_trust(SkillTrustLevel::Quarantined);
332
333 let result = gate.execute_tool_call(&make_call("bash")).await;
334 assert!(matches!(result, Err(ToolError::Blocked { .. })));
335 }
336
337 #[tokio::test]
338 async fn quarantined_denies_write() {
339 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
340 gate.set_effective_trust(SkillTrustLevel::Quarantined);
341
342 let result = gate.execute_tool_call(&make_call("write")).await;
343 assert!(matches!(result, Err(ToolError::Blocked { .. })));
344 }
345
346 #[tokio::test]
347 async fn quarantined_denies_edit() {
348 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
349 gate.set_effective_trust(SkillTrustLevel::Quarantined);
350
351 let result = gate.execute_tool_call(&make_call("edit")).await;
352 assert!(matches!(result, Err(ToolError::Blocked { .. })));
353 }
354
355 #[tokio::test]
356 async fn quarantined_denies_delete_path() {
357 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
358 gate.set_effective_trust(SkillTrustLevel::Quarantined);
359
360 let result = gate.execute_tool_call(&make_call("delete_path")).await;
361 assert!(matches!(result, Err(ToolError::Blocked { .. })));
362 }
363
364 #[tokio::test]
365 async fn quarantined_denies_fetch() {
366 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
367 gate.set_effective_trust(SkillTrustLevel::Quarantined);
368
369 let result = gate.execute_tool_call(&make_call("fetch")).await;
370 assert!(matches!(result, Err(ToolError::Blocked { .. })));
371 }
372
373 #[tokio::test]
374 async fn quarantined_denies_memory_save() {
375 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
376 gate.set_effective_trust(SkillTrustLevel::Quarantined);
377
378 let result = gate.execute_tool_call(&make_call("memory_save")).await;
379 assert!(matches!(result, Err(ToolError::Blocked { .. })));
380 }
381
382 #[tokio::test]
383 async fn quarantined_allows_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("read")).await;
390 assert!(result.is_ok());
391 }
392
393 #[tokio::test]
394 async fn quarantined_allows_file_read() {
395 let policy = crate::permissions::PermissionPolicy::from_legacy(&[], &[]);
396 let gate = TrustGateExecutor::new(MockExecutor, policy);
397 gate.set_effective_trust(SkillTrustLevel::Quarantined);
398
399 let result = gate.execute_tool_call(&make_call("file_read")).await;
400 assert!(result.is_ok());
402 }
403
404 #[tokio::test]
405 async fn blocked_denies_everything() {
406 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
407 gate.set_effective_trust(SkillTrustLevel::Blocked);
408
409 let result = gate.execute_tool_call(&make_call("file_read")).await;
410 assert!(matches!(result, Err(ToolError::Blocked { .. })));
411 }
412
413 #[tokio::test]
414 async fn policy_deny_overrides_trust() {
415 let policy = crate::permissions::PermissionPolicy::from_legacy(&["sudo".into()], &[]);
416 let gate = TrustGateExecutor::new(MockExecutor, policy);
417 gate.set_effective_trust(SkillTrustLevel::Trusted);
418
419 let result = gate
420 .execute_tool_call(&make_call_with_cmd("bash", "sudo rm"))
421 .await;
422 assert!(matches!(result, Err(ToolError::Blocked { .. })));
423 }
424
425 #[tokio::test]
426 async fn blocked_denies_execute() {
427 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
428 gate.set_effective_trust(SkillTrustLevel::Blocked);
429
430 let result = gate.execute("some response").await;
431 assert!(matches!(result, Err(ToolError::Blocked { .. })));
432 }
433
434 #[tokio::test]
435 async fn blocked_denies_execute_confirmed() {
436 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
437 gate.set_effective_trust(SkillTrustLevel::Blocked);
438
439 let result = gate.execute_confirmed("some response").await;
440 assert!(matches!(result, Err(ToolError::Blocked { .. })));
441 }
442
443 #[tokio::test]
444 async fn trusted_allows_execute() {
445 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
446 gate.set_effective_trust(SkillTrustLevel::Trusted);
447
448 let result = gate.execute("some response").await;
449 assert!(result.is_ok());
450 }
451
452 #[tokio::test]
453 async fn verified_with_allow_policy_succeeds() {
454 let policy = crate::permissions::PermissionPolicy::from_legacy(&[], &[]);
455 let gate = TrustGateExecutor::new(MockExecutor, policy);
456 gate.set_effective_trust(SkillTrustLevel::Verified);
457
458 let result = gate
459 .execute_tool_call(&make_call_with_cmd("bash", "echo hi"))
460 .await
461 .unwrap();
462 assert!(result.is_some());
463 }
464
465 #[tokio::test]
466 async fn quarantined_denies_web_scrape() {
467 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
468 gate.set_effective_trust(SkillTrustLevel::Quarantined);
469
470 let result = gate.execute_tool_call(&make_call("web_scrape")).await;
471 assert!(matches!(result, Err(ToolError::Blocked { .. })));
472 }
473
474 #[derive(Debug)]
475 struct EnvCapture {
476 captured: std::sync::Mutex<Option<std::collections::HashMap<String, String>>>,
477 }
478 impl EnvCapture {
479 fn new() -> Self {
480 Self {
481 captured: std::sync::Mutex::new(None),
482 }
483 }
484 }
485 impl ToolExecutor for EnvCapture {
486 async fn execute(&self, _: &str) -> Result<Option<ToolOutput>, ToolError> {
487 Ok(None)
488 }
489 async fn execute_tool_call(&self, _: &ToolCall) -> Result<Option<ToolOutput>, ToolError> {
490 Ok(None)
491 }
492 fn set_skill_env(&self, env: Option<std::collections::HashMap<String, String>>) {
493 *self.captured.lock().unwrap() = env;
494 }
495 }
496
497 #[test]
498 fn is_tool_retryable_delegated_to_inner() {
499 #[derive(Debug)]
500 struct RetryableExecutor;
501 impl ToolExecutor for RetryableExecutor {
502 async fn execute(&self, _: &str) -> Result<Option<ToolOutput>, ToolError> {
503 Ok(None)
504 }
505 async fn execute_tool_call(
506 &self,
507 _: &ToolCall,
508 ) -> Result<Option<ToolOutput>, ToolError> {
509 Ok(None)
510 }
511 fn is_tool_retryable(&self, tool_id: &str) -> bool {
512 tool_id == "fetch"
513 }
514 }
515 let gate = TrustGateExecutor::new(RetryableExecutor, PermissionPolicy::default());
516 assert!(gate.is_tool_retryable("fetch"));
517 assert!(!gate.is_tool_retryable("bash"));
518 }
519
520 #[test]
521 fn set_skill_env_forwarded_to_inner() {
522 let inner = EnvCapture::new();
523 let gate = TrustGateExecutor::new(inner, PermissionPolicy::default());
524
525 let mut env = std::collections::HashMap::new();
526 env.insert("MY_VAR".to_owned(), "42".to_owned());
527 gate.set_skill_env(Some(env.clone()));
528
529 let captured = gate.inner.captured.lock().unwrap();
530 assert_eq!(*captured, Some(env));
531 }
532
533 #[tokio::test]
534 async fn mcp_tool_supervised_no_rules_allows() {
535 let policy = crate::permissions::PermissionPolicy::from_legacy(&[], &[]);
537 let gate = TrustGateExecutor::new(MockExecutor, policy);
538 gate.set_effective_trust(SkillTrustLevel::Trusted);
539
540 let mut params = serde_json::Map::new();
541 params.insert(
542 "file_path".into(),
543 serde_json::Value::String("/tmp/test.txt".into()),
544 );
545 let call = ToolCall {
546 tool_id: "mcp_filesystem__read_file".into(),
547 params,
548 caller_id: None,
549 context: None,
550
551 tool_call_id: String::new(),
552 skill_name: None,
553 };
554 let result = gate.execute_tool_call(&call).await;
555 assert!(
556 result.is_ok(),
557 "MCP tool should be allowed when no rules exist"
558 );
559 }
560
561 #[tokio::test]
562 async fn bash_with_explicit_deny_rule_blocked() {
563 let policy = crate::permissions::PermissionPolicy::from_legacy(&["sudo".into()], &[]);
565 let gate = TrustGateExecutor::new(MockExecutor, policy);
566 gate.set_effective_trust(SkillTrustLevel::Trusted);
567
568 let result = gate
569 .execute_tool_call(&make_call_with_cmd("bash", "sudo apt install vim"))
570 .await;
571 assert!(
572 matches!(result, Err(ToolError::Blocked { .. })),
573 "bash with explicit deny rule should be blocked"
574 );
575 }
576
577 #[tokio::test]
578 async fn bash_with_explicit_allow_rule_succeeds() {
579 let policy = crate::permissions::PermissionPolicy::from_legacy(&[], &[]);
581 let gate = TrustGateExecutor::new(MockExecutor, policy);
582 gate.set_effective_trust(SkillTrustLevel::Trusted);
583
584 let result = gate
585 .execute_tool_call(&make_call_with_cmd("bash", "echo hello"))
586 .await;
587 assert!(
588 result.is_ok(),
589 "bash with explicit allow rule should succeed"
590 );
591 }
592
593 #[tokio::test]
594 async fn readonly_denies_mcp_tool_not_in_allowlist() {
595 let policy =
597 crate::permissions::PermissionPolicy::default().with_autonomy(AutonomyLevel::ReadOnly);
598 let gate = TrustGateExecutor::new(MockExecutor, policy);
599 gate.set_effective_trust(SkillTrustLevel::Trusted);
600
601 let result = gate
602 .execute_tool_call(&make_call("mcpls_get_diagnostics"))
603 .await;
604 assert!(
605 matches!(result, Err(ToolError::Blocked { .. })),
606 "ReadOnly mode must deny non-allowlisted tools"
607 );
608 }
609
610 #[test]
611 fn set_effective_trust_interior_mutability() {
612 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
613 assert_eq!(gate.effective_trust(), SkillTrustLevel::Trusted);
614
615 gate.set_effective_trust(SkillTrustLevel::Quarantined);
616 assert_eq!(gate.effective_trust(), SkillTrustLevel::Quarantined);
617
618 gate.set_effective_trust(SkillTrustLevel::Blocked);
619 assert_eq!(gate.effective_trust(), SkillTrustLevel::Blocked);
620
621 gate.set_effective_trust(SkillTrustLevel::Trusted);
622 assert_eq!(gate.effective_trust(), SkillTrustLevel::Trusted);
623 }
624
625 #[test]
628 fn is_quarantine_denied_exact_match() {
629 assert!(is_quarantine_denied("bash"));
630 assert!(is_quarantine_denied("write"));
631 assert!(is_quarantine_denied("fetch"));
632 assert!(is_quarantine_denied("memory_save"));
633 assert!(is_quarantine_denied("delete_path"));
634 assert!(is_quarantine_denied("create_directory"));
635 }
636
637 #[test]
638 fn is_quarantine_denied_suffix_match_mcp_write() {
639 assert!(is_quarantine_denied("filesystem_write"));
641 assert!(!is_quarantine_denied("filesystem_write_file"));
643 }
644
645 #[test]
646 fn is_quarantine_denied_suffix_mcp_bash() {
647 assert!(is_quarantine_denied("shell_bash"));
648 assert!(is_quarantine_denied("mcp_shell_bash"));
649 }
650
651 #[test]
652 fn is_quarantine_denied_suffix_mcp_fetch() {
653 assert!(is_quarantine_denied("http_fetch"));
654 assert!(!is_quarantine_denied("server_prefetch"));
656 }
657
658 #[test]
659 fn is_quarantine_denied_suffix_mcp_memory_save() {
660 assert!(is_quarantine_denied("server_memory_save"));
661 assert!(!is_quarantine_denied("server_save"));
663 }
664
665 #[test]
666 fn is_quarantine_denied_suffix_mcp_delete_path() {
667 assert!(is_quarantine_denied("fs_delete_path"));
668 assert!(is_quarantine_denied("fs_not_delete_path"));
670 }
671
672 #[test]
673 fn is_quarantine_denied_substring_not_suffix() {
674 assert!(!is_quarantine_denied("write_log"));
676 }
677
678 #[test]
679 fn is_quarantine_denied_read_only_tools_allowed() {
680 assert!(!is_quarantine_denied("filesystem_read_file"));
681 assert!(!is_quarantine_denied("filesystem_list_dir"));
682 assert!(!is_quarantine_denied("read"));
683 assert!(!is_quarantine_denied("file_read"));
684 }
685
686 #[tokio::test]
687 async fn quarantined_denies_mcp_write_tool() {
688 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
689 gate.set_effective_trust(SkillTrustLevel::Quarantined);
690
691 let result = gate.execute_tool_call(&make_call("filesystem_write")).await;
692 assert!(matches!(result, Err(ToolError::Blocked { .. })));
693 }
694
695 #[tokio::test]
696 async fn quarantined_allows_mcp_read_file() {
697 let policy = crate::permissions::PermissionPolicy::from_legacy(&[], &[]);
698 let gate = TrustGateExecutor::new(MockExecutor, policy);
699 gate.set_effective_trust(SkillTrustLevel::Quarantined);
700
701 let result = gate
702 .execute_tool_call(&make_call("filesystem_read_file"))
703 .await;
704 assert!(result.is_ok());
705 }
706
707 #[tokio::test]
708 async fn quarantined_denies_mcp_bash_tool() {
709 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
710 gate.set_effective_trust(SkillTrustLevel::Quarantined);
711
712 let result = gate.execute_tool_call(&make_call("shell_bash")).await;
713 assert!(matches!(result, Err(ToolError::Blocked { .. })));
714 }
715
716 #[tokio::test]
717 async fn quarantined_denies_mcp_memory_save() {
718 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
719 gate.set_effective_trust(SkillTrustLevel::Quarantined);
720
721 let result = gate
722 .execute_tool_call(&make_call("server_memory_save"))
723 .await;
724 assert!(matches!(result, Err(ToolError::Blocked { .. })));
725 }
726
727 #[tokio::test]
728 async fn quarantined_denies_mcp_confirmed_path() {
729 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
731 gate.set_effective_trust(SkillTrustLevel::Quarantined);
732
733 let result = gate
734 .execute_tool_call_confirmed(&make_call("filesystem_write"))
735 .await;
736 assert!(matches!(result, Err(ToolError::Blocked { .. })));
737 }
738
739 fn gate_with_mcp_ids(ids: &[&str]) -> TrustGateExecutor<MockExecutor> {
742 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
743 let handle = gate.mcp_tool_ids_handle();
744 let set: std::collections::HashSet<String> = ids.iter().map(ToString::to_string).collect();
745 *handle.write() = set;
746 gate
747 }
748
749 #[tokio::test]
750 async fn quarantined_denies_registered_mcp_tool_novel_name() {
751 let gate = gate_with_mcp_ids(&["github_run_command"]);
753 gate.set_effective_trust(SkillTrustLevel::Quarantined);
754
755 let result = gate
756 .execute_tool_call(&make_call("github_run_command"))
757 .await;
758 assert!(matches!(result, Err(ToolError::Blocked { .. })));
759 }
760
761 #[tokio::test]
762 async fn quarantined_denies_registered_mcp_tool_execute() {
763 let gate = gate_with_mcp_ids(&["shell_execute"]);
765 gate.set_effective_trust(SkillTrustLevel::Quarantined);
766
767 let result = gate.execute_tool_call(&make_call("shell_execute")).await;
768 assert!(matches!(result, Err(ToolError::Blocked { .. })));
769 }
770
771 #[tokio::test]
772 async fn quarantined_allows_unregistered_tool_not_in_denied_list() {
773 let gate = gate_with_mcp_ids(&["other_tool"]);
775 gate.set_effective_trust(SkillTrustLevel::Quarantined);
776
777 let result = gate.execute_tool_call(&make_call("read")).await;
778 assert!(result.is_ok());
779 }
780
781 #[tokio::test]
782 async fn trusted_allows_registered_mcp_tool() {
783 let gate = gate_with_mcp_ids(&["github_run_command"]);
785 gate.set_effective_trust(SkillTrustLevel::Trusted);
786
787 let result = gate
788 .execute_tool_call(&make_call("github_run_command"))
789 .await;
790 assert!(result.is_ok());
791 }
792
793 #[tokio::test]
794 async fn quarantined_denies_mcp_tool_via_confirmed_path() {
795 let gate = gate_with_mcp_ids(&["docker_container_exec"]);
797 gate.set_effective_trust(SkillTrustLevel::Quarantined);
798
799 let result = gate
800 .execute_tool_call_confirmed(&make_call("docker_container_exec"))
801 .await;
802 assert!(matches!(result, Err(ToolError::Blocked { .. })));
803 }
804
805 #[test]
806 fn mcp_tool_ids_handle_shared_arc() {
807 let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
808 let handle = gate.mcp_tool_ids_handle();
809 handle.write().insert("test_tool".to_owned());
810 assert!(gate.is_mcp_tool("test_tool"));
811 assert!(!gate.is_mcp_tool("other_tool"));
812 }
813
814 #[test]
817 fn invoke_skill_and_load_skill_suffix_match_is_intentional() {
818 assert!(is_quarantine_denied("invoke_skill"));
820 assert!(is_quarantine_denied("load_skill"));
821 assert!(is_quarantine_denied("foo_invoke_skill"));
824 assert!(is_quarantine_denied("foo_load_skill"));
825 }
826}