1use crate::compiled::{
15 CompiledConditions, CompiledConstraint, CompiledContextCondition, CompiledIpRules,
16 CompiledNetworkRules, CompiledPathRules, CompiledPolicy,
17};
18use crate::error::PolicyValidationError;
19use crate::matcher::{CompiledToolMatcher, PatternMatcher};
20use crate::normalize::normalize_full;
21use crate::PolicyEngine;
22use globset::Glob;
23use ipnet::IpNet;
24use vellaveto_types::{Policy, PolicyType, MAX_CONDITIONS_SIZE};
25
26impl PolicyEngine {
27 pub fn compile_policies(
32 policies: &[Policy],
33 strict_mode: bool,
34 ) -> Result<Vec<CompiledPolicy>, Vec<PolicyValidationError>> {
35 let mut compiled = Vec::with_capacity(policies.len());
36 let mut errors = Vec::new();
37
38 for policy in policies {
39 match Self::compile_single_policy(policy, strict_mode) {
40 Ok(cp) => compiled.push(cp),
41 Err(e) => errors.push(e),
42 }
43 }
44
45 if !errors.is_empty() {
46 return Err(errors);
47 }
48
49 compiled.sort_by(|a, b| {
51 let pri = b.policy.priority.cmp(&a.policy.priority);
52 if pri != std::cmp::Ordering::Equal {
53 return pri;
54 }
55 let a_deny = matches!(a.policy.policy_type, PolicyType::Deny);
56 let b_deny = matches!(b.policy.policy_type, PolicyType::Deny);
57 let deny_ord = b_deny.cmp(&a_deny);
58 if deny_ord != std::cmp::Ordering::Equal {
59 return deny_ord;
60 }
61 a.policy.id.cmp(&b.policy.id)
62 });
63 Ok(compiled)
64 }
65
66 fn compile_single_policy(
68 policy: &Policy,
69 strict_mode: bool,
70 ) -> Result<CompiledPolicy, PolicyValidationError> {
71 const MAX_POLICY_NAME_LEN: usize = 512;
76 if policy.name.len() > MAX_POLICY_NAME_LEN {
77 return Err(PolicyValidationError {
78 policy_id: policy.id.clone(),
79 policy_name: policy.name.clone(),
80 reason: format!(
81 "Policy name is {} bytes, max is {}",
82 policy.name.len(),
83 MAX_POLICY_NAME_LEN
84 ),
85 });
86 }
87
88 let tool_matcher = CompiledToolMatcher::compile(&policy.id);
89
90 let CompiledConditions {
91 require_approval,
92 forbidden_parameters,
93 required_parameters,
94 constraints,
95 on_no_match_continue,
96 context_conditions,
97 } = match &policy.policy_type {
98 PolicyType::Allow | PolicyType::Deny => CompiledConditions {
99 require_approval: false,
100 forbidden_parameters: Vec::new(),
101 required_parameters: Vec::new(),
102 constraints: Vec::new(),
103 on_no_match_continue: false,
104 context_conditions: Vec::new(),
105 },
106 PolicyType::Conditional { conditions } => {
107 Self::compile_conditions(policy, conditions, strict_mode)?
108 }
109 _ => {
113 return Err(PolicyValidationError {
114 policy_id: policy.id.clone(),
115 policy_name: policy.name.clone(),
116 reason: format!(
117 "Unknown policy type variant for policy '{}' — cannot compile",
118 policy.name
119 ),
120 });
121 }
122 };
123
124 let deny_reason = format!("Denied by policy '{}'", policy.name);
125 let approval_reason = format!("Approval required by policy '{}'", policy.name);
126 let forbidden_reasons = forbidden_parameters
127 .iter()
128 .map(|p| format!("Parameter '{}' is forbidden by policy '{}'", p, policy.name))
129 .collect();
130 let required_reasons = required_parameters
131 .iter()
132 .map(|p| {
133 format!(
134 "Required parameter '{}' missing (policy '{}')",
135 p, policy.name
136 )
137 })
138 .collect();
139
140 let compiled_path_rules = match policy.path_rules.as_ref() {
144 Some(pr) => {
145 let mut allowed = Vec::with_capacity(pr.allowed.len());
146 for pattern in &pr.allowed {
147 let g = Glob::new(pattern).map_err(|e| PolicyValidationError {
148 policy_id: policy.id.clone(),
149 policy_name: policy.name.clone(),
150 reason: format!("Invalid allowed path glob '{}': {}", pattern, e),
151 })?;
152 allowed.push((pattern.clone(), g.compile_matcher()));
153 }
154 let mut blocked = Vec::with_capacity(pr.blocked.len());
155 for pattern in &pr.blocked {
156 let g = Glob::new(pattern).map_err(|e| PolicyValidationError {
157 policy_id: policy.id.clone(),
158 policy_name: policy.name.clone(),
159 reason: format!("Invalid blocked path glob '{}': {}", pattern, e),
160 })?;
161 blocked.push((pattern.clone(), g.compile_matcher()));
162 }
163 Some(CompiledPathRules { allowed, blocked })
164 }
165 None => None,
166 };
167
168 let compiled_network_rules = match policy.network_rules.as_ref() {
171 Some(nr) => {
172 for domain in nr.allowed_domains.iter().chain(nr.blocked_domains.iter()) {
173 if let Err(reason) = Self::validate_domain_pattern(domain) {
174 return Err(PolicyValidationError {
175 policy_id: policy.id.clone(),
176 policy_name: policy.name.clone(),
177 reason: format!("Invalid domain pattern: {}", reason),
178 });
179 }
180 }
181 Some(CompiledNetworkRules {
182 allowed_domains: nr.allowed_domains.clone(),
183 blocked_domains: nr.blocked_domains.clone(),
184 })
185 }
186 None => None,
187 };
188
189 let compiled_ip_rules = match policy
191 .network_rules
192 .as_ref()
193 .and_then(|nr| nr.ip_rules.as_ref())
194 {
195 Some(ir) => {
196 let mut blocked_cidrs = Vec::with_capacity(ir.blocked_cidrs.len());
197 for cidr_str in &ir.blocked_cidrs {
198 let cidr: IpNet = cidr_str.parse().map_err(|e| PolicyValidationError {
199 policy_id: policy.id.clone(),
200 policy_name: policy.name.clone(),
201 reason: format!("Invalid blocked CIDR '{}': {}", cidr_str, e),
202 })?;
203 blocked_cidrs.push(cidr);
204 }
205 let mut allowed_cidrs = Vec::with_capacity(ir.allowed_cidrs.len());
206 for cidr_str in &ir.allowed_cidrs {
207 let cidr: IpNet = cidr_str.parse().map_err(|e| PolicyValidationError {
208 policy_id: policy.id.clone(),
209 policy_name: policy.name.clone(),
210 reason: format!("Invalid allowed CIDR '{}': {}", cidr_str, e),
211 })?;
212 allowed_cidrs.push(cidr);
213 }
214 Some(CompiledIpRules {
215 block_private: ir.block_private,
216 blocked_cidrs,
217 allowed_cidrs,
218 })
219 }
220 None => None,
221 };
222
223 Ok(CompiledPolicy {
224 policy: policy.clone(),
225 tool_matcher,
226 require_approval,
227 forbidden_parameters,
228 required_parameters,
229 constraints,
230 on_no_match_continue,
231 deny_reason,
232 approval_reason,
233 forbidden_reasons,
234 required_reasons,
235 compiled_path_rules,
236 compiled_network_rules,
237 compiled_ip_rules,
238 context_conditions,
239 })
240 }
241
242 fn compile_conditions(
244 policy: &Policy,
245 conditions: &serde_json::Value,
246 strict_mode: bool,
247 ) -> Result<CompiledConditions, PolicyValidationError> {
248 if Self::json_depth(conditions) > 10 {
250 return Err(PolicyValidationError {
251 policy_id: policy.id.clone(),
252 policy_name: policy.name.clone(),
253 reason: "Condition JSON exceeds maximum nesting depth of 10".to_string(),
254 });
255 }
256
257 let size = conditions.to_string().len();
264 if size > MAX_CONDITIONS_SIZE {
265 return Err(PolicyValidationError {
266 policy_id: policy.id.clone(),
267 policy_name: policy.name.clone(),
268 reason: format!(
269 "Condition JSON too large: {} bytes (max {})",
270 size, MAX_CONDITIONS_SIZE
271 ),
272 });
273 }
274
275 let require_approval = conditions
278 .get("require_approval")
279 .map(|v| v.as_bool().unwrap_or(true))
280 .unwrap_or(false);
281
282 let on_no_match_continue = conditions
283 .get("on_no_match")
284 .and_then(|v| v.as_str())
285 .map(|s| s == "continue")
286 .unwrap_or(false);
287
288 let forbidden_parameters: Vec<String> = conditions
289 .get("forbidden_parameters")
290 .and_then(|v| v.as_array())
291 .map(|arr| {
292 arr.iter()
293 .filter_map(|v| v.as_str().map(|s| s.to_string()))
294 .collect()
295 })
296 .unwrap_or_default();
297
298 let required_parameters: Vec<String> = conditions
299 .get("required_parameters")
300 .and_then(|v| v.as_array())
301 .map(|arr| {
302 arr.iter()
303 .filter_map(|v| v.as_str().map(|s| s.to_string()))
304 .collect()
305 })
306 .unwrap_or_default();
307
308 const MAX_PARAMETER_CONSTRAINTS: usize = 100;
310
311 let constraints = if let Some(constraint_arr) = conditions.get("parameter_constraints") {
312 let arr = constraint_arr
313 .as_array()
314 .ok_or_else(|| PolicyValidationError {
315 policy_id: policy.id.clone(),
316 policy_name: policy.name.clone(),
317 reason: "parameter_constraints must be an array".to_string(),
318 })?;
319
320 if arr.len() > MAX_PARAMETER_CONSTRAINTS {
323 return Err(PolicyValidationError {
324 policy_id: policy.id.clone(),
325 policy_name: policy.name.clone(),
326 reason: format!(
327 "parameter_constraints has {} entries, max is {}",
328 arr.len(),
329 MAX_PARAMETER_CONSTRAINTS
330 ),
331 });
332 }
333
334 let mut constraints = Vec::with_capacity(arr.len());
335 for constraint_val in arr {
336 constraints.push(Self::compile_constraint(policy, constraint_val)?);
337 }
338 constraints
339 } else {
340 Vec::new()
341 };
342
343 const MAX_CONTEXT_CONDITIONS: usize = 50;
345
346 let context_conditions = if let Some(ctx_arr) = conditions.get("context_conditions") {
348 let arr = ctx_arr.as_array().ok_or_else(|| PolicyValidationError {
349 policy_id: policy.id.clone(),
350 policy_name: policy.name.clone(),
351 reason: "context_conditions must be an array".to_string(),
352 })?;
353
354 if arr.len() > MAX_CONTEXT_CONDITIONS {
357 return Err(PolicyValidationError {
358 policy_id: policy.id.clone(),
359 policy_name: policy.name.clone(),
360 reason: format!(
361 "context_conditions has {} entries, max is {}",
362 arr.len(),
363 MAX_CONTEXT_CONDITIONS
364 ),
365 });
366 }
367
368 let mut context_conditions = Vec::with_capacity(arr.len());
369 for ctx_val in arr {
370 context_conditions.push(Self::compile_context_condition(policy, ctx_val)?);
371 }
372 context_conditions
373 } else {
374 Vec::new()
375 };
376
377 if strict_mode {
379 let known_keys = [
380 "require_approval",
381 "forbidden_parameters",
382 "required_parameters",
383 "parameter_constraints",
384 "context_conditions",
385 "on_no_match",
386 ];
387 if let Some(obj) = conditions.as_object() {
388 for key in obj.keys() {
389 if !known_keys.contains(&key.as_str()) {
390 return Err(PolicyValidationError {
391 policy_id: policy.id.clone(),
392 policy_name: policy.name.clone(),
393 reason: format!("Unknown condition key '{}' in strict mode", key),
394 });
395 }
396 }
397 }
398 }
399
400 if on_no_match_continue
410 && !constraints.is_empty()
411 && constraints.iter().all(|c| c.on_missing() == "skip")
412 && forbidden_parameters.is_empty()
413 && required_parameters.is_empty()
414 {
415 tracing::warn!(
416 policy_id = %policy.id,
417 policy_name = %policy.name,
418 constraint_count = constraints.len(),
419 "Conditional policy has on_no_match=\"continue\" with ALL constraints \
420 using on_missing=\"skip\" — an attacker can bypass this policy entirely \
421 by omitting the required parameters. Set at least one constraint to \
422 on_missing=\"deny\" or remove on_no_match=\"continue\"."
423 );
424 }
425
426 Ok(CompiledConditions {
427 require_approval,
428 forbidden_parameters,
429 required_parameters,
430 constraints,
431 on_no_match_continue,
432 context_conditions,
433 })
434 }
435
436 fn compile_constraint(
441 policy: &Policy,
442 constraint: &serde_json::Value,
443 ) -> Result<CompiledConstraint, PolicyValidationError> {
444 const MAX_CONSTRAINT_ELEMENTS: usize = 1000;
446 let obj = constraint
447 .as_object()
448 .ok_or_else(|| PolicyValidationError {
449 policy_id: policy.id.clone(),
450 policy_name: policy.name.clone(),
451 reason: "Each parameter constraint must be a JSON object".to_string(),
452 })?;
453
454 let param = obj
455 .get("param")
456 .and_then(|v| v.as_str())
457 .ok_or_else(|| PolicyValidationError {
458 policy_id: policy.id.clone(),
459 policy_name: policy.name.clone(),
460 reason: "Constraint missing required 'param' string field".to_string(),
461 })?
462 .to_string();
463
464 let op = obj
465 .get("op")
466 .and_then(|v| v.as_str())
467 .ok_or_else(|| PolicyValidationError {
468 policy_id: policy.id.clone(),
469 policy_name: policy.name.clone(),
470 reason: "Constraint missing required 'op' string field".to_string(),
471 })?;
472
473 let on_match = obj
474 .get("on_match")
475 .and_then(|v| v.as_str())
476 .unwrap_or("deny")
477 .to_string();
478 match on_match.as_str() {
481 "deny" | "allow" | "require_approval" => {}
482 other => {
483 return Err(PolicyValidationError {
484 policy_id: policy.id.clone(),
485 policy_name: policy.name.clone(),
486 reason: format!(
487 "Constraint 'on_match' value '{}' is invalid; expected 'deny', 'allow', or 'require_approval'",
488 other
489 ),
490 });
491 }
492 }
493 let on_missing = obj
494 .get("on_missing")
495 .and_then(|v| v.as_str())
496 .unwrap_or("deny")
497 .to_string();
498 match on_missing.as_str() {
499 "deny" | "skip" => {}
500 other => {
501 return Err(PolicyValidationError {
502 policy_id: policy.id.clone(),
503 policy_name: policy.name.clone(),
504 reason: format!(
505 "Constraint 'on_missing' value '{}' is invalid; expected 'deny' or 'skip'",
506 other
507 ),
508 });
509 }
510 }
511
512 match op {
513 "glob" => {
514 let pattern_str = obj
515 .get("pattern")
516 .and_then(|v| v.as_str())
517 .ok_or_else(|| PolicyValidationError {
518 policy_id: policy.id.clone(),
519 policy_name: policy.name.clone(),
520 reason: "glob constraint missing 'pattern' string".to_string(),
521 })?
522 .to_string();
523
524 let matcher = Glob::new(&pattern_str)
525 .map_err(|e| PolicyValidationError {
526 policy_id: policy.id.clone(),
527 policy_name: policy.name.clone(),
528 reason: format!("Invalid glob pattern '{}': {}", pattern_str, e),
529 })?
530 .compile_matcher();
531
532 Ok(CompiledConstraint::Glob {
533 param,
534 matcher,
535 pattern_str,
536 on_match,
537 on_missing,
538 })
539 }
540 "not_glob" => {
541 let patterns = obj
542 .get("patterns")
543 .and_then(|v| v.as_array())
544 .ok_or_else(|| PolicyValidationError {
545 policy_id: policy.id.clone(),
546 policy_name: policy.name.clone(),
547 reason: "not_glob constraint missing 'patterns' array".to_string(),
548 })?;
549
550 if patterns.len() > MAX_CONSTRAINT_ELEMENTS {
552 return Err(PolicyValidationError {
553 policy_id: policy.id.clone(),
554 policy_name: policy.name.clone(),
555 reason: format!(
556 "not_glob patterns count {} exceeds maximum {}",
557 patterns.len(),
558 MAX_CONSTRAINT_ELEMENTS
559 ),
560 });
561 }
562
563 let mut matchers = Vec::new();
564 for pat_val in patterns {
565 let pat_str = pat_val.as_str().ok_or_else(|| PolicyValidationError {
566 policy_id: policy.id.clone(),
567 policy_name: policy.name.clone(),
568 reason: "not_glob patterns must be strings".to_string(),
569 })?;
570 let matcher = Glob::new(pat_str)
571 .map_err(|e| PolicyValidationError {
572 policy_id: policy.id.clone(),
573 policy_name: policy.name.clone(),
574 reason: format!("Invalid glob pattern '{}': {}", pat_str, e),
575 })?
576 .compile_matcher();
577 matchers.push((pat_str.to_string(), matcher));
578 }
579
580 Ok(CompiledConstraint::NotGlob {
581 param,
582 matchers,
583 on_match,
584 on_missing,
585 })
586 }
587 "regex" => {
588 let pattern_str = obj
589 .get("pattern")
590 .and_then(|v| v.as_str())
591 .ok_or_else(|| PolicyValidationError {
592 policy_id: policy.id.clone(),
593 policy_name: policy.name.clone(),
594 reason: "regex constraint missing 'pattern' string".to_string(),
595 })?
596 .to_string();
597
598 Self::validate_regex_safety(&pattern_str).map_err(|reason| {
600 PolicyValidationError {
601 policy_id: policy.id.clone(),
602 policy_name: policy.name.clone(),
603 reason,
604 }
605 })?;
606
607 let regex = regex::RegexBuilder::new(&pattern_str)
610 .dfa_size_limit(256 * 1024)
611 .size_limit(256 * 1024)
612 .build()
613 .map_err(|e| PolicyValidationError {
614 policy_id: policy.id.clone(),
615 policy_name: policy.name.clone(),
616 reason: format!("Invalid regex pattern '{}': {}", pattern_str, e),
617 })?;
618
619 Ok(CompiledConstraint::Regex {
620 param,
621 regex,
622 pattern_str,
623 on_match,
624 on_missing,
625 })
626 }
627 "domain_match" => {
628 let pattern = obj
629 .get("pattern")
630 .and_then(|v| v.as_str())
631 .ok_or_else(|| PolicyValidationError {
632 policy_id: policy.id.clone(),
633 policy_name: policy.name.clone(),
634 reason: "domain_match constraint missing 'pattern' string".to_string(),
635 })?
636 .to_string();
637
638 if let Err(reason) = crate::domain::validate_domain_pattern(&pattern) {
640 return Err(PolicyValidationError {
641 policy_id: policy.id.clone(),
642 policy_name: policy.name.clone(),
643 reason: format!("domain_match pattern invalid: {}", reason),
644 });
645 }
646
647 Ok(CompiledConstraint::DomainMatch {
648 param,
649 pattern,
650 on_match,
651 on_missing,
652 })
653 }
654 "domain_not_in" => {
655 let patterns_arr =
656 obj.get("patterns")
657 .and_then(|v| v.as_array())
658 .ok_or_else(|| PolicyValidationError {
659 policy_id: policy.id.clone(),
660 policy_name: policy.name.clone(),
661 reason: "domain_not_in constraint missing 'patterns' array".to_string(),
662 })?;
663
664 if patterns_arr.len() > MAX_CONSTRAINT_ELEMENTS {
666 return Err(PolicyValidationError {
667 policy_id: policy.id.clone(),
668 policy_name: policy.name.clone(),
669 reason: format!(
670 "domain_not_in patterns count {} exceeds maximum {}",
671 patterns_arr.len(),
672 MAX_CONSTRAINT_ELEMENTS
673 ),
674 });
675 }
676
677 let mut patterns = Vec::new();
678 for pat_val in patterns_arr {
679 let pat_str = pat_val.as_str().ok_or_else(|| PolicyValidationError {
680 policy_id: policy.id.clone(),
681 policy_name: policy.name.clone(),
682 reason: "domain_not_in patterns must be strings".to_string(),
683 })?;
684 if let Err(reason) = crate::domain::validate_domain_pattern(pat_str) {
686 return Err(PolicyValidationError {
687 policy_id: policy.id.clone(),
688 policy_name: policy.name.clone(),
689 reason: format!("domain_not_in pattern invalid: {}", reason),
690 });
691 }
692 patterns.push(pat_str.to_string());
693 }
694
695 Ok(CompiledConstraint::DomainNotIn {
696 param,
697 patterns,
698 on_match,
699 on_missing,
700 })
701 }
702 "eq" => {
703 let value = obj
704 .get("value")
705 .ok_or_else(|| PolicyValidationError {
706 policy_id: policy.id.clone(),
707 policy_name: policy.name.clone(),
708 reason: "eq constraint missing 'value' field".to_string(),
709 })?
710 .clone();
711
712 Ok(CompiledConstraint::Eq {
713 param,
714 value,
715 on_match,
716 on_missing,
717 })
718 }
719 "ne" => {
720 let value = obj
721 .get("value")
722 .ok_or_else(|| PolicyValidationError {
723 policy_id: policy.id.clone(),
724 policy_name: policy.name.clone(),
725 reason: "ne constraint missing 'value' field".to_string(),
726 })?
727 .clone();
728
729 Ok(CompiledConstraint::Ne {
730 param,
731 value,
732 on_match,
733 on_missing,
734 })
735 }
736 "one_of" => {
737 let values = obj
738 .get("values")
739 .and_then(|v| v.as_array())
740 .ok_or_else(|| PolicyValidationError {
741 policy_id: policy.id.clone(),
742 policy_name: policy.name.clone(),
743 reason: "one_of constraint missing 'values' array".to_string(),
744 })?;
745
746 if values.len() > MAX_CONSTRAINT_ELEMENTS {
748 return Err(PolicyValidationError {
749 policy_id: policy.id.clone(),
750 policy_name: policy.name.clone(),
751 reason: format!(
752 "one_of values count {} exceeds maximum {}",
753 values.len(),
754 MAX_CONSTRAINT_ELEMENTS
755 ),
756 });
757 }
758 let values = values.clone();
759
760 Ok(CompiledConstraint::OneOf {
761 param,
762 values,
763 on_match,
764 on_missing,
765 })
766 }
767 "none_of" => {
768 let values = obj
769 .get("values")
770 .and_then(|v| v.as_array())
771 .ok_or_else(|| PolicyValidationError {
772 policy_id: policy.id.clone(),
773 policy_name: policy.name.clone(),
774 reason: "none_of constraint missing 'values' array".to_string(),
775 })?;
776
777 if values.len() > MAX_CONSTRAINT_ELEMENTS {
779 return Err(PolicyValidationError {
780 policy_id: policy.id.clone(),
781 policy_name: policy.name.clone(),
782 reason: format!(
783 "none_of values count {} exceeds maximum {}",
784 values.len(),
785 MAX_CONSTRAINT_ELEMENTS
786 ),
787 });
788 }
789 let values = values.clone();
790
791 Ok(CompiledConstraint::NoneOf {
792 param,
793 values,
794 on_match,
795 on_missing,
796 })
797 }
798 _ => Err(PolicyValidationError {
799 policy_id: policy.id.clone(),
800 policy_name: policy.name.clone(),
801 reason: format!("Unknown constraint operator '{}'", op),
802 }),
803 }
804 }
805
806 fn compile_context_condition(
808 policy: &Policy,
809 value: &serde_json::Value,
810 ) -> Result<CompiledContextCondition, PolicyValidationError> {
811 let obj = value.as_object().ok_or_else(|| PolicyValidationError {
812 policy_id: policy.id.clone(),
813 policy_name: policy.name.clone(),
814 reason: "Each context condition must be a JSON object".to_string(),
815 })?;
816
817 let kind =
818 obj.get("type")
819 .and_then(|v| v.as_str())
820 .ok_or_else(|| PolicyValidationError {
821 policy_id: policy.id.clone(),
822 policy_name: policy.name.clone(),
823 reason: "Context condition missing required 'type' string field".to_string(),
824 })?;
825
826 match kind {
827 "time_window" => {
828 let start_hour_u64 =
833 obj.get("start_hour")
834 .and_then(|v| v.as_u64())
835 .ok_or_else(|| PolicyValidationError {
836 policy_id: policy.id.clone(),
837 policy_name: policy.name.clone(),
838 reason: "time_window missing 'start_hour' integer".to_string(),
839 })?;
840 let end_hour_u64 =
841 obj.get("end_hour")
842 .and_then(|v| v.as_u64())
843 .ok_or_else(|| PolicyValidationError {
844 policy_id: policy.id.clone(),
845 policy_name: policy.name.clone(),
846 reason: "time_window missing 'end_hour' integer".to_string(),
847 })?;
848 if start_hour_u64 > 23 || end_hour_u64 > 23 {
849 return Err(PolicyValidationError {
850 policy_id: policy.id.clone(),
851 policy_name: policy.name.clone(),
852 reason: format!(
853 "time_window hours must be 0-23, got start={} end={}",
854 start_hour_u64, end_hour_u64
855 ),
856 });
857 }
858 let start_hour = start_hour_u64 as u8;
859 let end_hour = end_hour_u64 as u8;
860 let days_u64: Vec<u64> = obj
863 .get("days")
864 .and_then(|v| v.as_array())
865 .map(|arr| arr.iter().filter_map(|v| v.as_u64()).collect())
866 .unwrap_or_default();
867 for &day in &days_u64 {
868 if !(1..=7).contains(&day) {
869 return Err(PolicyValidationError {
870 policy_id: policy.id.clone(),
871 policy_name: policy.name.clone(),
872 reason: format!(
873 "time_window day value must be 1-7 (Mon-Sun), got {}",
874 day
875 ),
876 });
877 }
878 }
879 let days: Vec<u8> = days_u64.iter().map(|&d| d as u8).collect();
880 if start_hour == end_hour {
885 return Err(PolicyValidationError {
886 policy_id: policy.id.clone(),
887 policy_name: policy.name.clone(),
888 reason: format!(
889 "time_window start_hour and end_hour must differ (both are {}); \
890 a zero-width window permanently denies all requests",
891 start_hour
892 ),
893 });
894 }
895 let deny_reason = format!(
896 "Outside allowed time window ({:02}:00-{:02}:00) for policy '{}'",
897 start_hour, end_hour, policy.name
898 );
899 Ok(CompiledContextCondition::TimeWindow {
900 start_hour,
901 end_hour,
902 days,
903 deny_reason,
904 })
905 }
906 "max_calls" => {
907 let tool_pattern = obj
911 .get("tool_pattern")
912 .and_then(|v| v.as_str())
913 .unwrap_or("*")
914 .to_ascii_lowercase();
915 let max = obj.get("max").and_then(|v| v.as_u64()).ok_or_else(|| {
916 PolicyValidationError {
917 policy_id: policy.id.clone(),
918 policy_name: policy.name.clone(),
919 reason: "max_calls missing 'max' integer".to_string(),
920 }
921 })?;
922 let deny_reason = format!(
923 "Tool call limit ({}) exceeded for pattern '{}' in policy '{}'",
924 max, tool_pattern, policy.name
925 );
926 Ok(CompiledContextCondition::MaxCalls {
927 tool_pattern: PatternMatcher::compile(&tool_pattern),
928 max,
929 deny_reason,
930 })
931 }
932 "agent_id" => {
933 const MAX_AGENT_ID_LIST: usize = 1000;
935
936 let allowed_raw = obj
939 .get("allowed")
940 .and_then(|v| v.as_array())
941 .map(|arr| arr.as_slice())
942 .unwrap_or_default();
943 if allowed_raw.len() > MAX_AGENT_ID_LIST {
944 return Err(PolicyValidationError {
945 policy_id: policy.id.clone(),
946 policy_name: policy.name.clone(),
947 reason: format!(
948 "agent_id allowed list count {} exceeds max {}",
949 allowed_raw.len(),
950 MAX_AGENT_ID_LIST
951 ),
952 });
953 }
954 let allowed: Vec<String> = allowed_raw
958 .iter()
959 .filter_map(|v| v.as_str().map(normalize_full))
960 .collect();
961
962 let blocked_raw = obj
963 .get("blocked")
964 .and_then(|v| v.as_array())
965 .map(|arr| arr.as_slice())
966 .unwrap_or_default();
967 if blocked_raw.len() > MAX_AGENT_ID_LIST {
968 return Err(PolicyValidationError {
969 policy_id: policy.id.clone(),
970 policy_name: policy.name.clone(),
971 reason: format!(
972 "agent_id blocked list count {} exceeds max {}",
973 blocked_raw.len(),
974 MAX_AGENT_ID_LIST
975 ),
976 });
977 }
978 let blocked: Vec<String> = blocked_raw
980 .iter()
981 .filter_map(|v| v.as_str().map(normalize_full))
982 .collect();
983
984 let deny_reason =
985 format!("Agent identity not authorized by policy '{}'", policy.name);
986 Ok(CompiledContextCondition::AgentId {
987 allowed,
988 blocked,
989 deny_reason,
990 })
991 }
992 "require_previous_action" => {
993 let required_tool = normalize_full(
997 obj.get("required_tool")
998 .and_then(|v| v.as_str())
999 .ok_or_else(|| PolicyValidationError {
1000 policy_id: policy.id.clone(),
1001 policy_name: policy.name.clone(),
1002 reason: "require_previous_action missing 'required_tool' string"
1003 .to_string(),
1004 })?,
1005 );
1006 let deny_reason = format!(
1007 "Required previous action '{}' not found in session history (policy '{}')",
1008 required_tool, policy.name
1009 );
1010 Ok(CompiledContextCondition::RequirePreviousAction {
1011 required_tool,
1012 deny_reason,
1013 })
1014 }
1015 "forbidden_previous_action" => {
1016 let forbidden_tool = normalize_full(
1020 obj.get("forbidden_tool")
1021 .and_then(|v| v.as_str())
1022 .ok_or_else(|| PolicyValidationError {
1023 policy_id: policy.id.clone(),
1024 policy_name: policy.name.clone(),
1025 reason: "forbidden_previous_action missing 'forbidden_tool' string"
1026 .to_string(),
1027 })?,
1028 );
1029 let deny_reason = format!(
1030 "Forbidden previous action '{}' detected in session history (policy '{}')",
1031 forbidden_tool, policy.name
1032 );
1033 Ok(CompiledContextCondition::ForbiddenPreviousAction {
1034 forbidden_tool,
1035 deny_reason,
1036 })
1037 }
1038 "max_calls_in_window" => {
1039 let tool_pattern = obj
1043 .get("tool_pattern")
1044 .and_then(|v| v.as_str())
1045 .unwrap_or("*")
1046 .to_ascii_lowercase();
1047 let max = obj.get("max").and_then(|v| v.as_u64()).ok_or_else(|| {
1048 PolicyValidationError {
1049 policy_id: policy.id.clone(),
1050 policy_name: policy.name.clone(),
1051 reason: "max_calls_in_window missing 'max' integer".to_string(),
1052 }
1053 })?;
1054 let window_raw = obj.get("window").and_then(|v| v.as_u64()).unwrap_or(0);
1058 let window = usize::try_from(window_raw).map_err(|_| PolicyValidationError {
1059 policy_id: policy.id.clone(),
1060 policy_name: policy.name.clone(),
1061 reason: format!(
1062 "max_calls_in_window 'window' value {} exceeds platform maximum ({})",
1063 window_raw,
1064 usize::MAX
1065 ),
1066 })?;
1067 let deny_reason = format!(
1068 "Tool '{}' called more than {} times in last {} actions (policy '{}')",
1069 tool_pattern,
1070 max,
1071 if window == 0 {
1072 "all".to_string()
1073 } else {
1074 window.to_string()
1075 },
1076 policy.name
1077 );
1078 Ok(CompiledContextCondition::MaxCallsInWindow {
1079 tool_pattern: PatternMatcher::compile(&tool_pattern),
1080 max,
1081 window,
1082 deny_reason,
1083 })
1084 }
1085 "max_chain_depth" => {
1086 let raw_depth = obj
1090 .get("max_depth")
1091 .and_then(|v| v.as_u64())
1092 .ok_or_else(|| PolicyValidationError {
1093 policy_id: policy.id.clone(),
1094 policy_name: policy.name.clone(),
1095 reason: "max_chain_depth missing 'max_depth' integer".to_string(),
1096 })?;
1097 let max_depth = usize::try_from(raw_depth).map_err(|_| PolicyValidationError {
1098 policy_id: policy.id.clone(),
1099 policy_name: policy.name.clone(),
1100 reason: format!(
1101 "max_chain_depth value {} exceeds platform maximum",
1102 raw_depth
1103 ),
1104 })?;
1105 let deny_reason = format!(
1106 "Call chain depth exceeds maximum of {} (policy '{}')",
1107 max_depth, policy.name
1108 );
1109 Ok(CompiledContextCondition::MaxChainDepth {
1110 max_depth,
1111 deny_reason,
1112 })
1113 }
1114 "agent_identity" => {
1115 let required_issuer = obj
1121 .get("issuer")
1122 .and_then(|v| v.as_str())
1123 .map(normalize_full);
1124 let required_subject = obj
1125 .get("subject")
1126 .and_then(|v| v.as_str())
1127 .map(normalize_full);
1128 let required_audience = obj
1129 .get("audience")
1130 .and_then(|v| v.as_str())
1131 .map(normalize_full);
1132
1133 const MAX_REQUIRED_CLAIMS: usize = 64;
1135 const MAX_CLAIM_KEY_LEN: usize = 256;
1136 const MAX_CLAIM_VALUE_LEN: usize = 512;
1137
1138 let required_claims = if let Some(m) = obj.get("claims").and_then(|v| v.as_object())
1142 {
1143 if m.len() > MAX_REQUIRED_CLAIMS {
1144 return Err(PolicyValidationError {
1145 policy_id: policy.id.clone(),
1146 policy_name: policy.name.clone(),
1147 reason: format!(
1148 "agent_identity claims count {} exceeds max {}",
1149 m.len(),
1150 MAX_REQUIRED_CLAIMS
1151 ),
1152 });
1153 }
1154 let mut map = std::collections::HashMap::new();
1155 for (k, v) in m {
1156 if k.len() > MAX_CLAIM_KEY_LEN {
1157 return Err(PolicyValidationError {
1158 policy_id: policy.id.clone(),
1159 policy_name: policy.name.clone(),
1160 reason: format!(
1161 "agent_identity claim key length {} exceeds max {}",
1162 k.len(),
1163 MAX_CLAIM_KEY_LEN
1164 ),
1165 });
1166 }
1167 if let Some(s) = v.as_str() {
1168 if s.len() > MAX_CLAIM_VALUE_LEN {
1169 return Err(PolicyValidationError {
1170 policy_id: policy.id.clone(),
1171 policy_name: policy.name.clone(),
1172 reason: format!(
1173 "agent_identity claim value for key '{}' length {} exceeds max {}",
1174 k,
1175 s.len(),
1176 MAX_CLAIM_VALUE_LEN
1177 ),
1178 });
1179 }
1180 map.insert(k.clone(), normalize_full(s));
1181 }
1182 }
1183 map
1184 } else {
1185 std::collections::HashMap::new()
1186 };
1187
1188 const MAX_ISSUER_LIST: usize = 256;
1190 const MAX_SUBJECT_LIST: usize = 256;
1191
1192 let blocked_issuers_raw = obj
1196 .get("blocked_issuers")
1197 .and_then(|v| v.as_array())
1198 .map(|arr| arr.as_slice())
1199 .unwrap_or_default();
1200 if blocked_issuers_raw.len() > MAX_ISSUER_LIST {
1201 return Err(PolicyValidationError {
1202 policy_id: policy.id.clone(),
1203 policy_name: policy.name.clone(),
1204 reason: format!(
1205 "agent_identity blocked_issuers count {} exceeds max {}",
1206 blocked_issuers_raw.len(),
1207 MAX_ISSUER_LIST
1208 ),
1209 });
1210 }
1211 let blocked_issuers: Vec<String> = blocked_issuers_raw
1212 .iter()
1213 .filter_map(|v| v.as_str().map(normalize_full))
1214 .collect();
1215
1216 let blocked_subjects_raw = obj
1217 .get("blocked_subjects")
1218 .and_then(|v| v.as_array())
1219 .map(|arr| arr.as_slice())
1220 .unwrap_or_default();
1221 if blocked_subjects_raw.len() > MAX_SUBJECT_LIST {
1222 return Err(PolicyValidationError {
1223 policy_id: policy.id.clone(),
1224 policy_name: policy.name.clone(),
1225 reason: format!(
1226 "agent_identity blocked_subjects count {} exceeds max {}",
1227 blocked_subjects_raw.len(),
1228 MAX_SUBJECT_LIST
1229 ),
1230 });
1231 }
1232 let blocked_subjects: Vec<String> = blocked_subjects_raw
1233 .iter()
1234 .filter_map(|v| v.as_str().map(normalize_full))
1235 .collect();
1236
1237 let require_attestation = obj
1239 .get("require_attestation")
1240 .and_then(|v| v.as_bool())
1241 .unwrap_or(true); let deny_reason = format!(
1244 "Agent identity attestation failed for policy '{}'",
1245 policy.name
1246 );
1247
1248 Ok(CompiledContextCondition::AgentIdentityMatch {
1249 required_issuer,
1250 required_subject,
1251 required_audience,
1252 required_claims,
1253 blocked_issuers,
1254 blocked_subjects,
1255 require_attestation,
1256 deny_reason,
1257 })
1258 }
1259
1260 "async_task_policy" => {
1264 tracing::warn!(
1267 policy_id = %policy.id,
1268 "Policy condition 'async_task_policy' is not enforced at the engine level; \
1269 it requires the MCP proxy layer for enforcement"
1270 );
1271 let max_concurrent_raw = obj
1274 .get("max_concurrent")
1275 .and_then(|v| v.as_u64())
1276 .unwrap_or(0); let max_concurrent = if max_concurrent_raw == 0 {
1278 0
1279 } else {
1280 usize::try_from(max_concurrent_raw).map_err(|_| PolicyValidationError {
1281 policy_id: policy.id.clone(),
1282 policy_name: policy.name.clone(),
1283 reason: format!(
1284 "async_task_policy 'max_concurrent' value {} exceeds platform maximum ({})",
1285 max_concurrent_raw,
1286 usize::MAX
1287 ),
1288 })?
1289 };
1290
1291 let max_duration_secs = obj
1292 .get("max_duration_secs")
1293 .and_then(|v| v.as_u64())
1294 .unwrap_or(0); let require_self_cancel = obj
1297 .get("require_self_cancel")
1298 .and_then(|v| v.as_bool())
1299 .unwrap_or(true); let deny_reason =
1302 format!("Async task policy violated for policy '{}'", policy.name);
1303
1304 Ok(CompiledContextCondition::AsyncTaskPolicy {
1305 max_concurrent,
1306 max_duration_secs,
1307 require_self_cancel,
1308 deny_reason,
1309 })
1310 }
1311
1312 "resource_indicator" => {
1313 const MAX_RESOURCE_PATTERNS: usize = 256;
1316
1317 let allowed_resources_raw = obj
1318 .get("allowed_resources")
1319 .and_then(|v| v.as_array())
1320 .map(|arr| arr.as_slice())
1321 .unwrap_or_default();
1322 if allowed_resources_raw.len() > MAX_RESOURCE_PATTERNS {
1323 return Err(PolicyValidationError {
1324 policy_id: policy.id.clone(),
1325 policy_name: policy.name.clone(),
1326 reason: format!(
1327 "resource_indicator allowed_resources count {} exceeds max {}",
1328 allowed_resources_raw.len(),
1329 MAX_RESOURCE_PATTERNS
1330 ),
1331 });
1332 }
1333 let allowed_resources: Vec<PatternMatcher> = allowed_resources_raw
1334 .iter()
1335 .filter_map(|v| v.as_str())
1336 .map(PatternMatcher::compile)
1337 .collect();
1338
1339 let require_resource = obj
1340 .get("require_resource")
1341 .and_then(|v| v.as_bool())
1342 .unwrap_or(false);
1343
1344 let deny_reason = format!(
1345 "Resource indicator validation failed for policy '{}'",
1346 policy.name
1347 );
1348
1349 Ok(CompiledContextCondition::ResourceIndicator {
1350 allowed_resources,
1351 require_resource,
1352 deny_reason,
1353 })
1354 }
1355
1356 "capability_required" => {
1357 const MAX_CAPABILITY_LIST: usize = 256;
1363 const MAX_CAPABILITY_NAME_LEN: usize = 256;
1364
1365 let required_capabilities: Vec<String> = obj
1369 .get("required_capabilities")
1370 .and_then(|v| v.as_array())
1371 .map(|arr| {
1372 arr.iter()
1373 .filter_map(|v| v.as_str().map(normalize_full))
1374 .collect()
1375 })
1376 .unwrap_or_default();
1377
1378 if required_capabilities.len() > MAX_CAPABILITY_LIST {
1379 return Err(PolicyValidationError {
1380 policy_id: policy.id.clone(),
1381 policy_name: policy.name.clone(),
1382 reason: format!(
1383 "capability_required has {} required_capabilities (max {MAX_CAPABILITY_LIST})",
1384 required_capabilities.len()
1385 ),
1386 });
1387 }
1388 for (i, cap) in required_capabilities.iter().enumerate() {
1389 if cap.len() > MAX_CAPABILITY_NAME_LEN {
1390 return Err(PolicyValidationError {
1391 policy_id: policy.id.clone(),
1392 policy_name: policy.name.clone(),
1393 reason: format!(
1394 "capability_required required_capabilities[{i}] length {} exceeds max {MAX_CAPABILITY_NAME_LEN}",
1395 cap.len()
1396 ),
1397 });
1398 }
1399 }
1400
1401 let blocked_capabilities: Vec<String> = obj
1402 .get("blocked_capabilities")
1403 .and_then(|v| v.as_array())
1404 .map(|arr| {
1405 arr.iter()
1406 .filter_map(|v| v.as_str().map(normalize_full))
1407 .collect()
1408 })
1409 .unwrap_or_default();
1410
1411 if blocked_capabilities.len() > MAX_CAPABILITY_LIST {
1412 return Err(PolicyValidationError {
1413 policy_id: policy.id.clone(),
1414 policy_name: policy.name.clone(),
1415 reason: format!(
1416 "capability_required has {} blocked_capabilities (max {MAX_CAPABILITY_LIST})",
1417 blocked_capabilities.len()
1418 ),
1419 });
1420 }
1421 for (i, cap) in blocked_capabilities.iter().enumerate() {
1422 if cap.len() > MAX_CAPABILITY_NAME_LEN {
1423 return Err(PolicyValidationError {
1424 policy_id: policy.id.clone(),
1425 policy_name: policy.name.clone(),
1426 reason: format!(
1427 "capability_required blocked_capabilities[{i}] length {} exceeds max {MAX_CAPABILITY_NAME_LEN}",
1428 cap.len()
1429 ),
1430 });
1431 }
1432 }
1433
1434 let deny_reason = format!(
1435 "Capability requirement not met for policy '{}'",
1436 policy.name
1437 );
1438
1439 Ok(CompiledContextCondition::CapabilityRequired {
1440 required_capabilities,
1441 blocked_capabilities,
1442 deny_reason,
1443 })
1444 }
1445
1446 "step_up_auth" => {
1447 let required_level_u64 = obj
1449 .get("required_level")
1450 .and_then(|v| v.as_u64())
1451 .ok_or_else(|| PolicyValidationError {
1452 policy_id: policy.id.clone(),
1453 policy_name: policy.name.clone(),
1454 reason: "step_up_auth missing 'required_level' integer".to_string(),
1455 })?;
1456
1457 if required_level_u64 > 4 {
1459 return Err(PolicyValidationError {
1460 policy_id: policy.id.clone(),
1461 policy_name: policy.name.clone(),
1462 reason: format!(
1463 "step_up_auth required_level must be 0-4, got {}",
1464 required_level_u64
1465 ),
1466 });
1467 }
1468
1469 let required_level = required_level_u64 as u8;
1470
1471 let deny_reason = format!(
1472 "Step-up authentication required (level {}) for policy '{}'",
1473 required_level, policy.name
1474 );
1475
1476 Ok(CompiledContextCondition::StepUpAuth {
1477 required_level,
1478 deny_reason,
1479 })
1480 }
1481
1482 "circuit_breaker" => {
1486 tracing::warn!(
1489 policy_id = %policy.id,
1490 "Policy condition 'circuit_breaker' is not enforced at the engine level; \
1491 it requires the MCP proxy layer for enforcement"
1492 );
1493 let tool_pattern = obj
1494 .get("tool_pattern")
1495 .and_then(|v| v.as_str())
1496 .unwrap_or("*")
1497 .to_ascii_lowercase();
1498
1499 let deny_reason = format!("Circuit breaker open (policy '{}')", policy.name);
1503
1504 Ok(CompiledContextCondition::CircuitBreaker {
1505 tool_pattern: PatternMatcher::compile(&tool_pattern),
1506 deny_reason,
1507 })
1508 }
1509
1510 "deputy_validation" => {
1511 let require_principal = obj
1513 .get("require_principal")
1514 .and_then(|v| v.as_bool())
1515 .unwrap_or(true);
1516
1517 let max_delegation_depth_u64 = obj
1518 .get("max_delegation_depth")
1519 .and_then(|v| v.as_u64())
1520 .unwrap_or(3);
1521
1522 if max_delegation_depth_u64 > 255 {
1524 return Err(PolicyValidationError {
1525 policy_id: policy.id.clone(),
1526 policy_name: policy.name.clone(),
1527 reason: format!(
1528 "deputy_validation max_delegation_depth must be 0-255, got {}",
1529 max_delegation_depth_u64
1530 ),
1531 });
1532 }
1533
1534 let max_delegation_depth = max_delegation_depth_u64 as u8;
1535
1536 let deny_reason = format!("Deputy validation failed for policy '{}'", policy.name);
1537
1538 Ok(CompiledContextCondition::DeputyValidation {
1539 require_principal,
1540 max_delegation_depth,
1541 deny_reason,
1542 })
1543 }
1544
1545 "shadow_agent_check" => {
1546 tracing::warn!(
1549 policy_id = %policy.id,
1550 "Policy condition 'shadow_agent_check' is not enforced at the engine level; \
1551 it requires the MCP proxy layer for enforcement"
1552 );
1553 let require_known_fingerprint = obj
1554 .get("require_known_fingerprint")
1555 .and_then(|v| v.as_bool())
1556 .unwrap_or(false);
1557
1558 let min_trust_level_u64 = obj
1559 .get("min_trust_level")
1560 .and_then(|v| v.as_u64())
1561 .unwrap_or(1); if min_trust_level_u64 > 4 {
1565 return Err(PolicyValidationError {
1566 policy_id: policy.id.clone(),
1567 policy_name: policy.name.clone(),
1568 reason: format!(
1569 "shadow_agent_check min_trust_level must be 0-4, got {}",
1570 min_trust_level_u64
1571 ),
1572 });
1573 }
1574
1575 let min_trust_level = min_trust_level_u64 as u8;
1576
1577 let deny_reason = format!("Shadow agent check failed for policy '{}'", policy.name);
1578
1579 Ok(CompiledContextCondition::ShadowAgentCheck {
1580 require_known_fingerprint,
1581 min_trust_level,
1582 deny_reason,
1583 })
1584 }
1585
1586 "schema_poisoning_check" => {
1587 tracing::warn!(
1590 policy_id = %policy.id,
1591 "Policy condition 'schema_poisoning_check' is not enforced at the engine level; \
1592 it requires the MCP proxy layer for enforcement"
1593 );
1594 let mutation_threshold = obj
1595 .get("mutation_threshold")
1596 .and_then(|v| v.as_f64())
1597 .map(|v| v as f32)
1598 .unwrap_or(0.1); if !mutation_threshold.is_finite() || !(0.0..=1.0).contains(&mutation_threshold) {
1602 return Err(PolicyValidationError {
1603 policy_id: policy.id.clone(),
1604 policy_name: policy.name.clone(),
1605 reason: format!(
1606 "schema_poisoning_check mutation_threshold must be in [0.0, 1.0], got {}",
1607 mutation_threshold
1608 ),
1609 });
1610 }
1611
1612 let deny_reason = format!("Schema poisoning detected for policy '{}'", policy.name);
1613
1614 Ok(CompiledContextCondition::SchemaPoisoningCheck {
1615 mutation_threshold,
1616 deny_reason,
1617 })
1618 }
1619
1620 "require_capability_token" => {
1621 const MAX_ISSUER_LIST_CAP: usize = 256;
1623
1624 let required_issuers_raw = obj
1626 .get("required_issuers")
1627 .and_then(|v| v.as_array())
1628 .map(|arr| arr.as_slice())
1629 .unwrap_or_default();
1630 if required_issuers_raw.len() > MAX_ISSUER_LIST_CAP {
1631 return Err(PolicyValidationError {
1632 policy_id: policy.id.clone(),
1633 policy_name: policy.name.clone(),
1634 reason: format!(
1635 "require_capability_token required_issuers count {} exceeds max {}",
1636 required_issuers_raw.len(),
1637 MAX_ISSUER_LIST_CAP
1638 ),
1639 });
1640 }
1641 let required_issuers: Vec<String> = required_issuers_raw
1644 .iter()
1645 .filter_map(|v| v.as_str().map(normalize_full))
1646 .collect();
1647
1648 let min_remaining_depth = obj
1650 .get("min_remaining_depth")
1651 .and_then(|v| v.as_u64())
1652 .unwrap_or(0);
1653 if min_remaining_depth > 16 {
1654 return Err(PolicyValidationError {
1655 policy_id: policy.id.clone(),
1656 policy_name: policy.name.clone(),
1657 reason: format!(
1658 "require_capability_token min_remaining_depth must be 0-16, got {}",
1659 min_remaining_depth
1660 ),
1661 });
1662 }
1663
1664 let deny_reason = format!("Capability token required for policy '{}'", policy.name);
1665
1666 Ok(CompiledContextCondition::RequireCapabilityToken {
1667 required_issuers,
1668 min_remaining_depth: min_remaining_depth as u8,
1669 deny_reason,
1670 })
1671 }
1672
1673 "min_verification_tier" => {
1674 let required_tier = if let Some(level_val) = obj.get("required_tier") {
1676 if let Some(level_u64) = level_val.as_u64() {
1677 if level_u64 > 4 {
1678 return Err(PolicyValidationError {
1679 policy_id: policy.id.clone(),
1680 policy_name: policy.name.clone(),
1681 reason: format!(
1682 "min_verification_tier required_tier must be 0-4, got {}",
1683 level_u64
1684 ),
1685 });
1686 }
1687 level_u64 as u8
1688 } else if let Some(name) = level_val.as_str() {
1689 vellaveto_types::VerificationTier::from_name(name)
1690 .map(|t| t.level())
1691 .ok_or_else(|| PolicyValidationError {
1692 policy_id: policy.id.clone(),
1693 policy_name: policy.name.clone(),
1694 reason: format!(
1695 "min_verification_tier unknown tier name '{}'",
1696 name
1697 ),
1698 })?
1699 } else {
1700 return Err(PolicyValidationError {
1701 policy_id: policy.id.clone(),
1702 policy_name: policy.name.clone(),
1703 reason: "min_verification_tier required_tier must be an integer (0-4) or tier name string".to_string(),
1704 });
1705 }
1706 } else {
1707 return Err(PolicyValidationError {
1708 policy_id: policy.id.clone(),
1709 policy_name: policy.name.clone(),
1710 reason: "min_verification_tier missing 'required_tier' field".to_string(),
1711 });
1712 };
1713
1714 let deny_reason = format!(
1715 "Verification tier below minimum (required level {}) for policy '{}'",
1716 required_tier, policy.name
1717 );
1718
1719 Ok(CompiledContextCondition::MinVerificationTier {
1720 required_tier,
1721 deny_reason,
1722 })
1723 }
1724
1725 "session_state_required" => {
1726 const MAX_ALLOWED_STATES: usize = 1000;
1729 const MAX_STATE_NAME_LEN: usize = 256;
1730
1731 let raw_arr = obj
1733 .get("allowed_states")
1734 .and_then(|v| v.as_array())
1735 .cloned()
1736 .unwrap_or_default();
1737
1738 if raw_arr.len() > MAX_ALLOWED_STATES {
1739 return Err(PolicyValidationError {
1740 policy_id: policy.id.clone(),
1741 policy_name: policy.name.clone(),
1742 reason: format!(
1743 "session_state_required allowed_states has {} entries, max {}",
1744 raw_arr.len(),
1745 MAX_ALLOWED_STATES,
1746 ),
1747 });
1748 }
1749
1750 let mut allowed_states = Vec::with_capacity(raw_arr.len());
1751 for entry in &raw_arr {
1752 if let Some(s) = entry.as_str() {
1753 if s.len() > MAX_STATE_NAME_LEN {
1754 return Err(PolicyValidationError {
1755 policy_id: policy.id.clone(),
1756 policy_name: policy.name.clone(),
1757 reason: format!(
1758 "session_state_required allowed_states entry length {} exceeds max {}",
1759 s.len(),
1760 MAX_STATE_NAME_LEN,
1761 ),
1762 });
1763 }
1764 allowed_states.push(normalize_full(s));
1767 }
1768 }
1769
1770 if allowed_states.is_empty() {
1771 return Err(PolicyValidationError {
1772 policy_id: policy.id.clone(),
1773 policy_name: policy.name.clone(),
1774 reason:
1775 "session_state_required must have at least one allowed_states entry"
1776 .to_string(),
1777 });
1778 }
1779
1780 let deny_reason = format!(
1781 "Session state not in allowed states for policy '{}'",
1782 policy.name
1783 );
1784
1785 Ok(CompiledContextCondition::SessionStateRequired {
1786 allowed_states,
1787 deny_reason,
1788 })
1789 }
1790
1791 "required_action_sequence" => Self::compile_action_sequence(obj, policy, true),
1795
1796 "forbidden_action_sequence" => Self::compile_action_sequence(obj, policy, false),
1797
1798 "workflow_template" => Self::compile_workflow_template(obj, policy),
1799
1800 _ => Err(PolicyValidationError {
1801 policy_id: policy.id.clone(),
1802 policy_name: policy.name.clone(),
1803 reason: format!("Unknown context condition type '{}'", kind),
1804 }),
1805 }
1806 }
1807
1808 fn compile_action_sequence(
1812 obj: &serde_json::Map<String, serde_json::Value>,
1813 policy: &vellaveto_types::Policy,
1814 is_required: bool,
1815 ) -> Result<CompiledContextCondition, PolicyValidationError> {
1816 const MAX_SEQUENCE_STEPS: usize = 20;
1817 const MAX_TOOL_NAME_LEN: usize = 256;
1819
1820 let kind = if is_required {
1821 "required_action_sequence"
1822 } else {
1823 "forbidden_action_sequence"
1824 };
1825
1826 let arr = obj
1827 .get("sequence")
1828 .and_then(|v| v.as_array())
1829 .ok_or_else(|| PolicyValidationError {
1830 policy_id: policy.id.clone(),
1831 policy_name: policy.name.clone(),
1832 reason: format!("{kind} requires a 'sequence' array"),
1833 })?;
1834
1835 if arr.is_empty() {
1836 return Err(PolicyValidationError {
1837 policy_id: policy.id.clone(),
1838 policy_name: policy.name.clone(),
1839 reason: format!("{kind} sequence must not be empty"),
1840 });
1841 }
1842
1843 if arr.len() > MAX_SEQUENCE_STEPS {
1844 return Err(PolicyValidationError {
1845 policy_id: policy.id.clone(),
1846 policy_name: policy.name.clone(),
1847 reason: format!(
1848 "{kind} sequence has {} steps (max {MAX_SEQUENCE_STEPS})",
1849 arr.len()
1850 ),
1851 });
1852 }
1853
1854 let mut sequence = Vec::with_capacity(arr.len());
1855 for (i, val) in arr.iter().enumerate() {
1856 let s = val.as_str().ok_or_else(|| PolicyValidationError {
1857 policy_id: policy.id.clone(),
1858 policy_name: policy.name.clone(),
1859 reason: format!("{kind} sequence[{i}] must be a string"),
1860 })?;
1861
1862 if s.is_empty() {
1863 return Err(PolicyValidationError {
1864 policy_id: policy.id.clone(),
1865 policy_name: policy.name.clone(),
1866 reason: format!("{kind} sequence[{i}] must not be empty"),
1867 });
1868 }
1869
1870 if s.chars()
1872 .any(|c| c.is_control() || vellaveto_types::is_unicode_format_char(c))
1873 {
1874 return Err(PolicyValidationError {
1875 policy_id: policy.id.clone(),
1876 policy_name: policy.name.clone(),
1877 reason: format!("{kind} sequence[{i}] contains control or format characters"),
1878 });
1879 }
1880
1881 if s.len() > MAX_TOOL_NAME_LEN {
1883 return Err(PolicyValidationError {
1884 policy_id: policy.id.clone(),
1885 policy_name: policy.name.clone(),
1886 reason: format!(
1887 "{kind} sequence[{i}] tool name is {} bytes, max is {MAX_TOOL_NAME_LEN}",
1888 s.len()
1889 ),
1890 });
1891 }
1892
1893 sequence.push(normalize_full(s));
1896 }
1897
1898 let ordered = obj.get("ordered").and_then(|v| v.as_bool()).unwrap_or(true);
1899
1900 if is_required {
1901 let deny_reason = format!(
1902 "Required action sequence not satisfied for policy '{}'",
1903 policy.name
1904 );
1905 Ok(CompiledContextCondition::RequiredActionSequence {
1906 sequence,
1907 ordered,
1908 deny_reason,
1909 })
1910 } else {
1911 let deny_reason = format!(
1912 "Forbidden action sequence detected for policy '{}'",
1913 policy.name
1914 );
1915 Ok(CompiledContextCondition::ForbiddenActionSequence {
1916 sequence,
1917 ordered,
1918 deny_reason,
1919 })
1920 }
1921 }
1922
1923 fn compile_workflow_template(
1928 obj: &serde_json::Map<String, serde_json::Value>,
1929 policy: &vellaveto_types::Policy,
1930 ) -> Result<CompiledContextCondition, PolicyValidationError> {
1931 use std::collections::{HashMap, HashSet, VecDeque};
1932
1933 const MAX_WORKFLOW_STEPS: usize = 50;
1934 const MAX_SUCCESSORS_PER_STEP: usize = 50;
1936 const MAX_TOOL_NAME_LEN: usize = 256;
1938
1939 let steps =
1940 obj.get("steps")
1941 .and_then(|v| v.as_array())
1942 .ok_or_else(|| PolicyValidationError {
1943 policy_id: policy.id.clone(),
1944 policy_name: policy.name.clone(),
1945 reason: "workflow_template requires a 'steps' array".to_string(),
1946 })?;
1947
1948 if steps.is_empty() {
1949 return Err(PolicyValidationError {
1950 policy_id: policy.id.clone(),
1951 policy_name: policy.name.clone(),
1952 reason: "workflow_template steps must not be empty".to_string(),
1953 });
1954 }
1955
1956 if steps.len() > MAX_WORKFLOW_STEPS {
1957 return Err(PolicyValidationError {
1958 policy_id: policy.id.clone(),
1959 policy_name: policy.name.clone(),
1960 reason: format!(
1961 "workflow_template has {} steps (max {MAX_WORKFLOW_STEPS})",
1962 steps.len()
1963 ),
1964 });
1965 }
1966
1967 let enforce = obj
1968 .get("enforce")
1969 .and_then(|v| v.as_str())
1970 .unwrap_or("strict");
1971
1972 let strict = match enforce {
1973 "strict" => true,
1974 "warn" => false,
1975 other => {
1976 return Err(PolicyValidationError {
1977 policy_id: policy.id.clone(),
1978 policy_name: policy.name.clone(),
1979 reason: format!(
1980 "workflow_template enforce must be 'strict' or 'warn', got '{other}'"
1981 ),
1982 });
1983 }
1984 };
1985
1986 let mut adjacency: HashMap<String, Vec<String>> = HashMap::new();
1987 let mut governed_tools: HashSet<String> = HashSet::new();
1988 let mut seen_tools: HashSet<String> = HashSet::new();
1989
1990 for (i, step) in steps.iter().enumerate() {
1991 let step_obj = step.as_object().ok_or_else(|| PolicyValidationError {
1992 policy_id: policy.id.clone(),
1993 policy_name: policy.name.clone(),
1994 reason: format!("workflow_template steps[{i}] must be an object"),
1995 })?;
1996
1997 let tool = step_obj
1998 .get("tool")
1999 .and_then(|v| v.as_str())
2000 .ok_or_else(|| PolicyValidationError {
2001 policy_id: policy.id.clone(),
2002 policy_name: policy.name.clone(),
2003 reason: format!("workflow_template steps[{i}] requires a 'tool' string"),
2004 })?;
2005
2006 if tool.is_empty() {
2007 return Err(PolicyValidationError {
2008 policy_id: policy.id.clone(),
2009 policy_name: policy.name.clone(),
2010 reason: format!("workflow_template steps[{i}].tool must not be empty"),
2011 });
2012 }
2013
2014 if tool
2016 .chars()
2017 .any(|c| c.is_control() || vellaveto_types::is_unicode_format_char(c))
2018 {
2019 return Err(PolicyValidationError {
2020 policy_id: policy.id.clone(),
2021 policy_name: policy.name.clone(),
2022 reason: format!(
2023 "workflow_template steps[{i}].tool contains control or format characters"
2024 ),
2025 });
2026 }
2027
2028 if tool.len() > MAX_TOOL_NAME_LEN {
2030 return Err(PolicyValidationError {
2031 policy_id: policy.id.clone(),
2032 policy_name: policy.name.clone(),
2033 reason: format!(
2034 "workflow_template steps[{i}].tool is {} bytes, max is {MAX_TOOL_NAME_LEN}",
2035 tool.len()
2036 ),
2037 });
2038 }
2039
2040 let tool_lower = normalize_full(tool);
2042
2043 if !seen_tools.insert(tool_lower.clone()) {
2044 return Err(PolicyValidationError {
2045 policy_id: policy.id.clone(),
2046 policy_name: policy.name.clone(),
2047 reason: format!("workflow_template has duplicate step tool '{tool}'"),
2048 });
2049 }
2050
2051 let then_arr = step_obj
2052 .get("then")
2053 .and_then(|v| v.as_array())
2054 .ok_or_else(|| PolicyValidationError {
2055 policy_id: policy.id.clone(),
2056 policy_name: policy.name.clone(),
2057 reason: format!("workflow_template steps[{i}] requires a 'then' array"),
2058 })?;
2059
2060 if then_arr.len() > MAX_SUCCESSORS_PER_STEP {
2063 return Err(PolicyValidationError {
2064 policy_id: policy.id.clone(),
2065 policy_name: policy.name.clone(),
2066 reason: format!(
2067 "workflow_template steps[{i}].then has {} entries, max is {MAX_SUCCESSORS_PER_STEP}",
2068 then_arr.len()
2069 ),
2070 });
2071 }
2072
2073 let mut successors = Vec::with_capacity(then_arr.len());
2074 let mut seen_successors: HashSet<String> = HashSet::new();
2076 for (j, v) in then_arr.iter().enumerate() {
2077 let s = v.as_str().ok_or_else(|| PolicyValidationError {
2078 policy_id: policy.id.clone(),
2079 policy_name: policy.name.clone(),
2080 reason: format!("workflow_template steps[{i}].then[{j}] must be a string"),
2081 })?;
2082
2083 if s.is_empty() {
2084 return Err(PolicyValidationError {
2085 policy_id: policy.id.clone(),
2086 policy_name: policy.name.clone(),
2087 reason: format!("workflow_template steps[{i}].then[{j}] must not be empty"),
2088 });
2089 }
2090
2091 if s.chars()
2093 .any(|c| c.is_control() || vellaveto_types::is_unicode_format_char(c))
2094 {
2095 return Err(PolicyValidationError {
2096 policy_id: policy.id.clone(),
2097 policy_name: policy.name.clone(),
2098 reason: format!(
2099 "workflow_template steps[{i}].then[{j}] contains control or format characters"
2100 ),
2101 });
2102 }
2103
2104 if s.len() > MAX_TOOL_NAME_LEN {
2106 return Err(PolicyValidationError {
2107 policy_id: policy.id.clone(),
2108 policy_name: policy.name.clone(),
2109 reason: format!(
2110 "workflow_template steps[{i}].then[{j}] is {} bytes, max is {MAX_TOOL_NAME_LEN}",
2111 s.len()
2112 ),
2113 });
2114 }
2115
2116 let lowered = normalize_full(s);
2118 if seen_successors.insert(lowered.clone()) {
2121 successors.push(lowered);
2122 }
2123 }
2124
2125 governed_tools.insert(tool_lower.clone());
2126 for succ in &successors {
2127 governed_tools.insert(succ.clone());
2128 }
2129
2130 adjacency.insert(tool_lower, successors);
2131 }
2132
2133 let mut has_predecessor: HashSet<&str> = HashSet::new();
2135 for successors in adjacency.values() {
2136 for succ in successors {
2137 has_predecessor.insert(succ.as_str());
2138 }
2139 }
2140 let mut entry_points: Vec<String> = governed_tools
2143 .iter()
2144 .filter(|t| !has_predecessor.contains(t.as_str()))
2145 .cloned()
2146 .collect();
2147 entry_points.sort();
2148
2149 if entry_points.is_empty() {
2150 return Err(PolicyValidationError {
2151 policy_id: policy.id.clone(),
2152 policy_name: policy.name.clone(),
2153 reason: "workflow_template has no entry points (implies cycle)".to_string(),
2154 });
2155 }
2156
2157 let mut in_degree: HashMap<&str, usize> = HashMap::new();
2159 for tool in &governed_tools {
2160 in_degree.insert(tool.as_str(), 0);
2161 }
2162 for successors in adjacency.values() {
2163 for succ in successors {
2164 if let Some(deg) = in_degree.get_mut(succ.as_str()) {
2165 *deg = deg.saturating_add(1);
2166 }
2167 }
2168 }
2169
2170 let mut queue: VecDeque<&str> = VecDeque::new();
2171 for (tool, deg) in &in_degree {
2172 if *deg == 0 {
2173 queue.push_back(tool);
2174 }
2175 }
2176
2177 let mut visited_count: usize = 0;
2178 while let Some(node) = queue.pop_front() {
2179 visited_count += 1;
2180 if let Some(successors) = adjacency.get(node) {
2181 for succ in successors {
2182 if let Some(deg) = in_degree.get_mut(succ.as_str()) {
2183 *deg = deg.saturating_sub(1);
2184 if *deg == 0 {
2185 queue.push_back(succ.as_str());
2186 }
2187 }
2188 }
2189 }
2190 }
2191
2192 if visited_count < governed_tools.len() {
2193 return Err(PolicyValidationError {
2194 policy_id: policy.id.clone(),
2195 policy_name: policy.name.clone(),
2196 reason: "workflow_template contains a cycle".to_string(),
2197 });
2198 }
2199
2200 let deny_reason = format!("Workflow template violation for policy '{}'", policy.name);
2201
2202 Ok(CompiledContextCondition::WorkflowTemplate {
2203 adjacency,
2204 governed_tools,
2205 entry_points,
2206 strict,
2207 deny_reason,
2208 })
2209 }
2210}