1use std::collections::{HashMap, VecDeque};
12
13use crate::identity::{AgentId, MessageKind, SignedMessage, TaskStatus, check_freshness};
14use crate::policy::Policy;
15
16pub const MAX_FUTURE_MS: u64 = 60_000;
19
20pub const MAX_PAST_MS: u64 = 86_400_000; #[derive(Debug, PartialEq, Eq)]
29pub enum Dispatch {
30 Inline {
34 petname: String,
35 text: String,
36 task_id: Option<String>,
37 status: Option<TaskStatus>,
38 in_reply_to: Option<String>,
39 },
40 PairRequest { from_key: String, name: String },
43 PairAccept { from_key: String, name: String },
46}
47
48#[derive(Debug, PartialEq, Eq)]
50pub enum Reject {
51 BadSignature,
52 NotAllowlisted,
53 WrongRecipient,
54 Stale,
55 Replay,
56 Unexpected,
59}
60
61pub type Verdict = Result<Dispatch, Reject>;
62
63pub fn decide(
66 msg: &SignedMessage,
67 me: AgentId,
68 policy: &Policy,
69 now: u64,
70 seen: &mut Dedupe,
71) -> Verdict {
72 let from = msg.verify().map_err(|_| Reject::BadSignature)?;
75
76 let to = AgentId::from_b64(&msg.to).map_err(|_| Reject::WrongRecipient)?;
78 if to != me {
79 return Err(Reject::WrongRecipient);
80 }
81
82 let peer = policy.peer(from);
90 let is_self = from == me;
91 if peer.is_none() && !is_self && msg.kind == MessageKind::Message {
92 return Err(Reject::NotAllowlisted);
93 }
94
95 check_freshness(msg.ts, now, MAX_FUTURE_MS, MAX_PAST_MS).map_err(|_| Reject::Stale)?;
98
99 if !seen.insert(&msg.msg_id) {
102 return Err(Reject::Replay);
103 }
104
105 match (peer, msg.kind) {
106 (Some(peer), MessageKind::Message) => Ok(Dispatch::Inline {
107 petname: peer.petname.clone(),
108 text: msg.text.clone(),
109 task_id: msg.task_id.clone(),
110 status: msg.status,
111 in_reply_to: msg.in_reply_to.clone(),
112 }),
113 (None, MessageKind::Message) if is_self => Ok(Dispatch::Inline {
115 petname: "self".to_string(),
116 text: msg.text.clone(),
117 task_id: msg.task_id.clone(),
118 status: msg.status,
119 in_reply_to: msg.in_reply_to.clone(),
120 }),
121 (None, MessageKind::PairRequest) => Ok(Dispatch::PairRequest {
123 from_key: from.to_b64(),
124 name: msg.text.clone(),
125 }),
126 (_, MessageKind::PairAccept) => Ok(Dispatch::PairAccept {
129 from_key: from.to_b64(),
130 name: msg.text.clone(),
131 }),
132 _ => Err(Reject::Unexpected),
134 }
135}
136
137pub struct PairTable {
141 order: VecDeque<String>,
142 map: HashMap<String, String>,
143 cap: usize,
144}
145
146impl PairTable {
147 pub fn new(cap: usize) -> Self {
148 Self {
149 order: VecDeque::new(),
150 map: HashMap::new(),
151 cap: cap.max(1),
152 }
153 }
154
155 pub fn put(&mut self, key: String, value: String) {
157 if !self.map.contains_key(&key) {
158 if self.order.len() >= self.cap
159 && let Some(old) = self.order.pop_front()
160 {
161 self.map.remove(&old);
162 }
163 self.order.push_back(key.clone());
164 }
165 self.map.insert(key, value);
166 }
167
168 pub fn take(&mut self, key: &str) -> Option<String> {
170 let v = self.map.remove(key)?;
171 self.order.retain(|k| k != key);
172 Some(v)
173 }
174
175 pub fn find(&self, key_or_fp: &str) -> Option<(String, String)> {
177 self.map
178 .iter()
179 .find(|(k, _)| {
180 k.as_str() == key_or_fp || k.chars().take(8).collect::<String>() == key_or_fp
181 })
182 .map(|(k, v)| (k.clone(), v.clone()))
183 }
184
185 pub fn entries(&self) -> impl Iterator<Item = (&String, &String)> {
186 self.map.iter()
187 }
188
189 pub fn is_empty(&self) -> bool {
190 self.map.is_empty()
191 }
192}
193
194pub struct Dedupe {
201 order: VecDeque<String>,
202 seen: std::collections::HashSet<String>,
203 cap: usize,
204}
205
206impl Dedupe {
207 pub fn new(cap: usize) -> Self {
208 Self {
209 order: VecDeque::with_capacity(cap),
210 seen: std::collections::HashSet::with_capacity(cap),
211 cap: cap.max(1),
212 }
213 }
214
215 pub fn insert(&mut self, id: &str) -> bool {
217 if self.seen.contains(id) {
218 return false;
219 }
220 if self.order.len() >= self.cap
221 && let Some(old) = self.order.pop_front()
222 {
223 self.seen.remove(&old);
224 }
225 self.order.push_back(id.to_string());
226 self.seen.insert(id.to_string());
227 true
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234 use crate::identity::AgentKey;
235 use crate::policy::Policy;
236
237 fn policy_for(key: &AgentKey) -> Policy {
238 let raw = format!(r#"{{ "alice": {{ "key": "{}" }} }}"#, key.id().to_b64());
239 Policy::parse(&raw).unwrap()
240 }
241
242 #[test]
243 fn admitted_peer_is_dispatched_inline() {
244 let (alice, me) = (AgentKey::generate().unwrap(), AgentKey::generate().unwrap());
245 let policy = policy_for(&alice);
246 let msg = alice.sign(me.id(), "run the deploy", 1_000, "m1");
247 let mut seen = Dedupe::new(16);
248 assert_eq!(
249 decide(&msg, me.id(), &policy, 1_000, &mut seen),
250 Ok(Dispatch::Inline {
251 petname: "alice".into(),
252 text: "run the deploy".into(),
253 task_id: None,
254 status: None,
255 in_reply_to: None,
256 })
257 );
258 }
259
260 #[test]
261 fn stranger_is_rejected_even_with_valid_signature() {
262 let (stranger, me) = (AgentKey::generate().unwrap(), AgentKey::generate().unwrap());
263 let policy = policy_for(&AgentKey::generate().unwrap()); let msg = stranger.sign(me.id(), "hi", 1_000, "m1");
265 let mut seen = Dedupe::new(16);
266 assert_eq!(
267 decide(&msg, me.id(), &policy, 1_000, &mut seen),
268 Err(Reject::NotAllowlisted)
269 );
270 }
271
272 #[test]
273 fn own_key_is_trusted_without_a_peers_entry() {
274 let me = AgentKey::generate().unwrap();
277 let policy = Policy::default(); let msg = me.sign(me.id(), "from my other session", 1_000, "m1");
279 let mut seen = Dedupe::new(16);
280 assert_eq!(
281 decide(&msg, me.id(), &policy, 1_000, &mut seen),
282 Ok(Dispatch::Inline {
283 petname: "self".into(),
284 text: "from my other session".into(),
285 task_id: None,
286 status: None,
287 in_reply_to: None,
288 })
289 );
290 }
291
292 #[test]
293 fn forged_sender_is_bad_signature() {
294 let (alice, me) = (AgentKey::generate().unwrap(), AgentKey::generate().unwrap());
295 let policy = policy_for(&alice);
296 let eve = AgentKey::generate().unwrap();
298 let mut msg = eve.sign(me.id(), "hi", 1_000, "m1");
299 msg.from = alice.id().to_b64();
300 let mut seen = Dedupe::new(16);
301 assert_eq!(
302 decide(&msg, me.id(), &policy, 1_000, &mut seen),
303 Err(Reject::BadSignature)
304 );
305 }
306
307 #[test]
308 fn message_for_someone_else_is_rejected() {
309 let (alice, me) = (AgentKey::generate().unwrap(), AgentKey::generate().unwrap());
310 let other = AgentKey::generate().unwrap();
311 let policy = policy_for(&alice);
312 let msg = alice.sign(other.id(), "hi", 1_000, "m1"); let mut seen = Dedupe::new(16);
314 assert_eq!(
315 decide(&msg, me.id(), &policy, 1_000, &mut seen),
316 Err(Reject::WrongRecipient)
317 );
318 }
319
320 #[test]
321 fn stale_message_is_rejected() {
322 let (alice, me) = (AgentKey::generate().unwrap(), AgentKey::generate().unwrap());
323 let policy = policy_for(&alice);
324 let msg = alice.sign(me.id(), "hi", 1_000, "m1");
325 let mut seen = Dedupe::new(16);
326 let long_after = 1_000 + MAX_PAST_MS + 1;
328 assert_eq!(
329 decide(&msg, me.id(), &policy, long_after, &mut seen),
330 Err(Reject::Stale)
331 );
332 let future_msg = alice.sign(me.id(), "hi", 1_000 + MAX_FUTURE_MS + 1, "m2");
334 assert_eq!(
335 decide(&future_msg, me.id(), &policy, 1_000, &mut seen),
336 Err(Reject::Stale)
337 );
338 let ok_msg = alice.sign(me.id(), "hi", 1_000, "m3");
340 assert!(decide(&ok_msg, me.id(), &policy, 1_000 + 300_000, &mut seen).is_ok());
341 }
342
343 #[test]
344 fn replay_is_rejected_the_second_time() {
345 let (alice, me) = (AgentKey::generate().unwrap(), AgentKey::generate().unwrap());
346 let policy = policy_for(&alice);
347 let msg = alice.sign(me.id(), "hi", 1_000, "m1");
348 let mut seen = Dedupe::new(16);
349 assert!(decide(&msg, me.id(), &policy, 1_000, &mut seen).is_ok());
350 assert_eq!(
351 decide(&msg, me.id(), &policy, 1_000, &mut seen),
352 Err(Reject::Replay)
353 );
354 }
355
356 #[test]
357 fn dedupe_forgets_oldest_beyond_cap() {
358 let mut d = Dedupe::new(2);
359 assert!(d.insert("a"));
360 assert!(d.insert("b"));
361 assert!(d.insert("c")); assert!(d.insert("a"), "a was evicted, so it is fresh again");
363 assert!(!d.insert("c"), "c is still within the window");
364 }
365
366 #[test]
367 fn non_peer_knock_is_surfaced_not_dropped() {
368 let (stranger, me) = (AgentKey::generate().unwrap(), AgentKey::generate().unwrap());
369 let policy = Policy::default(); let msg = stranger.sign_as(
371 me.id(),
372 "stranger-laptop",
373 1_000,
374 "k1",
375 MessageKind::PairRequest,
376 );
377 let mut seen = Dedupe::new(16);
378 assert_eq!(
379 decide(&msg, me.id(), &policy, 1_000, &mut seen),
380 Ok(Dispatch::PairRequest {
381 from_key: stranger.id().to_b64(),
382 name: "stranger-laptop".into()
383 })
384 );
385 }
386
387 #[test]
388 fn non_peer_plain_message_is_still_denied() {
389 let (stranger, me) = (AgentKey::generate().unwrap(), AgentKey::generate().unwrap());
390 let policy = Policy::default();
391 let msg = stranger.sign(me.id(), "hi", 1_000, "m1");
392 let mut seen = Dedupe::new(16);
393 assert_eq!(
394 decide(&msg, me.id(), &policy, 1_000, &mut seen),
395 Err(Reject::NotAllowlisted)
396 );
397 }
398
399 #[test]
400 fn pairing_kind_from_existing_peer_is_unexpected() {
401 let (alice, me) = (AgentKey::generate().unwrap(), AgentKey::generate().unwrap());
402 let policy = policy_for(&alice);
403 let msg = alice.sign_as(me.id(), "x", 1_000, "k1", MessageKind::PairRequest);
404 let mut seen = Dedupe::new(16);
405 assert_eq!(
406 decide(&msg, me.id(), &policy, 1_000, &mut seen),
407 Err(Reject::Unexpected)
408 );
409 }
410
411 #[test]
412 fn pair_table_put_take_and_find() {
413 let mut t = PairTable::new(8);
414 t.put("aaaabbbbcccc".into(), "desktop".into());
415 assert_eq!(
416 t.find("aaaabbbbcccc"), Some(("aaaabbbbcccc".into(), "desktop".into()))
418 );
419 assert_eq!(
420 t.find("aaaabbbb"), Some(("aaaabbbbcccc".into(), "desktop".into()))
422 );
423 assert_eq!(t.take("aaaabbbbcccc"), Some("desktop".into()));
424 assert!(t.is_empty());
425 }
426}