agentzero_channels/
outbound.rs1use crate::leak_guard::LeakGuardPolicy;
7use crate::SendMessage;
8
9#[derive(Debug, Clone)]
11pub enum OutboundResult {
12 Send(SendMessage),
14 Blocked { reason: String },
16}
17
18pub fn process_outbound(msg: SendMessage, guard: &LeakGuardPolicy) -> OutboundResult {
23 match guard.process(&msg.content) {
24 Ok(processed_content) => {
25 if processed_content == msg.content {
26 OutboundResult::Send(msg)
27 } else {
28 tracing::info!("leak guard redacted content in outbound message");
29 OutboundResult::Send(SendMessage {
30 content: processed_content,
31 ..msg
32 })
33 }
34 }
35 Err(reason) => {
36 tracing::warn!(reason = %reason, "leak guard blocked outbound message");
37 OutboundResult::Blocked { reason }
38 }
39 }
40}
41
42#[cfg(test)]
43mod tests {
44 use super::*;
45 use crate::leak_guard::{LeakAction, LeakGuardPolicy};
46
47 fn msg(content: &str) -> SendMessage {
48 SendMessage::new(content, "user-1")
49 }
50
51 #[test]
52 fn clean_message_passes_through() {
53 let guard = LeakGuardPolicy::default();
54 let result = process_outbound(msg("Hello, how can I help?"), &guard);
55 match result {
56 OutboundResult::Send(m) => assert_eq!(m.content, "Hello, how can I help?"),
57 OutboundResult::Blocked { .. } => panic!("should not be blocked"),
58 }
59 }
60
61 #[test]
62 fn leaked_key_gets_redacted() {
63 let guard = LeakGuardPolicy {
64 enabled: true,
65 action: LeakAction::Redact,
66 sensitivity: 0.7,
67 };
68 let result = process_outbound(
69 msg("Here is the key: sk-abc123def456ghi789jkl012mno345"),
70 &guard,
71 );
72 match result {
73 OutboundResult::Send(m) => {
74 assert!(m.content.contains("[REDACTED:"));
75 assert!(!m.content.contains("sk-abc123"));
76 }
77 OutboundResult::Blocked { .. } => panic!("should be redacted, not blocked"),
78 }
79 }
80
81 #[test]
82 fn leaked_key_gets_blocked() {
83 let guard = LeakGuardPolicy {
84 enabled: true,
85 action: LeakAction::Block,
86 sensitivity: 0.7,
87 };
88 let result = process_outbound(
89 msg("Here is the key: sk-abc123def456ghi789jkl012mno345"),
90 &guard,
91 );
92 match result {
93 OutboundResult::Blocked { reason } => {
94 assert!(reason.contains("blocked"));
95 }
96 OutboundResult::Send(_) => panic!("should be blocked"),
97 }
98 }
99
100 #[test]
101 fn disabled_guard_passes_everything() {
102 let guard = LeakGuardPolicy {
103 enabled: false,
104 action: LeakAction::Block,
105 sensitivity: 0.7,
106 };
107 let result = process_outbound(msg("sk-abc123def456ghi789jkl012mno345"), &guard);
108 match result {
109 OutboundResult::Send(m) => {
110 assert!(m.content.contains("sk-abc123"));
111 }
112 OutboundResult::Blocked { .. } => panic!("disabled guard should not block"),
113 }
114 }
115
116 #[test]
117 fn preserves_message_metadata() {
118 let guard = LeakGuardPolicy::default();
119 let original = SendMessage::with_subject("Hello", "user-1", "Subject")
120 .in_thread(Some("thread-1".into()));
121 let result = process_outbound(original, &guard);
122 match result {
123 OutboundResult::Send(m) => {
124 assert_eq!(m.recipient, "user-1");
125 assert_eq!(m.subject.as_deref(), Some("Subject"));
126 assert_eq!(m.thread_ts.as_deref(), Some("thread-1"));
127 }
128 OutboundResult::Blocked { .. } => panic!("should not be blocked"),
129 }
130 }
131}