1use leviath_core::taint::{
8 GateDecision, GateDecisionSource, GateEvent, SecurityConfig, TaintLevel, ToolClassification,
9 builtin_tool_classification,
10};
11use std::collections::HashMap;
12
13use crate::components::ContextWindow;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum GateResolution {
18 AllowOnce,
20 AlwaysAllow,
22 Deny,
24}
25
26#[async_trait::async_trait]
32pub trait GatePrompt: Send + Sync {
33 async fn resolve(&self, decision: &GateDecision) -> GateResolution;
36}
37
38pub type ScriptRuleChecker = dyn Fn(&str, Option<&str>, TaintLevel) -> Option<String> + Send + Sync;
42
43#[derive(Debug, Clone, bevy_ecs::component::Component)]
47pub struct TaintGate {
48 config: SecurityConfig,
50 tool_overrides: HashMap<String, ToolClassification>,
52 audit_log: Vec<GateEvent>,
54}
55
56impl TaintGate {
57 pub fn new(config: SecurityConfig) -> Self {
59 Self {
60 config,
61 tool_overrides: HashMap::new(),
62 audit_log: Vec::new(),
63 }
64 }
65
66 pub fn disabled() -> Self {
68 Self {
69 config: SecurityConfig {
70 taint_tracking: false,
71 },
72 tool_overrides: HashMap::new(),
73 audit_log: Vec::new(),
74 }
75 }
76
77 pub fn config(&self) -> &SecurityConfig {
79 &self.config
80 }
81
82 pub fn set_tool_classification(
84 &mut self,
85 tool_name: String,
86 classification: ToolClassification,
87 ) {
88 self.tool_overrides.insert(tool_name, classification);
89 }
90
91 pub fn tool_classification(&self, tool_name: &str) -> ToolClassification {
93 self.tool_overrides
94 .get(tool_name)
95 .cloned()
96 .unwrap_or_else(|| builtin_tool_classification(tool_name))
97 }
98
99 pub fn apply_mcp_overrides(
111 &mut self,
112 overrides: &HashMap<String, leviath_core::policy::McpToolOverride>,
113 ) {
114 for (tool_name, over) in overrides {
115 let mut classification = self.tool_classification(tool_name);
116 if let Some(sensitivity) = over.sensitivity {
117 classification.sensitivity = sensitivity;
118 }
119 if let Some(clearance) = over.clearance {
120 classification.clearance = clearance;
121 }
122 if let Some(direction) = over.direction.as_deref() {
123 match leviath_core::taint::ToolDirection::from_str_loose(direction) {
124 Some(parsed) => classification.direction = parsed,
125 None => tracing::warn!(
126 tool = %tool_name,
127 direction = %direction,
128 "ignoring unrecognized direction in [mcp_overrides]"
129 ),
130 }
131 }
132 self.set_tool_classification(tool_name.clone(), classification);
133 }
134 }
135
136 pub fn check_traditional(
141 &mut self,
142 agent_id: &str,
143 tool_name: &str,
144 window: &ContextWindow,
145 ) -> GateDecision {
146 if !self.config.taint_tracking {
147 self.log_event(
148 agent_id,
149 tool_name,
150 TaintLevel::Public,
151 TaintLevel::Public,
152 true,
153 GateDecisionSource::TaintDisabled,
154 );
155 return GateDecision::Allowed;
156 }
157
158 let classification = self.tool_classification(tool_name);
159
160 if !classification.is_outbound() {
162 self.log_event(
163 agent_id,
164 tool_name,
165 TaintLevel::Public,
166 classification.clearance,
167 true,
168 GateDecisionSource::AutoAllow,
169 );
170 return GateDecision::Allowed;
171 }
172
173 let taint = window.overall_taint().unwrap_or(TaintLevel::Public);
175
176 if classification.check_clearance(taint) {
177 self.log_event(
178 agent_id,
179 tool_name,
180 taint,
181 classification.clearance,
182 true,
183 GateDecisionSource::AutoAllow,
184 );
185 GateDecision::Allowed
186 } else {
187 let source_regions: Vec<String> = window
189 .taint_summary()
190 .into_iter()
191 .filter(|(_, level)| *level > classification.clearance)
192 .map(|(name, _)| name)
193 .collect();
194
195 self.log_event(
196 agent_id,
197 tool_name,
198 taint,
199 classification.clearance,
200 false,
201 GateDecisionSource::AutoBlock,
202 );
203
204 GateDecision::Blocked {
205 taint_level: taint,
206 clearance: classification.clearance,
207 source_regions,
208 tool_name: tool_name.to_string(),
209 }
210 }
211 }
212
213 pub fn check_with_policy(
221 &mut self,
222 agent_id: &str,
223 tool_name: &str,
224 window: &ContextWindow,
225 target: Option<&str>,
226 policy: &leviath_core::PolicyConfig,
227 script_checker: Option<&ScriptRuleChecker>,
228 ) -> GateDecision {
229 let decision = self.check_traditional(agent_id, tool_name, window);
230
231 if decision.is_allowed() {
232 return decision;
233 }
234
235 let (taint, clearance) = decision
239 .blocked_levels()
240 .expect("infallible: a non-Allowed GateDecision is always Blocked");
241
242 if let Some(rule_idx) = policy.check_allowlist(tool_name, target, taint) {
244 self.log_event(
245 agent_id,
246 tool_name,
247 taint,
248 clearance,
249 true,
250 GateDecisionSource::AllowlistRule {
251 rule_index: rule_idx,
252 },
253 );
254 return GateDecision::Allowed;
255 }
256
257 if let Some(checker) = script_checker
259 && let Some(script_name) = checker(tool_name, target, taint)
260 {
261 self.log_event(
262 agent_id,
263 tool_name,
264 taint,
265 clearance,
266 true,
267 GateDecisionSource::ScriptedRule { script_name },
268 );
269 return GateDecision::Allowed;
270 }
271
272 decision
273 }
274
275 pub fn record_allow(
277 &mut self,
278 agent_id: &str,
279 tool_name: &str,
280 taint: TaintLevel,
281 clearance: TaintLevel,
282 source: GateDecisionSource,
283 ) {
284 self.log_event(agent_id, tool_name, taint, clearance, true, source);
285 }
286
287 pub fn record_deny(
289 &mut self,
290 agent_id: &str,
291 tool_name: &str,
292 taint: TaintLevel,
293 clearance: TaintLevel,
294 source: GateDecisionSource,
295 ) {
296 self.log_event(agent_id, tool_name, taint, clearance, false, source);
297 }
298
299 pub fn apply_resolution(
307 &mut self,
308 agent_id: &str,
309 tool_name: &str,
310 tool_id: &str,
311 taint: TaintLevel,
312 clearance: TaintLevel,
313 resolution: GateResolution,
314 ) -> Option<(String, String)> {
315 match resolution {
316 GateResolution::AllowOnce => {
317 self.record_allow(
318 agent_id,
319 tool_name,
320 taint,
321 clearance,
322 GateDecisionSource::UserAllowOnce,
323 );
324 None
325 }
326 GateResolution::AlwaysAllow => {
327 self.record_allow(
328 agent_id,
329 tool_name,
330 taint,
331 clearance,
332 GateDecisionSource::UserAlwaysAllow,
333 );
334 let mut cls = self.tool_classification(tool_name);
335 cls.clearance = TaintLevel::Private;
336 self.set_tool_classification(tool_name.to_string(), cls);
337 None
338 }
339 GateResolution::Deny => {
340 self.record_deny(
341 agent_id,
342 tool_name,
343 taint,
344 clearance,
345 GateDecisionSource::UserDenied,
346 );
347 Some((
348 tool_id.to_string(),
349 format!(
350 "[blocked] Tool '{}' would send data at {} sensitivity, above its {} \
351 clearance. Denied by user.",
352 tool_name, taint, clearance
353 ),
354 ))
355 }
356 }
357 }
358
359 pub fn audit_log(&self) -> &[GateEvent] {
361 &self.audit_log
362 }
363
364 fn log_event(
365 &mut self,
366 agent_id: &str,
367 tool_name: &str,
368 taint_level: TaintLevel,
369 clearance: TaintLevel,
370 allowed: bool,
371 decision_source: GateDecisionSource,
372 ) {
373 self.audit_log.push(GateEvent {
374 timestamp: chrono::Utc::now().timestamp(),
375 agent_id: agent_id.to_string(),
376 tool_name: tool_name.to_string(),
377 taint_level,
378 clearance,
379 allowed,
380 decision_source,
381 });
382 }
383}
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388 use leviath_core::taint::ToolDirection;
389 use leviath_core::{Region, RegionKind};
390
391 fn make_window_with_taint(taint: TaintLevel) -> ContextWindow {
392 let mut window = ContextWindow::new(10000);
393 let region =
394 Region::new("conv".to_string(), RegionKind::Temporary, 5000).with_taint_tracking();
395 window.add_region(region);
396 if taint != TaintLevel::Public {
397 window
398 .add_tainted_to_region("conv", "data".to_string(), 10, taint)
399 .unwrap();
400 }
401 window
402 }
403
404 #[test]
405 fn gate_disabled_always_allows() {
406 let mut gate = TaintGate::disabled();
407 assert!(!gate.config().taint_tracking);
408
409 let window = make_window_with_taint(TaintLevel::Private);
410 let decision = gate.check_traditional("agent-1", "shell", &window);
411 assert!(decision.is_allowed());
412 assert_eq!(gate.audit_log().len(), 1);
413 assert_eq!(
414 gate.audit_log()[0].decision_source,
415 GateDecisionSource::TaintDisabled
416 );
417 }
418
419 #[test]
420 fn gate_allows_non_outbound_tool() {
421 let mut gate = TaintGate::new(SecurityConfig::default());
422 let window = make_window_with_taint(TaintLevel::Private);
423 let decision = gate.check_traditional("agent-1", "read_file", &window);
424 assert!(decision.is_allowed());
425 }
426
427 #[test]
428 fn gate_allows_outbound_when_taint_within_clearance() {
429 let mut gate = TaintGate::new(SecurityConfig::default());
430 let window = make_window_with_taint(TaintLevel::Public);
431 let decision = gate.check_traditional("agent-1", "shell", &window);
432 assert!(decision.is_allowed());
433 }
434
435 #[test]
436 fn gate_blocks_outbound_when_taint_exceeds_clearance() {
437 let mut gate = TaintGate::new(SecurityConfig::default());
438 let window = make_window_with_taint(TaintLevel::Private);
439 let decision = gate.check_traditional("agent-1", "shell", &window);
440 assert!(!decision.is_allowed());
441 assert_eq!(
442 decision,
443 GateDecision::Blocked {
444 taint_level: TaintLevel::Private,
445 clearance: TaintLevel::Public,
446 source_regions: vec!["conv".to_string()],
447 tool_name: "shell".to_string(),
448 }
449 );
450 }
451
452 #[test]
453 fn gate_uses_tool_override() {
454 let mut gate = TaintGate::new(SecurityConfig::default());
455 gate.set_tool_classification(
456 "shell".to_string(),
457 ToolClassification::new(
458 TaintLevel::Public,
459 ToolDirection::Outbound,
460 TaintLevel::Private, ),
462 );
463 let window = make_window_with_taint(TaintLevel::Private);
464 let decision = gate.check_traditional("agent-1", "shell", &window);
465 assert!(decision.is_allowed());
466 }
467
468 #[test]
469 fn apply_mcp_overrides_replaces_only_the_set_fields() {
470 let mut gate = TaintGate::new(SecurityConfig::default());
471 let before = gate.tool_classification("srv.notify");
472 let overrides = std::collections::HashMap::from([(
473 "srv.notify".to_string(),
474 leviath_core::policy::McpToolOverride {
475 sensitivity: Some(TaintLevel::Private),
476 direction: None,
477 clearance: None,
478 },
479 )]);
480 gate.apply_mcp_overrides(&overrides);
481 let after = gate.tool_classification("srv.notify");
482 assert_eq!(after.sensitivity, TaintLevel::Private);
483 assert_eq!(after.direction, before.direction);
484 assert_eq!(after.clearance, before.clearance);
485 }
486
487 #[test]
488 fn apply_mcp_overrides_parses_direction_and_clearance() {
489 let mut gate = TaintGate::new(SecurityConfig::default());
490 let overrides = std::collections::HashMap::from([(
491 "srv.post".to_string(),
492 leviath_core::policy::McpToolOverride {
493 sensitivity: None,
494 direction: Some("outbound".to_string()),
495 clearance: Some(TaintLevel::Internal),
496 },
497 )]);
498 gate.apply_mcp_overrides(&overrides);
499 let after = gate.tool_classification("srv.post");
500 assert_eq!(after.direction, ToolDirection::Outbound);
501 assert_eq!(after.clearance, TaintLevel::Internal);
502 }
503
504 #[test]
505 fn apply_mcp_overrides_keeps_direction_on_unrecognized_string() {
506 let mut gate = TaintGate::new(SecurityConfig::default());
507 let before = gate.tool_classification("srv.odd");
508 let overrides = std::collections::HashMap::from([(
509 "srv.odd".to_string(),
510 leviath_core::policy::McpToolOverride {
511 sensitivity: None,
512 direction: Some("sideways".to_string()),
513 clearance: None,
514 },
515 )]);
516 gate.apply_mcp_overrides(&overrides);
517 assert_eq!(
519 gate.tool_classification("srv.odd").direction,
520 before.direction
521 );
522 }
523
524 #[test]
525 fn session_approval_still_wins_over_an_mcp_override() {
526 let mut gate = TaintGate::new(SecurityConfig::default());
527 let overrides = std::collections::HashMap::from([(
528 "srv.send".to_string(),
529 leviath_core::policy::McpToolOverride {
530 sensitivity: None,
531 direction: Some("outbound".to_string()),
532 clearance: Some(TaintLevel::Public),
533 },
534 )]);
535 gate.apply_mcp_overrides(&overrides);
536 gate.set_tool_classification(
539 "srv.send".to_string(),
540 ToolClassification::new(
541 TaintLevel::Public,
542 ToolDirection::Outbound,
543 TaintLevel::Private,
544 ),
545 );
546 assert_eq!(
547 gate.tool_classification("srv.send").clearance,
548 TaintLevel::Private
549 );
550 }
551
552 #[test]
553 fn gate_blocked_identifies_source_regions() {
554 let mut gate = TaintGate::new(SecurityConfig::default());
555 let mut window = ContextWindow::new(10000);
556 let r1 =
557 Region::new("clean".to_string(), RegionKind::Temporary, 5000).with_taint_tracking();
558 let r2 =
559 Region::new("dirty".to_string(), RegionKind::Temporary, 5000).with_taint_tracking();
560 window.add_region(r1);
561 window.add_region(r2);
562
563 window
564 .add_tainted_to_region("clean", "ok".to_string(), 5, TaintLevel::Public)
565 .unwrap();
566 window
567 .add_tainted_to_region("dirty", "secret".to_string(), 5, TaintLevel::Private)
568 .unwrap();
569
570 let decision = gate.check_traditional("agent-1", "shell", &window);
571 assert_eq!(
574 decision,
575 GateDecision::Blocked {
576 taint_level: TaintLevel::Private,
577 clearance: TaintLevel::Public,
578 source_regions: vec!["dirty".to_string()],
579 tool_name: "shell".to_string(),
580 }
581 );
582 }
583
584 #[test]
585 fn gate_audit_log_records_events() {
586 let mut gate = TaintGate::new(SecurityConfig::default());
587 let window = make_window_with_taint(TaintLevel::Public);
588
589 gate.check_traditional("agent-1", "shell", &window);
590 gate.check_traditional("agent-1", "read_file", &window);
591
592 assert_eq!(gate.audit_log().len(), 2);
593 assert!(gate.audit_log()[0].allowed);
594 assert!(gate.audit_log()[1].allowed);
595 }
596
597 #[test]
598 fn gate_record_allow() {
599 let mut gate = TaintGate::new(SecurityConfig::default());
600 gate.record_allow(
601 "agent-1",
602 "shell",
603 TaintLevel::Private,
604 TaintLevel::Public,
605 GateDecisionSource::UserAllowOnce,
606 );
607 assert_eq!(gate.audit_log().len(), 1);
608 assert!(gate.audit_log()[0].allowed);
609 assert_eq!(
610 gate.audit_log()[0].decision_source,
611 GateDecisionSource::UserAllowOnce
612 );
613 }
614
615 #[test]
616 fn gate_tool_classification_returns_override() {
617 let mut gate = TaintGate::new(SecurityConfig::default());
618 let custom = ToolClassification::new(
619 TaintLevel::Private,
620 ToolDirection::Outbound,
621 TaintLevel::Private,
622 );
623 gate.set_tool_classification("my_tool".to_string(), custom.clone());
624 assert_eq!(gate.tool_classification("my_tool"), custom);
625 }
626
627 #[test]
628 fn gate_tool_classification_falls_back_to_builtin() {
629 let gate = TaintGate::new(SecurityConfig::default());
630 let tc = gate.tool_classification("read_file");
631 assert_eq!(tc.direction, ToolDirection::Inbound);
632 }
633
634 #[test]
635 fn gate_new_and_config() {
636 let config = SecurityConfig {
637 taint_tracking: true,
638 };
639 let gate = TaintGate::new(config.clone());
640 assert!(gate.config().taint_tracking);
641 assert!(gate.config().taint_tracking);
642 }
643
644 #[test]
647 fn gate_with_policy_allows_via_allowlist() {
648 let mut gate = TaintGate::new(SecurityConfig::default());
649 let window = make_window_with_taint(TaintLevel::Private);
650 let policy = leviath_core::PolicyConfig {
651 allowlist: vec![leviath_core::AllowlistRule {
652 tool: "shell".into(),
653 to: vec![],
654 channel: vec![],
655 max_sensitivity: TaintLevel::Private,
656 }],
657 mcp_overrides: Default::default(),
658 };
659
660 let decision = gate.check_with_policy("agent-1", "shell", &window, None, &policy, None);
661 assert!(decision.is_allowed());
662
663 let last = gate.audit_log().last().unwrap();
665 assert!(last.allowed);
666 assert_eq!(
668 last.decision_source,
669 GateDecisionSource::AllowlistRule { rule_index: 0 }
670 );
671 }
672
673 #[test]
674 fn gate_with_policy_allows_via_scripted_rule() {
675 let mut gate = TaintGate::new(SecurityConfig::default());
676 let window = make_window_with_taint(TaintLevel::Private);
677 let policy = leviath_core::PolicyConfig::default(); let checker = |tool: &str, _target: Option<&str>, _taint: TaintLevel| -> Option<String> {
680 (tool == "shell").then(|| "company_rule.rhai".to_string())
681 };
682
683 let decision =
684 gate.check_with_policy("agent-1", "shell", &window, None, &policy, Some(&checker));
685 assert!(decision.is_allowed());
686
687 let last = gate.audit_log().last().unwrap();
688 assert_eq!(
689 last.decision_source,
690 GateDecisionSource::ScriptedRule {
691 script_name: "company_rule.rhai".to_string()
692 }
693 );
694 }
695
696 #[test]
697 fn gate_with_policy_blocks_when_no_rule_matches() {
698 let mut gate = TaintGate::new(SecurityConfig::default());
699 let window = make_window_with_taint(TaintLevel::Private);
700 let policy = leviath_core::PolicyConfig::default();
701
702 let decision = gate.check_with_policy("agent-1", "shell", &window, None, &policy, None);
703 assert!(!decision.is_allowed());
704 }
705
706 #[test]
707 fn gate_with_policy_passes_through_when_already_allowed() {
708 let mut gate = TaintGate::new(SecurityConfig::default());
709 let window = make_window_with_taint(TaintLevel::Public);
710 let policy = leviath_core::PolicyConfig::default();
711
712 let decision = gate.check_with_policy("agent-1", "shell", &window, None, &policy, None);
713 assert!(decision.is_allowed());
714 }
715
716 #[test]
717 fn gate_with_policy_target_pattern_matching() {
718 let mut gate = TaintGate::new(SecurityConfig::default());
719 gate.set_tool_classification(
720 "send_email".to_string(),
721 ToolClassification::new(
722 TaintLevel::Public,
723 ToolDirection::Outbound,
724 TaintLevel::Public,
725 ),
726 );
727 let window = make_window_with_taint(TaintLevel::Private);
728 let policy = leviath_core::PolicyConfig {
729 allowlist: vec![leviath_core::AllowlistRule {
730 tool: "send_email".into(),
731 to: vec!["megan@*".into()],
732 channel: vec![],
733 max_sensitivity: TaintLevel::Private,
734 }],
735 mcp_overrides: Default::default(),
736 };
737
738 let decision = gate.check_with_policy(
740 "agent-1",
741 "send_email",
742 &window,
743 Some("megan@work.com"),
744 &policy,
745 None,
746 );
747 assert!(decision.is_allowed());
748
749 let decision2 = gate.check_with_policy(
751 "agent-1",
752 "send_email",
753 &window,
754 Some("bob@work.com"),
755 &policy,
756 None,
757 );
758 assert!(!decision2.is_allowed());
759 }
760
761 #[test]
764 fn gate_check_traditional_with_internal_taint() {
765 let mut gate = TaintGate::new(SecurityConfig::default());
766 let window = make_window_with_taint(TaintLevel::Internal);
767 let decision = gate.check_traditional("agent-1", "shell", &window);
768 assert!(!decision.is_allowed());
770 assert_eq!(
771 decision,
772 GateDecision::Blocked {
773 taint_level: TaintLevel::Internal,
774 clearance: TaintLevel::Public,
775 source_regions: vec!["conv".to_string()],
776 tool_name: "shell".to_string(),
777 }
778 );
779 }
780
781 #[test]
782 fn gate_audit_log_records_blocked_events() {
783 let mut gate = TaintGate::new(SecurityConfig::default());
784 let window = make_window_with_taint(TaintLevel::Private);
785
786 gate.check_traditional("agent-1", "shell", &window);
787 assert_eq!(gate.audit_log().len(), 1);
788 assert!(!gate.audit_log()[0].allowed);
789 assert_eq!(gate.audit_log()[0].tool_name, "shell");
790 assert_eq!(gate.audit_log()[0].taint_level, TaintLevel::Private);
791 assert_eq!(
794 gate.audit_log()[0].decision_source,
795 GateDecisionSource::AutoBlock,
796 );
797 }
798
799 #[test]
800 fn gate_with_policy_scripted_rule_non_matching_tool() {
801 let mut gate = TaintGate::new(SecurityConfig::default());
802 let window = make_window_with_taint(TaintLevel::Private);
803 let policy = leviath_core::PolicyConfig::default();
804
805 let checker = |_tool: &str, _target: Option<&str>, _taint: TaintLevel| -> Option<String> {
806 None };
808
809 let decision =
810 gate.check_with_policy("agent-1", "shell", &window, None, &policy, Some(&checker));
811 assert!(!decision.is_allowed());
812 }
813
814 #[test]
815 fn gate_with_policy_non_outbound_skips_policy_check() {
816 let mut gate = TaintGate::new(SecurityConfig::default());
817 let window = make_window_with_taint(TaintLevel::Private);
818 let policy = leviath_core::PolicyConfig::default();
819
820 let decision = gate.check_with_policy("agent-1", "read_file", &window, None, &policy, None);
822 assert!(decision.is_allowed());
823 }
824
825 #[test]
826 fn gate_multiple_tool_overrides() {
827 let mut gate = TaintGate::new(SecurityConfig::default());
828 gate.set_tool_classification(
829 "tool_a".to_string(),
830 ToolClassification::new(
831 TaintLevel::Public,
832 ToolDirection::Outbound,
833 TaintLevel::Internal,
834 ),
835 );
836 gate.set_tool_classification(
837 "tool_b".to_string(),
838 ToolClassification::new(
839 TaintLevel::Public,
840 ToolDirection::Outbound,
841 TaintLevel::Private,
842 ),
843 );
844
845 let window = make_window_with_taint(TaintLevel::Private);
846
847 let decision_a = gate.check_traditional("agent-1", "tool_a", &window);
849 assert!(!decision_a.is_allowed());
850
851 let decision_b = gate.check_traditional("agent-1", "tool_b", &window);
853 assert!(decision_b.is_allowed());
854 }
855
856 #[test]
859 fn apply_resolution_allow_once_records_and_executes() {
860 let mut gate = TaintGate::new(SecurityConfig::default());
861 let out = gate.apply_resolution(
862 "a",
863 "shell",
864 "call1",
865 TaintLevel::Private,
866 TaintLevel::Public,
867 GateResolution::AllowOnce,
868 );
869 assert!(out.is_none()); let allow = gate
871 .audit_log()
872 .iter()
873 .find(|e| e.allowed)
874 .expect("an allowed event should be logged");
875 assert_eq!(allow.decision_source, GateDecisionSource::UserAllowOnce);
876 }
877
878 #[test]
879 fn apply_resolution_always_allow_raises_clearance() {
880 let mut gate = TaintGate::new(SecurityConfig::default());
881 let out = gate.apply_resolution(
882 "a",
883 "shell",
884 "call1",
885 TaintLevel::Private,
886 TaintLevel::Public,
887 GateResolution::AlwaysAllow,
888 );
889 assert!(out.is_none());
890 assert_eq!(
892 gate.tool_classification("shell").clearance,
893 TaintLevel::Private
894 );
895 let allow = gate
896 .audit_log()
897 .iter()
898 .find(|e| e.allowed)
899 .expect("an allowed event should be logged");
900 assert_eq!(allow.decision_source, GateDecisionSource::UserAlwaysAllow);
901 }
902
903 #[test]
904 fn apply_resolution_deny_returns_blocked_result() {
905 let mut gate = TaintGate::new(SecurityConfig::default());
906 let out = gate.apply_resolution(
907 "a",
908 "shell",
909 "call1",
910 TaintLevel::Private,
911 TaintLevel::Public,
912 GateResolution::Deny,
913 );
914 let (id, msg) = out.expect("deny yields a blocked result");
915 assert_eq!(id, "call1");
916 assert!(msg.contains("[blocked]") && msg.contains("shell"));
917 let deny = gate
918 .audit_log()
919 .iter()
920 .find(|e| !e.allowed)
921 .expect("a denied event should be logged");
922 assert_eq!(deny.decision_source, GateDecisionSource::UserDenied);
923 }
924}