Skip to main content

rustant_core/channels/
agent_bridge.rs

1//! Channels ↔ Multi-Agent bridge — routes channel messages to the multi-agent system.
2//!
3//! Optionally integrates with [`PairingManager`] to enforce device pairing for DM channels.
4//! When a `PairingManager` is attached, only messages from paired device IDs are routed;
5//! unpaired senders receive the `default_agent` fallback.
6
7use crate::channels::{ChannelMessage, ChannelType, ChannelUser};
8use crate::multi::messaging::{AgentEnvelope, AgentPayload};
9use crate::multi::routing::{AgentRouter, RouteRequest};
10use crate::pairing::PairingManager;
11use std::collections::HashMap;
12use uuid::Uuid;
13
14/// Bridge routing channel messages to agents and back.
15///
16/// When `pairing` is set, the bridge will only route messages from senders
17/// whose `sender.id` matches a paired device name. Messages from unpaired
18/// senders are routed to the `default_agent`.
19pub struct ChannelAgentBridge {
20    router: AgentRouter,
21    pairing: Option<PairingManager>,
22}
23
24impl ChannelAgentBridge {
25    pub fn new(router: AgentRouter) -> Self {
26        Self {
27            router,
28            pairing: None,
29        }
30    }
31
32    /// Attach a pairing manager to enforce device-pairing for DM routing.
33    pub fn with_pairing(mut self, pairing: PairingManager) -> Self {
34        self.pairing = Some(pairing);
35        self
36    }
37
38    /// Returns `true` if the sender is paired (or no pairing manager is set).
39    pub fn is_sender_paired(&self, sender_id: &str) -> bool {
40        match &self.pairing {
41            None => true,
42            Some(pm) => pm
43                .paired_devices()
44                .iter()
45                .any(|d| d.device_name == sender_id || d.device_id.to_string() == sender_id),
46        }
47    }
48
49    /// Access the pairing manager, if present.
50    pub fn pairing(&self) -> Option<&PairingManager> {
51        self.pairing.as_ref()
52    }
53
54    /// Mutable access to the pairing manager, if present.
55    pub fn pairing_mut(&mut self) -> Option<&mut PairingManager> {
56        self.pairing.as_mut()
57    }
58
59    /// Route a channel message to the appropriate agent.
60    /// Returns the target agent ID.
61    ///
62    /// If a pairing manager is attached, unpaired senders are routed to
63    /// `default_agent` regardless of router rules.
64    pub fn route_channel_message(&self, msg: &ChannelMessage, default_agent: Uuid) -> Uuid {
65        // When pairing is enabled, reject unpaired senders
66        if !self.is_sender_paired(&msg.sender.id) {
67            return default_agent;
68        }
69
70        let text = msg.content.as_text().unwrap_or("").to_string();
71        let request = RouteRequest::new()
72            .with_channel(msg.channel_type)
73            .with_user(&msg.sender.id)
74            .with_message(text);
75        self.router.route(&request).unwrap_or(default_agent)
76    }
77
78    /// Wrap a channel message into an AgentEnvelope (TaskRequest payload).
79    pub fn channel_message_to_envelope(
80        msg: &ChannelMessage,
81        from: Uuid,
82        to: Uuid,
83    ) -> AgentEnvelope {
84        let text = msg.content.as_text().unwrap_or("").to_string();
85        let mut args = HashMap::new();
86        args.insert("channel_type".into(), format!("{:?}", msg.channel_type));
87        args.insert("channel_id".into(), msg.channel_id.clone());
88        args.insert("sender".into(), msg.sender.id.clone());
89
90        AgentEnvelope::new(
91            from,
92            to,
93            AgentPayload::TaskRequest {
94                description: text,
95                args,
96            },
97        )
98    }
99
100    /// Extract a ChannelMessage from an AgentEnvelope (if it contains a TaskResult).
101    pub fn envelope_to_channel_message(
102        envelope: &AgentEnvelope,
103        channel_type: ChannelType,
104    ) -> Option<ChannelMessage> {
105        match &envelope.payload {
106            AgentPayload::TaskResult { output, .. } => {
107                let sender = ChannelUser::new("agent", channel_type);
108                Some(ChannelMessage::text(
109                    channel_type,
110                    "agent-response",
111                    sender,
112                    output,
113                ))
114            }
115            AgentPayload::Response { answer, .. } => {
116                let sender = ChannelUser::new("agent", channel_type);
117                Some(ChannelMessage::text(
118                    channel_type,
119                    "agent-response",
120                    sender,
121                    answer,
122                ))
123            }
124            _ => None,
125        }
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132    use crate::multi::routing::{AgentRoute, RouteCondition};
133
134    #[test]
135    fn test_agent_bridge_route_by_channel_type() {
136        let mut router = AgentRouter::new();
137        let agent_id = Uuid::new_v4();
138        let default = Uuid::new_v4();
139        router.add_route(AgentRoute {
140            priority: 1,
141            target_agent_id: agent_id,
142            conditions: vec![RouteCondition::ChannelType(ChannelType::Telegram)],
143        });
144
145        let bridge = ChannelAgentBridge::new(router);
146        let sender = ChannelUser::new("user1", ChannelType::Telegram);
147        let msg = ChannelMessage::text(ChannelType::Telegram, "telegram-chat", sender, "hi");
148
149        assert_eq!(bridge.route_channel_message(&msg, default), agent_id);
150    }
151
152    #[test]
153    fn test_agent_bridge_route_by_user() {
154        let mut router = AgentRouter::new();
155        let agent_id = Uuid::new_v4();
156        let default = Uuid::new_v4();
157        router.add_route(AgentRoute {
158            priority: 1,
159            target_agent_id: agent_id,
160            conditions: vec![RouteCondition::UserId("user-42".into())],
161        });
162
163        let bridge = ChannelAgentBridge::new(router);
164        let sender = ChannelUser::new("user-42", ChannelType::Slack);
165        let msg = ChannelMessage::text(ChannelType::Slack, "unknown-channel", sender, "hi");
166
167        assert_eq!(bridge.route_channel_message(&msg, default), agent_id);
168    }
169
170    #[test]
171    fn test_agent_bridge_fallback_to_default() {
172        let router = AgentRouter::new();
173        let default = Uuid::new_v4();
174
175        let bridge = ChannelAgentBridge::new(router);
176        let sender = ChannelUser::new("nobody", ChannelType::Discord);
177        let msg = ChannelMessage::text(ChannelType::Discord, "random", sender, "hi");
178
179        assert_eq!(bridge.route_channel_message(&msg, default), default);
180    }
181
182    #[test]
183    fn test_agent_bridge_channel_to_envelope() {
184        let sender = ChannelUser::new("user1", ChannelType::Telegram);
185        let msg = ChannelMessage::text(ChannelType::Telegram, "chat1", sender, "build project");
186        let from = Uuid::new_v4();
187        let to = Uuid::new_v4();
188
189        let envelope = ChannelAgentBridge::channel_message_to_envelope(&msg, from, to);
190        assert_eq!(envelope.from, from);
191        assert_eq!(envelope.to, to);
192        match &envelope.payload {
193            AgentPayload::TaskRequest { description, args } => {
194                assert_eq!(description, "build project");
195                assert_eq!(args.get("sender").unwrap(), "user1");
196            }
197            _ => panic!("Expected TaskRequest"),
198        }
199    }
200
201    #[test]
202    fn test_agent_bridge_envelope_to_channel() {
203        let from = Uuid::new_v4();
204        let to = Uuid::new_v4();
205        let envelope = AgentEnvelope::new(
206            from,
207            to,
208            AgentPayload::TaskResult {
209                success: true,
210                output: "Done!".into(),
211            },
212        );
213
214        let msg = ChannelAgentBridge::envelope_to_channel_message(&envelope, ChannelType::Telegram);
215        assert!(msg.is_some());
216        let msg = msg.unwrap();
217        assert_eq!(msg.content.as_text(), Some("Done!"));
218        assert_eq!(msg.channel_type, ChannelType::Telegram);
219    }
220
221    #[test]
222    fn test_agent_bridge_non_response_returns_none() {
223        let from = Uuid::new_v4();
224        let to = Uuid::new_v4();
225        let envelope = AgentEnvelope::new(from, to, AgentPayload::StatusQuery);
226
227        let msg = ChannelAgentBridge::envelope_to_channel_message(&envelope, ChannelType::Slack);
228        assert!(msg.is_none());
229    }
230
231    // -- Pairing integration --------------------------------------------------
232
233    #[test]
234    fn test_bridge_without_pairing_allows_all() {
235        let router = AgentRouter::new();
236        let bridge = ChannelAgentBridge::new(router);
237        assert!(bridge.is_sender_paired("anyone"));
238        assert!(bridge.pairing().is_none());
239    }
240
241    #[test]
242    fn test_bridge_with_pairing_rejects_unpaired_sender() {
243        let mut router = AgentRouter::new();
244        let agent_id = Uuid::new_v4();
245        let default = Uuid::new_v4();
246        router.add_route(AgentRoute {
247            priority: 1,
248            target_agent_id: agent_id,
249            conditions: vec![RouteCondition::ChannelType(ChannelType::IMessage)],
250        });
251
252        let pm = PairingManager::new(b"secret");
253        let bridge = ChannelAgentBridge::new(router).with_pairing(pm);
254
255        // No paired devices → unpaired sender goes to default
256        let sender = ChannelUser::new("stranger", ChannelType::IMessage);
257        let msg = ChannelMessage::text(ChannelType::IMessage, "dm", sender, "hello");
258        assert_eq!(bridge.route_channel_message(&msg, default), default);
259    }
260
261    #[test]
262    fn test_bridge_with_pairing_routes_paired_device() {
263        use crate::pairing::PairingResponse;
264
265        let secret = b"shared-secret-key-for-tests-32b!";
266        let mut router = AgentRouter::new();
267        let agent_id = Uuid::new_v4();
268        let default = Uuid::new_v4();
269        router.add_route(AgentRoute {
270            priority: 1,
271            target_agent_id: agent_id,
272            conditions: vec![RouteCondition::ChannelType(ChannelType::IMessage)],
273        });
274
275        let mut pm = PairingManager::new(secret);
276        let challenge = pm.create_challenge();
277
278        // Simulate the device computing the correct HMAC
279        use hmac::{Hmac, Mac};
280        use sha2::Sha256;
281        type HmacSha256 = Hmac<Sha256>;
282        let mut mac = HmacSha256::new_from_slice(secret).unwrap();
283        mac.update(challenge.nonce.as_bytes());
284        let hmac_result = mac.finalize().into_bytes();
285        let response_hmac: String = hmac_result.iter().map(|b| format!("{:02x}", b)).collect();
286
287        let device_id = Uuid::new_v4();
288        let pair_resp = PairingResponse {
289            challenge_id: challenge.challenge_id,
290            device_id,
291            device_name: "my-phone".into(),
292            public_key: "pk-abc".into(),
293            response_hmac,
294        };
295        pm.verify_response(&pair_resp);
296
297        let bridge = ChannelAgentBridge::new(router).with_pairing(pm);
298
299        // Paired device name matches sender.id → routed to agent
300        assert!(bridge.is_sender_paired("my-phone"));
301        let sender = ChannelUser::new("my-phone", ChannelType::IMessage);
302        let msg = ChannelMessage::text(ChannelType::IMessage, "dm", sender, "hello");
303        assert_eq!(bridge.route_channel_message(&msg, default), agent_id);
304
305        // Unknown sender → falls back to default
306        assert!(!bridge.is_sender_paired("stranger"));
307        let sender2 = ChannelUser::new("stranger", ChannelType::IMessage);
308        let msg2 = ChannelMessage::text(ChannelType::IMessage, "dm", sender2, "hello");
309        assert_eq!(bridge.route_channel_message(&msg2, default), default);
310    }
311
312    #[test]
313    fn test_bridge_pairing_revoke_device() {
314        use crate::pairing::PairingResponse;
315
316        let secret = b"shared-secret-key-for-tests-32b!";
317        let mut pm = PairingManager::new(secret);
318        let challenge = pm.create_challenge();
319
320        use hmac::{Hmac, Mac};
321        use sha2::Sha256;
322        type HmacSha256 = Hmac<Sha256>;
323        let mut mac = HmacSha256::new_from_slice(secret).unwrap();
324        mac.update(challenge.nonce.as_bytes());
325        let hmac_result = mac.finalize().into_bytes();
326        let response_hmac: String = hmac_result.iter().map(|b| format!("{:02x}", b)).collect();
327
328        let device_id = Uuid::new_v4();
329        let pair_resp = PairingResponse {
330            challenge_id: challenge.challenge_id,
331            device_id,
332            device_name: "laptop".into(),
333            public_key: "pk".into(),
334            response_hmac,
335        };
336        pm.verify_response(&pair_resp);
337
338        let router = AgentRouter::new();
339        let mut bridge = ChannelAgentBridge::new(router).with_pairing(pm);
340
341        assert!(bridge.is_sender_paired("laptop"));
342
343        // Revoke
344        bridge.pairing_mut().unwrap().revoke_device(&device_id);
345        assert!(!bridge.is_sender_paired("laptop"));
346    }
347}