1use std::net::IpAddr;
8
9use serde::{Deserialize, Serialize};
10
11use crate::{IdprovaError, Result};
12
13#[derive(Debug, Clone, Default, Serialize, Deserialize)]
21pub struct DatConstraints {
22 #[serde(rename = "maxActions", skip_serializing_if = "Option::is_none")]
25 pub max_actions: Option<u64>,
26
27 #[serde(rename = "allowedServers", skip_serializing_if = "Option::is_none")]
29 pub allowed_servers: Option<Vec<String>>,
30
31 #[serde(rename = "requireReceipt", skip_serializing_if = "Option::is_none")]
33 pub require_receipt: Option<bool>,
34
35 #[serde(rename = "rateLimit", skip_serializing_if = "Option::is_none")]
38 pub rate_limit: Option<RateLimit>,
39
40 #[serde(rename = "ipAllowlist", skip_serializing_if = "Option::is_none")]
44 pub ip_allowlist: Option<Vec<String>>,
45
46 #[serde(rename = "ipDenylist", skip_serializing_if = "Option::is_none")]
49 pub ip_denylist: Option<Vec<String>>,
50
51 #[serde(rename = "minTrustLevel", skip_serializing_if = "Option::is_none")]
54 pub min_trust_level: Option<u8>,
55
56 #[serde(rename = "maxDelegationDepth", skip_serializing_if = "Option::is_none")]
59 pub max_delegation_depth: Option<u32>,
60
61 #[serde(rename = "allowedCountries", skip_serializing_if = "Option::is_none")]
65 pub allowed_countries: Option<Vec<String>>,
66
67 #[serde(rename = "timeWindows", skip_serializing_if = "Option::is_none")]
70 pub time_windows: Option<Vec<TimeWindow>>,
71
72 #[serde(rename = "requiredConfigHash", skip_serializing_if = "Option::is_none")]
76 pub required_config_hash: Option<String>,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct RateLimit {
86 pub max_actions: u64,
88 pub window_secs: u64,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct TimeWindow {
98 pub start_hour: u8,
100 pub end_hour: u8,
102 #[serde(skip_serializing_if = "Option::is_none")]
104 pub days_of_week: Option<Vec<u8>>,
105}
106
107#[derive(Debug, Clone, Default)]
113pub struct EvaluationContext {
114 pub actions_in_window: u64,
116
117 pub request_ip: Option<IpAddr>,
119
120 pub agent_trust_level: Option<u8>,
122
123 pub delegation_depth: u32,
125
126 pub country_code: Option<String>,
128
129 pub current_timestamp: Option<i64>,
132
133 pub agent_config_hash: Option<String>,
135}
136
137impl DatConstraints {
142 pub fn evaluate(&self, ctx: &EvaluationContext) -> Result<()> {
146 self.eval_rate_limit(ctx)?;
147 self.eval_ip_allowlist(ctx)?;
148 self.eval_ip_denylist(ctx)?;
149 self.eval_trust_level(ctx)?;
150 self.eval_delegation_depth(ctx)?;
151 self.eval_geofence(ctx)?;
152 self.eval_time_windows(ctx)?;
153 Ok(())
156 }
157
158 pub fn eval_rate_limit(&self, ctx: &EvaluationContext) -> Result<()> {
165 if let Some(rl) = &self.rate_limit {
166 if ctx.actions_in_window >= rl.max_actions {
167 return Err(IdprovaError::ConstraintViolated(format!(
168 "rate limit exceeded: {} actions in {}s window (max {})",
169 ctx.actions_in_window, rl.window_secs, rl.max_actions
170 )));
171 }
172 }
173 Ok(())
174 }
175
176 pub fn eval_ip_allowlist(&self, ctx: &EvaluationContext) -> Result<()> {
180 let allowlist = match &self.ip_allowlist {
181 Some(list) if !list.is_empty() => list,
182 _ => return Ok(()), };
184
185 let ip = match ctx.request_ip {
186 Some(ip) => ip,
187 None => {
188 return Err(IdprovaError::ConstraintViolated(
189 "ip_allowlist is set but no request IP was provided".into(),
190 ))
191 }
192 };
193
194 for cidr in allowlist {
195 if cidr_contains(cidr, ip) {
196 return Ok(());
197 }
198 }
199
200 Err(IdprovaError::ConstraintViolated(format!(
201 "request IP {} is not in the allowlist",
202 ip
203 )))
204 }
205
206 pub fn eval_ip_denylist(&self, ctx: &EvaluationContext) -> Result<()> {
210 let denylist = match &self.ip_denylist {
211 Some(list) if !list.is_empty() => list,
212 _ => return Ok(()),
213 };
214
215 let ip = match ctx.request_ip {
216 Some(ip) => ip,
217 None => return Ok(()), };
219
220 for cidr in denylist {
221 if cidr_contains(cidr, ip) {
222 return Err(IdprovaError::ConstraintViolated(format!(
223 "request IP {} is in the denylist ({})",
224 ip, cidr
225 )));
226 }
227 }
228
229 Ok(())
230 }
231
232 pub fn eval_trust_level(&self, ctx: &EvaluationContext) -> Result<()> {
236 let min = match self.min_trust_level {
237 Some(m) => m,
238 None => return Ok(()),
239 };
240
241 let actual = match ctx.agent_trust_level {
242 Some(t) => t,
243 None => {
244 return Err(IdprovaError::ConstraintViolated(format!(
245 "min_trust_level {} required but agent trust level was not provided",
246 min
247 )))
248 }
249 };
250
251 if actual < min {
252 return Err(IdprovaError::ConstraintViolated(format!(
253 "agent trust level {} is below required minimum {}",
254 actual, min
255 )));
256 }
257
258 Ok(())
259 }
260
261 pub fn eval_delegation_depth(&self, ctx: &EvaluationContext) -> Result<()> {
265 let max = match self.max_delegation_depth {
266 Some(m) => m,
267 None => return Ok(()),
268 };
269
270 if ctx.delegation_depth > max {
271 return Err(IdprovaError::ConstraintViolated(format!(
272 "delegation depth {} exceeds maximum {}",
273 ctx.delegation_depth, max
274 )));
275 }
276
277 Ok(())
278 }
279
280 pub fn eval_geofence(&self, ctx: &EvaluationContext) -> Result<()> {
284 let allowed = match &self.allowed_countries {
285 Some(list) if !list.is_empty() => list,
286 _ => return Ok(()),
287 };
288
289 let country = match &ctx.country_code {
290 Some(c) => c,
291 None => {
292 return Err(IdprovaError::ConstraintViolated(
293 "allowed_countries is set but no country code was provided".into(),
294 ))
295 }
296 };
297
298 let upper = country.to_uppercase();
299 if allowed.iter().any(|a| a.to_uppercase() == upper) {
300 return Ok(());
301 }
302
303 Err(IdprovaError::ConstraintViolated(format!(
304 "country '{}' is not in the geofence allowlist",
305 country
306 )))
307 }
308
309 pub fn eval_time_windows(&self, ctx: &EvaluationContext) -> Result<()> {
314 let windows = match &self.time_windows {
315 Some(w) if !w.is_empty() => w,
316 _ => return Ok(()),
317 };
318
319 let now_secs = ctx
320 .current_timestamp
321 .unwrap_or_else(|| chrono::Utc::now().timestamp());
322
323 let dt = chrono::DateTime::<chrono::Utc>::from_timestamp(now_secs, 0)
324 .ok_or_else(|| IdprovaError::ConstraintViolated("invalid timestamp".into()))?;
325
326 let hour = dt.hour() as u8;
327 let dow = dt.weekday().num_days_from_monday() as u8;
329
330 for w in windows {
331 if w.start_hour > 23 || w.end_hour > 23 {
333 return Err(IdprovaError::ConstraintViolated(
334 "time window hour out of range (0-23)".into(),
335 ));
336 }
337
338 if let Some(days) = &w.days_of_week {
340 if !days.contains(&dow) {
341 continue;
342 }
343 }
344
345 let in_range = if w.start_hour <= w.end_hour {
347 hour >= w.start_hour && hour <= w.end_hour
348 } else {
349 hour >= w.start_hour || hour <= w.end_hour
351 };
352
353 if in_range {
354 return Ok(());
355 }
356 }
357
358 Err(IdprovaError::ConstraintViolated(format!(
359 "current UTC hour {} is outside all permitted time windows",
360 hour
361 )))
362 }
363
364 pub fn eval_config_attestation(
371 &self,
372 ctx: &EvaluationContext,
373 token_config_hash: Option<&str>,
374 ) -> Result<()> {
375 let required = match &self.required_config_hash {
376 Some(h) => h,
377 None => return Ok(()),
378 };
379
380 let token_hash = match token_config_hash {
382 Some(h) => h,
383 None => return Err(IdprovaError::ConstraintViolated(
384 "required_config_hash constraint set but token carries no configAttestation claim"
385 .into(),
386 )),
387 };
388
389 if token_hash != required {
390 return Err(IdprovaError::ConstraintViolated(format!(
391 "token configAttestation '{}' does not match required hash '{}'",
392 token_hash, required
393 )));
394 }
395
396 let live_hash =
398 match &ctx.agent_config_hash {
399 Some(h) => h,
400 None => return Err(IdprovaError::ConstraintViolated(
401 "required_config_hash constraint set but agent config hash was not provided"
402 .into(),
403 )),
404 };
405
406 if live_hash != required {
407 return Err(IdprovaError::ConstraintViolated(format!(
408 "agent live config hash '{}' does not match required '{}'",
409 live_hash, required
410 )));
411 }
412
413 Ok(())
414 }
415}
416
417fn cidr_contains(cidr_str: &str, ip: IpAddr) -> bool {
426 let (addr_str, prefix_len) = match cidr_str.split_once('/') {
427 Some((a, p)) => (a, p.parse::<u32>().unwrap_or(128)),
428 None => (cidr_str, if cidr_str.contains(':') { 128 } else { 32 }),
429 };
430
431 let Ok(network_addr) = addr_str.parse::<IpAddr>() else {
432 return false;
433 };
434
435 match (network_addr, ip) {
436 (IpAddr::V4(net), IpAddr::V4(req)) => {
437 let prefix = prefix_len.min(32);
438 if prefix == 0 {
439 return true;
440 }
441 let shift = 32 - prefix;
442 (u32::from(net) >> shift) == (u32::from(req) >> shift)
443 }
444 (IpAddr::V6(net), IpAddr::V6(req)) => {
445 let prefix = prefix_len.min(128);
446 if prefix == 0 {
447 return true;
448 }
449 let net_bits = u128::from(net);
450 let req_bits = u128::from(req);
451 let shift = 128 - prefix;
452 (net_bits >> shift) == (req_bits >> shift)
453 }
454 _ => false,
456 }
457}
458
459use chrono::Datelike;
464use chrono::Timelike;
465
466#[cfg(test)]
471mod tests {
472 use super::*;
473 use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
474
475 fn ctx() -> EvaluationContext {
476 EvaluationContext::default()
477 }
478
479 #[test]
482 fn test_cidr_ipv4_exact() {
483 let ip = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 5));
484 assert!(cidr_contains("192.168.1.0/24", ip));
485 assert!(!cidr_contains("10.0.0.0/8", ip));
486 }
487
488 #[test]
489 fn test_cidr_ipv4_host() {
490 let ip = IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4));
491 assert!(cidr_contains("1.2.3.4", ip));
492 assert!(cidr_contains("1.2.3.4/32", ip));
493 assert!(!cidr_contains("1.2.3.5/32", ip));
494 }
495
496 #[test]
497 fn test_cidr_ipv4_slash0() {
498 let ip = IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8));
499 assert!(cidr_contains("0.0.0.0/0", ip));
500 }
501
502 #[test]
503 fn test_cidr_ipv6() {
504 let ip = IpAddr::V6(Ipv6Addr::LOCALHOST);
505 assert!(cidr_contains("::1/128", ip));
506 assert!(!cidr_contains("fe80::/10", ip));
507 }
508
509 #[test]
510 fn test_cidr_mismatch_family() {
511 let ipv4 = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1));
512 assert!(!cidr_contains("::1/128", ipv4));
513 }
514
515 #[test]
518 fn test_rate_limit_pass() {
519 let c = DatConstraints {
520 rate_limit: Some(RateLimit {
521 max_actions: 10,
522 window_secs: 60,
523 }),
524 ..Default::default()
525 };
526 let mut cx = ctx();
527 cx.actions_in_window = 9;
528 assert!(c.eval_rate_limit(&cx).is_ok());
529 }
530
531 #[test]
532 fn test_rate_limit_exceeded() {
533 let c = DatConstraints {
534 rate_limit: Some(RateLimit {
535 max_actions: 10,
536 window_secs: 60,
537 }),
538 ..Default::default()
539 };
540 let mut cx = ctx();
541 cx.actions_in_window = 10;
542 let err = c.eval_rate_limit(&cx).unwrap_err();
543 assert!(err.to_string().contains("rate limit exceeded"));
544 }
545
546 #[test]
547 fn test_rate_limit_none() {
548 let c = DatConstraints::default();
549 assert!(c.eval_rate_limit(&ctx()).is_ok());
550 }
551
552 #[test]
555 fn test_ip_allowlist_pass() {
556 let c = DatConstraints {
557 ip_allowlist: Some(vec!["10.0.0.0/8".into()]),
558 ..Default::default()
559 };
560 let mut cx = ctx();
561 cx.request_ip = Some(IpAddr::V4(Ipv4Addr::new(10, 1, 2, 3)));
562 assert!(c.eval_ip_allowlist(&cx).is_ok());
563 }
564
565 #[test]
566 fn test_ip_allowlist_fail() {
567 let c = DatConstraints {
568 ip_allowlist: Some(vec!["10.0.0.0/8".into()]),
569 ..Default::default()
570 };
571 let mut cx = ctx();
572 cx.request_ip = Some(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)));
573 assert!(c.eval_ip_allowlist(&cx).is_err());
574 }
575
576 #[test]
577 fn test_ip_allowlist_no_ip_provided() {
578 let c = DatConstraints {
579 ip_allowlist: Some(vec!["10.0.0.0/8".into()]),
580 ..Default::default()
581 };
582 assert!(c.eval_ip_allowlist(&ctx()).is_err());
583 }
584
585 #[test]
588 fn test_ip_denylist_blocked() {
589 let c = DatConstraints {
590 ip_denylist: Some(vec!["192.168.0.0/16".into()]),
591 ..Default::default()
592 };
593 let mut cx = ctx();
594 cx.request_ip = Some(IpAddr::V4(Ipv4Addr::new(192, 168, 5, 10)));
595 assert!(c.eval_ip_denylist(&cx).is_err());
596 }
597
598 #[test]
599 fn test_ip_denylist_pass() {
600 let c = DatConstraints {
601 ip_denylist: Some(vec!["192.168.0.0/16".into()]),
602 ..Default::default()
603 };
604 let mut cx = ctx();
605 cx.request_ip = Some(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)));
606 assert!(c.eval_ip_denylist(&cx).is_ok());
607 }
608
609 #[test]
610 fn test_ip_denylist_no_ip_is_ok() {
611 let c = DatConstraints {
613 ip_denylist: Some(vec!["0.0.0.0/0".into()]),
614 ..Default::default()
615 };
616 assert!(c.eval_ip_denylist(&ctx()).is_ok());
617 }
618
619 #[test]
622 fn test_trust_level_pass() {
623 let c = DatConstraints {
624 min_trust_level: Some(50),
625 ..Default::default()
626 };
627 let mut cx = ctx();
628 cx.agent_trust_level = Some(75);
629 assert!(c.eval_trust_level(&cx).is_ok());
630 }
631
632 #[test]
633 fn test_trust_level_equal_passes() {
634 let c = DatConstraints {
635 min_trust_level: Some(80),
636 ..Default::default()
637 };
638 let mut cx = ctx();
639 cx.agent_trust_level = Some(80);
640 assert!(c.eval_trust_level(&cx).is_ok());
641 }
642
643 #[test]
644 fn test_trust_level_fail() {
645 let c = DatConstraints {
646 min_trust_level: Some(80),
647 ..Default::default()
648 };
649 let mut cx = ctx();
650 cx.agent_trust_level = Some(40);
651 assert!(c.eval_trust_level(&cx).is_err());
652 }
653
654 #[test]
655 fn test_trust_level_not_provided() {
656 let c = DatConstraints {
657 min_trust_level: Some(1),
658 ..Default::default()
659 };
660 assert!(c.eval_trust_level(&ctx()).is_err());
661 }
662
663 #[test]
666 fn test_delegation_depth_pass() {
667 let c = DatConstraints {
668 max_delegation_depth: Some(3),
669 ..Default::default()
670 };
671 let mut cx = ctx();
672 cx.delegation_depth = 2;
673 assert!(c.eval_delegation_depth(&cx).is_ok());
674 }
675
676 #[test]
677 fn test_delegation_depth_at_limit() {
678 let c = DatConstraints {
679 max_delegation_depth: Some(3),
680 ..Default::default()
681 };
682 let mut cx = ctx();
683 cx.delegation_depth = 3;
684 assert!(c.eval_delegation_depth(&cx).is_ok());
685 }
686
687 #[test]
688 fn test_delegation_depth_exceeded() {
689 let c = DatConstraints {
690 max_delegation_depth: Some(2),
691 ..Default::default()
692 };
693 let mut cx = ctx();
694 cx.delegation_depth = 3;
695 assert!(c.eval_delegation_depth(&cx).is_err());
696 }
697
698 #[test]
699 fn test_delegation_depth_zero_no_redelegate() {
700 let c = DatConstraints {
701 max_delegation_depth: Some(0),
702 ..Default::default()
703 };
704 let mut cx = ctx();
705 cx.delegation_depth = 0;
706 assert!(c.eval_delegation_depth(&cx).is_ok());
707 cx.delegation_depth = 1;
708 assert!(c.eval_delegation_depth(&cx).is_err());
709 }
710
711 #[test]
714 fn test_geofence_pass() {
715 let c = DatConstraints {
716 allowed_countries: Some(vec!["AU".into(), "NZ".into()]),
717 ..Default::default()
718 };
719 let mut cx = ctx();
720 cx.country_code = Some("AU".into());
721 assert!(c.eval_geofence(&cx).is_ok());
722 }
723
724 #[test]
725 fn test_geofence_case_insensitive() {
726 let c = DatConstraints {
727 allowed_countries: Some(vec!["AU".into()]),
728 ..Default::default()
729 };
730 let mut cx = ctx();
731 cx.country_code = Some("au".into());
732 assert!(c.eval_geofence(&cx).is_ok());
733 }
734
735 #[test]
736 fn test_geofence_fail() {
737 let c = DatConstraints {
738 allowed_countries: Some(vec!["AU".into()]),
739 ..Default::default()
740 };
741 let mut cx = ctx();
742 cx.country_code = Some("US".into());
743 assert!(c.eval_geofence(&cx).is_err());
744 }
745
746 #[test]
747 fn test_geofence_no_country_code() {
748 let c = DatConstraints {
749 allowed_countries: Some(vec!["AU".into()]),
750 ..Default::default()
751 };
752 assert!(c.eval_geofence(&ctx()).is_err());
753 }
754
755 #[test]
758 fn test_time_window_pass() {
759 let ts = 1705327800_i64; let c = DatConstraints {
762 time_windows: Some(vec![TimeWindow {
763 start_hour: 9,
764 end_hour: 17,
765 days_of_week: None,
766 }]),
767 ..Default::default()
768 };
769 let mut cx = ctx();
770 cx.current_timestamp = Some(ts);
771 assert!(c.eval_time_windows(&cx).is_ok());
772 }
773
774 #[test]
775 fn test_time_window_fail_outside_hours() {
776 let ts = 1705276800_i64;
778 let c = DatConstraints {
779 time_windows: Some(vec![TimeWindow {
780 start_hour: 9,
781 end_hour: 17,
782 days_of_week: None,
783 }]),
784 ..Default::default()
785 };
786 let mut cx = ctx();
787 cx.current_timestamp = Some(ts);
788 assert!(c.eval_time_windows(&cx).is_err());
789 }
790
791 #[test]
792 fn test_time_window_day_of_week_pass() {
793 let ts = 1705327800_i64;
795 let c = DatConstraints {
796 time_windows: Some(vec![TimeWindow {
797 start_hour: 9,
798 end_hour: 17,
799 days_of_week: Some(vec![0, 1, 2, 3, 4]), }]),
801 ..Default::default()
802 };
803 let mut cx = ctx();
804 cx.current_timestamp = Some(ts);
805 assert!(c.eval_time_windows(&cx).is_ok());
806 }
807
808 #[test]
809 fn test_time_window_day_of_week_fail() {
810 let ts = 1705759200_i64;
812 let c = DatConstraints {
813 time_windows: Some(vec![TimeWindow {
814 start_hour: 9,
815 end_hour: 17,
816 days_of_week: Some(vec![0, 1, 2, 3, 4]), }]),
818 ..Default::default()
819 };
820 let mut cx = ctx();
821 cx.current_timestamp = Some(ts);
822 assert!(c.eval_time_windows(&cx).is_err());
823 }
824
825 #[test]
826 fn test_time_window_wraparound() {
827 let ts = 1705363200_i64;
830 let c = DatConstraints {
831 time_windows: Some(vec![TimeWindow {
832 start_hour: 22,
833 end_hour: 2,
834 days_of_week: None,
835 }]),
836 ..Default::default()
837 };
838 let mut cx = ctx();
839 cx.current_timestamp = Some(ts);
840 assert!(c.eval_time_windows(&cx).is_ok());
841 }
842
843 #[test]
846 fn test_config_attestation_pass() {
847 let hash = "abc123def456".to_string();
848 let c = DatConstraints {
849 required_config_hash: Some(hash.clone()),
850 ..Default::default()
851 };
852 let mut cx = ctx();
853 cx.agent_config_hash = Some(hash.clone());
854 assert!(c.eval_config_attestation(&cx, Some(&hash)).is_ok());
855 }
856
857 #[test]
858 fn test_config_attestation_token_mismatch() {
859 let c = DatConstraints {
860 required_config_hash: Some("required_hash".into()),
861 ..Default::default()
862 };
863 let mut cx = ctx();
864 cx.agent_config_hash = Some("required_hash".into());
865 assert!(c.eval_config_attestation(&cx, Some("other_hash")).is_err());
867 }
868
869 #[test]
870 fn test_config_attestation_live_mismatch() {
871 let c = DatConstraints {
872 required_config_hash: Some("required_hash".into()),
873 ..Default::default()
874 };
875 let mut cx = ctx();
876 cx.agent_config_hash = Some("different_hash".into());
877 assert!(c
878 .eval_config_attestation(&cx, Some("required_hash"))
879 .is_err());
880 }
881
882 #[test]
883 fn test_config_attestation_no_token_claim() {
884 let c = DatConstraints {
885 required_config_hash: Some("required_hash".into()),
886 ..Default::default()
887 };
888 let mut cx = ctx();
889 cx.agent_config_hash = Some("required_hash".into());
890 assert!(c.eval_config_attestation(&cx, None).is_err());
891 }
892
893 #[test]
894 fn test_config_attestation_no_constraint() {
895 let c = DatConstraints::default();
896 assert!(c.eval_config_attestation(&ctx(), None).is_ok());
897 }
898
899 #[test]
902 fn test_evaluate_all_pass() {
903 let c = DatConstraints {
904 rate_limit: Some(RateLimit {
905 max_actions: 100,
906 window_secs: 60,
907 }),
908 ip_allowlist: Some(vec!["10.0.0.0/8".into()]),
909 ip_denylist: Some(vec!["10.0.0.0/24".into()]), min_trust_level: Some(50),
911 max_delegation_depth: Some(3),
912 allowed_countries: Some(vec!["AU".into()]),
913 ..Default::default()
915 };
916 let mut cx = ctx();
917 cx.actions_in_window = 5;
918 cx.request_ip = Some(IpAddr::V4(Ipv4Addr::new(10, 1, 0, 1))); cx.agent_trust_level = Some(75);
920 cx.delegation_depth = 2;
921 cx.country_code = Some("AU".into());
922 assert!(c.evaluate(&cx).is_ok());
923 }
924
925 #[test]
926 fn test_evaluate_stops_at_first_violation() {
927 let c = DatConstraints {
928 rate_limit: Some(RateLimit {
929 max_actions: 1,
930 window_secs: 60,
931 }),
932 min_trust_level: Some(99), ..Default::default()
934 };
935 let mut cx = ctx();
936 cx.actions_in_window = 5; cx.agent_trust_level = Some(10);
938 let err = c.evaluate(&cx).unwrap_err().to_string();
939 assert!(err.contains("rate limit exceeded"));
940 }
941}