Skip to main content

agentzero_channels/
outbound.rs

1//! Outbound message processing — applies security filters before sending.
2//!
3//! Currently applies the leak guard to scan and redact/block credential leaks
4//! in outbound channel messages.
5
6use crate::leak_guard::LeakGuardPolicy;
7use crate::SendMessage;
8
9/// Result of processing an outbound message through the security pipeline.
10#[derive(Debug, Clone)]
11pub enum OutboundResult {
12    /// Message is safe to send (possibly with redacted content).
13    Send(SendMessage),
14    /// Message was blocked by a security filter.
15    Blocked { reason: String },
16}
17
18/// Process an outbound message through the leak guard.
19///
20/// Returns `OutboundResult::Send` with potentially redacted content,
21/// or `OutboundResult::Blocked` if the leak guard action is "block".
22pub 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}