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 let all_wildcard_params = constraints.iter().all(|c| c.param() == "*");
414 if on_no_match_continue
415 && !constraints.is_empty()
416 && constraints.iter().all(|c| c.on_missing() == "skip")
417 && forbidden_parameters.is_empty()
418 && required_parameters.is_empty()
419 && !all_wildcard_params
420 {
421 tracing::warn!(
422 policy_id = %policy.id,
423 policy_name = %policy.name,
424 constraint_count = constraints.len(),
425 "Conditional policy has on_no_match=\"continue\" with ALL constraints \
426 using on_missing=\"skip\" — an attacker can bypass this policy entirely \
427 by omitting the required parameters. Set at least one constraint to \
428 on_missing=\"deny\" or remove on_no_match=\"continue\"."
429 );
430 }
431
432 Ok(CompiledConditions {
433 require_approval,
434 forbidden_parameters,
435 required_parameters,
436 constraints,
437 on_no_match_continue,
438 context_conditions,
439 })
440 }
441
442 fn compile_constraint(
447 policy: &Policy,
448 constraint: &serde_json::Value,
449 ) -> Result<CompiledConstraint, PolicyValidationError> {
450 const MAX_CONSTRAINT_ELEMENTS: usize = 1000;
452 let obj = constraint
453 .as_object()
454 .ok_or_else(|| PolicyValidationError {
455 policy_id: policy.id.clone(),
456 policy_name: policy.name.clone(),
457 reason: "Each parameter constraint must be a JSON object".to_string(),
458 })?;
459
460 let param = obj
461 .get("param")
462 .and_then(|v| v.as_str())
463 .ok_or_else(|| PolicyValidationError {
464 policy_id: policy.id.clone(),
465 policy_name: policy.name.clone(),
466 reason: "Constraint missing required 'param' string field".to_string(),
467 })?
468 .to_string();
469
470 let op = obj
471 .get("op")
472 .and_then(|v| v.as_str())
473 .ok_or_else(|| PolicyValidationError {
474 policy_id: policy.id.clone(),
475 policy_name: policy.name.clone(),
476 reason: "Constraint missing required 'op' string field".to_string(),
477 })?;
478
479 let on_match = obj
480 .get("on_match")
481 .and_then(|v| v.as_str())
482 .unwrap_or("deny")
483 .to_string();
484 match on_match.as_str() {
487 "deny" | "allow" | "require_approval" => {}
488 other => {
489 return Err(PolicyValidationError {
490 policy_id: policy.id.clone(),
491 policy_name: policy.name.clone(),
492 reason: format!(
493 "Constraint 'on_match' value '{}' is invalid; expected 'deny', 'allow', or 'require_approval'",
494 other
495 ),
496 });
497 }
498 }
499 let on_missing = obj
500 .get("on_missing")
501 .and_then(|v| v.as_str())
502 .unwrap_or("deny")
503 .to_string();
504 match on_missing.as_str() {
505 "deny" | "skip" => {}
506 other => {
507 return Err(PolicyValidationError {
508 policy_id: policy.id.clone(),
509 policy_name: policy.name.clone(),
510 reason: format!(
511 "Constraint 'on_missing' value '{}' is invalid; expected 'deny' or 'skip'",
512 other
513 ),
514 });
515 }
516 }
517
518 match op {
519 "glob" => {
520 let pattern_str = obj
521 .get("pattern")
522 .and_then(|v| v.as_str())
523 .ok_or_else(|| PolicyValidationError {
524 policy_id: policy.id.clone(),
525 policy_name: policy.name.clone(),
526 reason: "glob constraint missing 'pattern' string".to_string(),
527 })?
528 .to_string();
529
530 let matcher = Glob::new(&pattern_str)
531 .map_err(|e| PolicyValidationError {
532 policy_id: policy.id.clone(),
533 policy_name: policy.name.clone(),
534 reason: format!("Invalid glob pattern '{}': {}", pattern_str, e),
535 })?
536 .compile_matcher();
537
538 Ok(CompiledConstraint::Glob {
539 param,
540 matcher,
541 pattern_str,
542 on_match,
543 on_missing,
544 })
545 }
546 "not_glob" => {
547 let patterns = obj
548 .get("patterns")
549 .and_then(|v| v.as_array())
550 .ok_or_else(|| PolicyValidationError {
551 policy_id: policy.id.clone(),
552 policy_name: policy.name.clone(),
553 reason: "not_glob constraint missing 'patterns' array".to_string(),
554 })?;
555
556 if patterns.len() > MAX_CONSTRAINT_ELEMENTS {
558 return Err(PolicyValidationError {
559 policy_id: policy.id.clone(),
560 policy_name: policy.name.clone(),
561 reason: format!(
562 "not_glob patterns count {} exceeds maximum {}",
563 patterns.len(),
564 MAX_CONSTRAINT_ELEMENTS
565 ),
566 });
567 }
568
569 let mut matchers = Vec::new();
570 for pat_val in patterns {
571 let pat_str = pat_val.as_str().ok_or_else(|| PolicyValidationError {
572 policy_id: policy.id.clone(),
573 policy_name: policy.name.clone(),
574 reason: "not_glob patterns must be strings".to_string(),
575 })?;
576 let matcher = Glob::new(pat_str)
577 .map_err(|e| PolicyValidationError {
578 policy_id: policy.id.clone(),
579 policy_name: policy.name.clone(),
580 reason: format!("Invalid glob pattern '{}': {}", pat_str, e),
581 })?
582 .compile_matcher();
583 matchers.push((pat_str.to_string(), matcher));
584 }
585
586 Ok(CompiledConstraint::NotGlob {
587 param,
588 matchers,
589 on_match,
590 on_missing,
591 })
592 }
593 "regex" => {
594 let pattern_str = obj
595 .get("pattern")
596 .and_then(|v| v.as_str())
597 .ok_or_else(|| PolicyValidationError {
598 policy_id: policy.id.clone(),
599 policy_name: policy.name.clone(),
600 reason: "regex constraint missing 'pattern' string".to_string(),
601 })?
602 .to_string();
603
604 Self::validate_regex_safety(&pattern_str).map_err(|reason| {
606 PolicyValidationError {
607 policy_id: policy.id.clone(),
608 policy_name: policy.name.clone(),
609 reason,
610 }
611 })?;
612
613 let regex = regex::RegexBuilder::new(&pattern_str)
616 .dfa_size_limit(256 * 1024)
617 .size_limit(256 * 1024)
618 .build()
619 .map_err(|e| PolicyValidationError {
620 policy_id: policy.id.clone(),
621 policy_name: policy.name.clone(),
622 reason: format!("Invalid regex pattern '{}': {}", pattern_str, e),
623 })?;
624
625 Ok(CompiledConstraint::Regex {
626 param,
627 regex,
628 pattern_str,
629 on_match,
630 on_missing,
631 })
632 }
633 "domain_match" => {
634 let pattern = obj
635 .get("pattern")
636 .and_then(|v| v.as_str())
637 .ok_or_else(|| PolicyValidationError {
638 policy_id: policy.id.clone(),
639 policy_name: policy.name.clone(),
640 reason: "domain_match constraint missing 'pattern' string".to_string(),
641 })?
642 .to_string();
643
644 if let Err(reason) = crate::domain::validate_domain_pattern(&pattern) {
646 return Err(PolicyValidationError {
647 policy_id: policy.id.clone(),
648 policy_name: policy.name.clone(),
649 reason: format!("domain_match pattern invalid: {}", reason),
650 });
651 }
652
653 Ok(CompiledConstraint::DomainMatch {
654 param,
655 pattern,
656 on_match,
657 on_missing,
658 })
659 }
660 "domain_not_in" => {
661 let patterns_arr =
662 obj.get("patterns")
663 .and_then(|v| v.as_array())
664 .ok_or_else(|| PolicyValidationError {
665 policy_id: policy.id.clone(),
666 policy_name: policy.name.clone(),
667 reason: "domain_not_in constraint missing 'patterns' array".to_string(),
668 })?;
669
670 if patterns_arr.len() > MAX_CONSTRAINT_ELEMENTS {
672 return Err(PolicyValidationError {
673 policy_id: policy.id.clone(),
674 policy_name: policy.name.clone(),
675 reason: format!(
676 "domain_not_in patterns count {} exceeds maximum {}",
677 patterns_arr.len(),
678 MAX_CONSTRAINT_ELEMENTS
679 ),
680 });
681 }
682
683 let mut patterns = Vec::new();
684 for pat_val in patterns_arr {
685 let pat_str = pat_val.as_str().ok_or_else(|| PolicyValidationError {
686 policy_id: policy.id.clone(),
687 policy_name: policy.name.clone(),
688 reason: "domain_not_in patterns must be strings".to_string(),
689 })?;
690 if let Err(reason) = crate::domain::validate_domain_pattern(pat_str) {
692 return Err(PolicyValidationError {
693 policy_id: policy.id.clone(),
694 policy_name: policy.name.clone(),
695 reason: format!("domain_not_in pattern invalid: {}", reason),
696 });
697 }
698 patterns.push(pat_str.to_string());
699 }
700
701 Ok(CompiledConstraint::DomainNotIn {
702 param,
703 patterns,
704 on_match,
705 on_missing,
706 })
707 }
708 "eq" => {
709 let value = obj
710 .get("value")
711 .ok_or_else(|| PolicyValidationError {
712 policy_id: policy.id.clone(),
713 policy_name: policy.name.clone(),
714 reason: "eq constraint missing 'value' field".to_string(),
715 })?
716 .clone();
717
718 Ok(CompiledConstraint::Eq {
719 param,
720 value,
721 on_match,
722 on_missing,
723 })
724 }
725 "ne" => {
726 let value = obj
727 .get("value")
728 .ok_or_else(|| PolicyValidationError {
729 policy_id: policy.id.clone(),
730 policy_name: policy.name.clone(),
731 reason: "ne constraint missing 'value' field".to_string(),
732 })?
733 .clone();
734
735 Ok(CompiledConstraint::Ne {
736 param,
737 value,
738 on_match,
739 on_missing,
740 })
741 }
742 "one_of" => {
743 let values = obj
744 .get("values")
745 .and_then(|v| v.as_array())
746 .ok_or_else(|| PolicyValidationError {
747 policy_id: policy.id.clone(),
748 policy_name: policy.name.clone(),
749 reason: "one_of constraint missing 'values' array".to_string(),
750 })?;
751
752 if values.len() > MAX_CONSTRAINT_ELEMENTS {
754 return Err(PolicyValidationError {
755 policy_id: policy.id.clone(),
756 policy_name: policy.name.clone(),
757 reason: format!(
758 "one_of values count {} exceeds maximum {}",
759 values.len(),
760 MAX_CONSTRAINT_ELEMENTS
761 ),
762 });
763 }
764 let values = values.clone();
765
766 Ok(CompiledConstraint::OneOf {
767 param,
768 values,
769 on_match,
770 on_missing,
771 })
772 }
773 "none_of" => {
774 let values = obj
775 .get("values")
776 .and_then(|v| v.as_array())
777 .ok_or_else(|| PolicyValidationError {
778 policy_id: policy.id.clone(),
779 policy_name: policy.name.clone(),
780 reason: "none_of constraint missing 'values' array".to_string(),
781 })?;
782
783 if values.len() > MAX_CONSTRAINT_ELEMENTS {
785 return Err(PolicyValidationError {
786 policy_id: policy.id.clone(),
787 policy_name: policy.name.clone(),
788 reason: format!(
789 "none_of values count {} exceeds maximum {}",
790 values.len(),
791 MAX_CONSTRAINT_ELEMENTS
792 ),
793 });
794 }
795 let values = values.clone();
796
797 Ok(CompiledConstraint::NoneOf {
798 param,
799 values,
800 on_match,
801 on_missing,
802 })
803 }
804 _ => Err(PolicyValidationError {
805 policy_id: policy.id.clone(),
806 policy_name: policy.name.clone(),
807 reason: format!("Unknown constraint operator '{}'", op),
808 }),
809 }
810 }
811
812 fn compile_context_condition(
814 policy: &Policy,
815 value: &serde_json::Value,
816 ) -> Result<CompiledContextCondition, PolicyValidationError> {
817 let obj = value.as_object().ok_or_else(|| PolicyValidationError {
818 policy_id: policy.id.clone(),
819 policy_name: policy.name.clone(),
820 reason: "Each context condition must be a JSON object".to_string(),
821 })?;
822
823 let kind =
824 obj.get("type")
825 .and_then(|v| v.as_str())
826 .ok_or_else(|| PolicyValidationError {
827 policy_id: policy.id.clone(),
828 policy_name: policy.name.clone(),
829 reason: "Context condition missing required 'type' string field".to_string(),
830 })?;
831
832 match kind {
833 "time_window" => {
834 let start_hour_u64 =
839 obj.get("start_hour")
840 .and_then(|v| v.as_u64())
841 .ok_or_else(|| PolicyValidationError {
842 policy_id: policy.id.clone(),
843 policy_name: policy.name.clone(),
844 reason: "time_window missing 'start_hour' integer".to_string(),
845 })?;
846 let end_hour_u64 =
847 obj.get("end_hour")
848 .and_then(|v| v.as_u64())
849 .ok_or_else(|| PolicyValidationError {
850 policy_id: policy.id.clone(),
851 policy_name: policy.name.clone(),
852 reason: "time_window missing 'end_hour' integer".to_string(),
853 })?;
854 if start_hour_u64 > 23 || end_hour_u64 > 23 {
855 return Err(PolicyValidationError {
856 policy_id: policy.id.clone(),
857 policy_name: policy.name.clone(),
858 reason: format!(
859 "time_window hours must be 0-23, got start={} end={}",
860 start_hour_u64, end_hour_u64
861 ),
862 });
863 }
864 let start_hour = start_hour_u64 as u8;
865 let end_hour = end_hour_u64 as u8;
866 let days_u64: Vec<u64> = obj
869 .get("days")
870 .and_then(|v| v.as_array())
871 .map(|arr| arr.iter().filter_map(|v| v.as_u64()).collect())
872 .unwrap_or_default();
873 for &day in &days_u64 {
874 if !(1..=7).contains(&day) {
875 return Err(PolicyValidationError {
876 policy_id: policy.id.clone(),
877 policy_name: policy.name.clone(),
878 reason: format!(
879 "time_window day value must be 1-7 (Mon-Sun), got {}",
880 day
881 ),
882 });
883 }
884 }
885 let days: Vec<u8> = days_u64.iter().map(|&d| d as u8).collect();
886 if start_hour == end_hour {
891 return Err(PolicyValidationError {
892 policy_id: policy.id.clone(),
893 policy_name: policy.name.clone(),
894 reason: format!(
895 "time_window start_hour and end_hour must differ (both are {}); \
896 a zero-width window permanently denies all requests",
897 start_hour
898 ),
899 });
900 }
901 let deny_reason = format!(
902 "Outside allowed time window ({:02}:00-{:02}:00) for policy '{}'",
903 start_hour, end_hour, policy.name
904 );
905 Ok(CompiledContextCondition::TimeWindow {
906 start_hour,
907 end_hour,
908 days,
909 deny_reason,
910 })
911 }
912 "max_calls" => {
913 let tool_pattern = obj
917 .get("tool_pattern")
918 .and_then(|v| v.as_str())
919 .unwrap_or("*")
920 .to_ascii_lowercase();
921 let max = obj.get("max").and_then(|v| v.as_u64()).ok_or_else(|| {
922 PolicyValidationError {
923 policy_id: policy.id.clone(),
924 policy_name: policy.name.clone(),
925 reason: "max_calls missing 'max' integer".to_string(),
926 }
927 })?;
928 let deny_reason = format!(
929 "Tool call limit ({}) exceeded for pattern '{}' in policy '{}'",
930 max, tool_pattern, policy.name
931 );
932 Ok(CompiledContextCondition::MaxCalls {
933 tool_pattern: PatternMatcher::compile(&tool_pattern),
934 max,
935 deny_reason,
936 })
937 }
938 "agent_id" => {
939 const MAX_AGENT_ID_LIST: usize = 1000;
941
942 let allowed_raw = obj
945 .get("allowed")
946 .and_then(|v| v.as_array())
947 .map(|arr| arr.as_slice())
948 .unwrap_or_default();
949 if allowed_raw.len() > MAX_AGENT_ID_LIST {
950 return Err(PolicyValidationError {
951 policy_id: policy.id.clone(),
952 policy_name: policy.name.clone(),
953 reason: format!(
954 "agent_id allowed list count {} exceeds max {}",
955 allowed_raw.len(),
956 MAX_AGENT_ID_LIST
957 ),
958 });
959 }
960 let allowed: Vec<String> = allowed_raw
964 .iter()
965 .filter_map(|v| v.as_str().map(normalize_full))
966 .collect();
967
968 let blocked_raw = obj
969 .get("blocked")
970 .and_then(|v| v.as_array())
971 .map(|arr| arr.as_slice())
972 .unwrap_or_default();
973 if blocked_raw.len() > MAX_AGENT_ID_LIST {
974 return Err(PolicyValidationError {
975 policy_id: policy.id.clone(),
976 policy_name: policy.name.clone(),
977 reason: format!(
978 "agent_id blocked list count {} exceeds max {}",
979 blocked_raw.len(),
980 MAX_AGENT_ID_LIST
981 ),
982 });
983 }
984 let blocked: Vec<String> = blocked_raw
986 .iter()
987 .filter_map(|v| v.as_str().map(normalize_full))
988 .collect();
989
990 let deny_reason =
991 format!("Agent identity not authorized by policy '{}'", policy.name);
992 Ok(CompiledContextCondition::AgentId {
993 allowed,
994 blocked,
995 deny_reason,
996 })
997 }
998 "require_previous_action" => {
999 let required_tool = normalize_full(
1003 obj.get("required_tool")
1004 .and_then(|v| v.as_str())
1005 .ok_or_else(|| PolicyValidationError {
1006 policy_id: policy.id.clone(),
1007 policy_name: policy.name.clone(),
1008 reason: "require_previous_action missing 'required_tool' string"
1009 .to_string(),
1010 })?,
1011 );
1012 let deny_reason = format!(
1013 "Required previous action '{}' not found in session history (policy '{}')",
1014 required_tool, policy.name
1015 );
1016 Ok(CompiledContextCondition::RequirePreviousAction {
1017 required_tool,
1018 deny_reason,
1019 })
1020 }
1021 "forbidden_previous_action" => {
1022 let forbidden_tool = normalize_full(
1026 obj.get("forbidden_tool")
1027 .and_then(|v| v.as_str())
1028 .ok_or_else(|| PolicyValidationError {
1029 policy_id: policy.id.clone(),
1030 policy_name: policy.name.clone(),
1031 reason: "forbidden_previous_action missing 'forbidden_tool' string"
1032 .to_string(),
1033 })?,
1034 );
1035 let deny_reason = format!(
1036 "Forbidden previous action '{}' detected in session history (policy '{}')",
1037 forbidden_tool, policy.name
1038 );
1039 Ok(CompiledContextCondition::ForbiddenPreviousAction {
1040 forbidden_tool,
1041 deny_reason,
1042 })
1043 }
1044 "max_calls_in_window" => {
1045 let tool_pattern = obj
1049 .get("tool_pattern")
1050 .and_then(|v| v.as_str())
1051 .unwrap_or("*")
1052 .to_ascii_lowercase();
1053 let max = obj.get("max").and_then(|v| v.as_u64()).ok_or_else(|| {
1054 PolicyValidationError {
1055 policy_id: policy.id.clone(),
1056 policy_name: policy.name.clone(),
1057 reason: "max_calls_in_window missing 'max' integer".to_string(),
1058 }
1059 })?;
1060 let window_raw = obj.get("window").and_then(|v| v.as_u64()).unwrap_or(0);
1064 let window = usize::try_from(window_raw).map_err(|_| PolicyValidationError {
1065 policy_id: policy.id.clone(),
1066 policy_name: policy.name.clone(),
1067 reason: format!(
1068 "max_calls_in_window 'window' value {} exceeds platform maximum ({})",
1069 window_raw,
1070 usize::MAX
1071 ),
1072 })?;
1073 let deny_reason = format!(
1074 "Tool '{}' called more than {} times in last {} actions (policy '{}')",
1075 tool_pattern,
1076 max,
1077 if window == 0 {
1078 "all".to_string()
1079 } else {
1080 window.to_string()
1081 },
1082 policy.name
1083 );
1084 Ok(CompiledContextCondition::MaxCallsInWindow {
1085 tool_pattern: PatternMatcher::compile(&tool_pattern),
1086 max,
1087 window,
1088 deny_reason,
1089 })
1090 }
1091 "max_chain_depth" => {
1092 let raw_depth = obj
1096 .get("max_depth")
1097 .and_then(|v| v.as_u64())
1098 .ok_or_else(|| PolicyValidationError {
1099 policy_id: policy.id.clone(),
1100 policy_name: policy.name.clone(),
1101 reason: "max_chain_depth missing 'max_depth' integer".to_string(),
1102 })?;
1103 let max_depth = usize::try_from(raw_depth).map_err(|_| PolicyValidationError {
1104 policy_id: policy.id.clone(),
1105 policy_name: policy.name.clone(),
1106 reason: format!(
1107 "max_chain_depth value {} exceeds platform maximum",
1108 raw_depth
1109 ),
1110 })?;
1111 let deny_reason = format!(
1112 "Call chain depth exceeds maximum of {} (policy '{}')",
1113 max_depth, policy.name
1114 );
1115 Ok(CompiledContextCondition::MaxChainDepth {
1116 max_depth,
1117 deny_reason,
1118 })
1119 }
1120 "agent_identity" => {
1121 let required_issuer = obj
1127 .get("issuer")
1128 .and_then(|v| v.as_str())
1129 .map(normalize_full);
1130 let required_subject = obj
1131 .get("subject")
1132 .and_then(|v| v.as_str())
1133 .map(normalize_full);
1134 let required_audience = obj
1135 .get("audience")
1136 .and_then(|v| v.as_str())
1137 .map(normalize_full);
1138
1139 const MAX_REQUIRED_CLAIMS: usize = 64;
1141 const MAX_CLAIM_KEY_LEN: usize = 256;
1142 const MAX_CLAIM_VALUE_LEN: usize = 512;
1143
1144 let required_claims = if let Some(m) = obj.get("claims").and_then(|v| v.as_object())
1148 {
1149 if m.len() > MAX_REQUIRED_CLAIMS {
1150 return Err(PolicyValidationError {
1151 policy_id: policy.id.clone(),
1152 policy_name: policy.name.clone(),
1153 reason: format!(
1154 "agent_identity claims count {} exceeds max {}",
1155 m.len(),
1156 MAX_REQUIRED_CLAIMS
1157 ),
1158 });
1159 }
1160 let mut map = std::collections::HashMap::new();
1161 for (k, v) in m {
1162 if k.len() > MAX_CLAIM_KEY_LEN {
1163 return Err(PolicyValidationError {
1164 policy_id: policy.id.clone(),
1165 policy_name: policy.name.clone(),
1166 reason: format!(
1167 "agent_identity claim key length {} exceeds max {}",
1168 k.len(),
1169 MAX_CLAIM_KEY_LEN
1170 ),
1171 });
1172 }
1173 if let Some(s) = v.as_str() {
1174 if s.len() > MAX_CLAIM_VALUE_LEN {
1175 return Err(PolicyValidationError {
1176 policy_id: policy.id.clone(),
1177 policy_name: policy.name.clone(),
1178 reason: format!(
1179 "agent_identity claim value for key '{}' length {} exceeds max {}",
1180 k,
1181 s.len(),
1182 MAX_CLAIM_VALUE_LEN
1183 ),
1184 });
1185 }
1186 map.insert(k.clone(), normalize_full(s));
1187 }
1188 }
1189 map
1190 } else {
1191 std::collections::HashMap::new()
1192 };
1193
1194 const MAX_ISSUER_LIST: usize = 256;
1196 const MAX_SUBJECT_LIST: usize = 256;
1197
1198 let blocked_issuers_raw = obj
1202 .get("blocked_issuers")
1203 .and_then(|v| v.as_array())
1204 .map(|arr| arr.as_slice())
1205 .unwrap_or_default();
1206 if blocked_issuers_raw.len() > MAX_ISSUER_LIST {
1207 return Err(PolicyValidationError {
1208 policy_id: policy.id.clone(),
1209 policy_name: policy.name.clone(),
1210 reason: format!(
1211 "agent_identity blocked_issuers count {} exceeds max {}",
1212 blocked_issuers_raw.len(),
1213 MAX_ISSUER_LIST
1214 ),
1215 });
1216 }
1217 let blocked_issuers: Vec<String> = blocked_issuers_raw
1218 .iter()
1219 .filter_map(|v| v.as_str().map(normalize_full))
1220 .collect();
1221
1222 let blocked_subjects_raw = obj
1223 .get("blocked_subjects")
1224 .and_then(|v| v.as_array())
1225 .map(|arr| arr.as_slice())
1226 .unwrap_or_default();
1227 if blocked_subjects_raw.len() > MAX_SUBJECT_LIST {
1228 return Err(PolicyValidationError {
1229 policy_id: policy.id.clone(),
1230 policy_name: policy.name.clone(),
1231 reason: format!(
1232 "agent_identity blocked_subjects count {} exceeds max {}",
1233 blocked_subjects_raw.len(),
1234 MAX_SUBJECT_LIST
1235 ),
1236 });
1237 }
1238 let blocked_subjects: Vec<String> = blocked_subjects_raw
1239 .iter()
1240 .filter_map(|v| v.as_str().map(normalize_full))
1241 .collect();
1242
1243 let require_attestation = obj
1245 .get("require_attestation")
1246 .and_then(|v| v.as_bool())
1247 .unwrap_or(true); let deny_reason = format!(
1250 "Agent identity attestation failed for policy '{}'",
1251 policy.name
1252 );
1253
1254 Ok(CompiledContextCondition::AgentIdentityMatch {
1255 required_issuer,
1256 required_subject,
1257 required_audience,
1258 required_claims,
1259 blocked_issuers,
1260 blocked_subjects,
1261 require_attestation,
1262 deny_reason,
1263 })
1264 }
1265
1266 "async_task_policy" => {
1270 tracing::warn!(
1273 policy_id = %policy.id,
1274 "Policy condition 'async_task_policy' is not enforced at the engine level; \
1275 it requires the MCP proxy layer for enforcement"
1276 );
1277 let max_concurrent_raw = obj
1280 .get("max_concurrent")
1281 .and_then(|v| v.as_u64())
1282 .unwrap_or(0); let max_concurrent = if max_concurrent_raw == 0 {
1284 0
1285 } else {
1286 usize::try_from(max_concurrent_raw).map_err(|_| PolicyValidationError {
1287 policy_id: policy.id.clone(),
1288 policy_name: policy.name.clone(),
1289 reason: format!(
1290 "async_task_policy 'max_concurrent' value {} exceeds platform maximum ({})",
1291 max_concurrent_raw,
1292 usize::MAX
1293 ),
1294 })?
1295 };
1296
1297 let max_duration_secs = obj
1298 .get("max_duration_secs")
1299 .and_then(|v| v.as_u64())
1300 .unwrap_or(0); let require_self_cancel = obj
1303 .get("require_self_cancel")
1304 .and_then(|v| v.as_bool())
1305 .unwrap_or(true); let deny_reason =
1308 format!("Async task policy violated for policy '{}'", policy.name);
1309
1310 Ok(CompiledContextCondition::AsyncTaskPolicy {
1311 max_concurrent,
1312 max_duration_secs,
1313 require_self_cancel,
1314 deny_reason,
1315 })
1316 }
1317
1318 "resource_indicator" => {
1319 const MAX_RESOURCE_PATTERNS: usize = 256;
1322
1323 let allowed_resources_raw = obj
1324 .get("allowed_resources")
1325 .and_then(|v| v.as_array())
1326 .map(|arr| arr.as_slice())
1327 .unwrap_or_default();
1328 if allowed_resources_raw.len() > MAX_RESOURCE_PATTERNS {
1329 return Err(PolicyValidationError {
1330 policy_id: policy.id.clone(),
1331 policy_name: policy.name.clone(),
1332 reason: format!(
1333 "resource_indicator allowed_resources count {} exceeds max {}",
1334 allowed_resources_raw.len(),
1335 MAX_RESOURCE_PATTERNS
1336 ),
1337 });
1338 }
1339 let allowed_resources: Vec<PatternMatcher> = allowed_resources_raw
1340 .iter()
1341 .filter_map(|v| v.as_str())
1342 .map(PatternMatcher::compile)
1343 .collect();
1344
1345 let require_resource = obj
1346 .get("require_resource")
1347 .and_then(|v| v.as_bool())
1348 .unwrap_or(false);
1349
1350 let deny_reason = format!(
1351 "Resource indicator validation failed for policy '{}'",
1352 policy.name
1353 );
1354
1355 Ok(CompiledContextCondition::ResourceIndicator {
1356 allowed_resources,
1357 require_resource,
1358 deny_reason,
1359 })
1360 }
1361
1362 "capability_required" => {
1363 const MAX_CAPABILITY_LIST: usize = 256;
1369 const MAX_CAPABILITY_NAME_LEN: usize = 256;
1370
1371 let required_capabilities: Vec<String> = obj
1375 .get("required_capabilities")
1376 .and_then(|v| v.as_array())
1377 .map(|arr| {
1378 arr.iter()
1379 .filter_map(|v| v.as_str().map(normalize_full))
1380 .collect()
1381 })
1382 .unwrap_or_default();
1383
1384 if required_capabilities.len() > MAX_CAPABILITY_LIST {
1385 return Err(PolicyValidationError {
1386 policy_id: policy.id.clone(),
1387 policy_name: policy.name.clone(),
1388 reason: format!(
1389 "capability_required has {} required_capabilities (max {MAX_CAPABILITY_LIST})",
1390 required_capabilities.len()
1391 ),
1392 });
1393 }
1394 for (i, cap) in required_capabilities.iter().enumerate() {
1395 if cap.len() > MAX_CAPABILITY_NAME_LEN {
1396 return Err(PolicyValidationError {
1397 policy_id: policy.id.clone(),
1398 policy_name: policy.name.clone(),
1399 reason: format!(
1400 "capability_required required_capabilities[{i}] length {} exceeds max {MAX_CAPABILITY_NAME_LEN}",
1401 cap.len()
1402 ),
1403 });
1404 }
1405 }
1406
1407 let blocked_capabilities: Vec<String> = obj
1408 .get("blocked_capabilities")
1409 .and_then(|v| v.as_array())
1410 .map(|arr| {
1411 arr.iter()
1412 .filter_map(|v| v.as_str().map(normalize_full))
1413 .collect()
1414 })
1415 .unwrap_or_default();
1416
1417 if blocked_capabilities.len() > MAX_CAPABILITY_LIST {
1418 return Err(PolicyValidationError {
1419 policy_id: policy.id.clone(),
1420 policy_name: policy.name.clone(),
1421 reason: format!(
1422 "capability_required has {} blocked_capabilities (max {MAX_CAPABILITY_LIST})",
1423 blocked_capabilities.len()
1424 ),
1425 });
1426 }
1427 for (i, cap) in blocked_capabilities.iter().enumerate() {
1428 if cap.len() > MAX_CAPABILITY_NAME_LEN {
1429 return Err(PolicyValidationError {
1430 policy_id: policy.id.clone(),
1431 policy_name: policy.name.clone(),
1432 reason: format!(
1433 "capability_required blocked_capabilities[{i}] length {} exceeds max {MAX_CAPABILITY_NAME_LEN}",
1434 cap.len()
1435 ),
1436 });
1437 }
1438 }
1439
1440 let deny_reason = format!(
1441 "Capability requirement not met for policy '{}'",
1442 policy.name
1443 );
1444
1445 Ok(CompiledContextCondition::CapabilityRequired {
1446 required_capabilities,
1447 blocked_capabilities,
1448 deny_reason,
1449 })
1450 }
1451
1452 "step_up_auth" => {
1453 let required_level_u64 = obj
1455 .get("required_level")
1456 .and_then(|v| v.as_u64())
1457 .ok_or_else(|| PolicyValidationError {
1458 policy_id: policy.id.clone(),
1459 policy_name: policy.name.clone(),
1460 reason: "step_up_auth missing 'required_level' integer".to_string(),
1461 })?;
1462
1463 if required_level_u64 > 4 {
1465 return Err(PolicyValidationError {
1466 policy_id: policy.id.clone(),
1467 policy_name: policy.name.clone(),
1468 reason: format!(
1469 "step_up_auth required_level must be 0-4, got {}",
1470 required_level_u64
1471 ),
1472 });
1473 }
1474
1475 let required_level = required_level_u64 as u8;
1476
1477 let deny_reason = format!(
1478 "Step-up authentication required (level {}) for policy '{}'",
1479 required_level, policy.name
1480 );
1481
1482 Ok(CompiledContextCondition::StepUpAuth {
1483 required_level,
1484 deny_reason,
1485 })
1486 }
1487
1488 "circuit_breaker" => {
1492 tracing::warn!(
1495 policy_id = %policy.id,
1496 "Policy condition 'circuit_breaker' is not enforced at the engine level; \
1497 it requires the MCP proxy layer for enforcement"
1498 );
1499 let tool_pattern = obj
1500 .get("tool_pattern")
1501 .and_then(|v| v.as_str())
1502 .unwrap_or("*")
1503 .to_ascii_lowercase();
1504
1505 let deny_reason = format!("Circuit breaker open (policy '{}')", policy.name);
1509
1510 Ok(CompiledContextCondition::CircuitBreaker {
1511 tool_pattern: PatternMatcher::compile(&tool_pattern),
1512 deny_reason,
1513 })
1514 }
1515
1516 "deputy_validation" => {
1517 let require_principal = obj
1519 .get("require_principal")
1520 .and_then(|v| v.as_bool())
1521 .unwrap_or(true);
1522
1523 let max_delegation_depth_u64 = obj
1524 .get("max_delegation_depth")
1525 .and_then(|v| v.as_u64())
1526 .unwrap_or(3);
1527
1528 if max_delegation_depth_u64 > 255 {
1530 return Err(PolicyValidationError {
1531 policy_id: policy.id.clone(),
1532 policy_name: policy.name.clone(),
1533 reason: format!(
1534 "deputy_validation max_delegation_depth must be 0-255, got {}",
1535 max_delegation_depth_u64
1536 ),
1537 });
1538 }
1539
1540 let max_delegation_depth = max_delegation_depth_u64 as u8;
1541
1542 let deny_reason = format!("Deputy validation failed for policy '{}'", policy.name);
1543
1544 Ok(CompiledContextCondition::DeputyValidation {
1545 require_principal,
1546 max_delegation_depth,
1547 deny_reason,
1548 })
1549 }
1550
1551 "shadow_agent_check" => {
1552 tracing::warn!(
1555 policy_id = %policy.id,
1556 "Policy condition 'shadow_agent_check' is not enforced at the engine level; \
1557 it requires the MCP proxy layer for enforcement"
1558 );
1559 let require_known_fingerprint = obj
1560 .get("require_known_fingerprint")
1561 .and_then(|v| v.as_bool())
1562 .unwrap_or(false);
1563
1564 let min_trust_level_u64 = obj
1565 .get("min_trust_level")
1566 .and_then(|v| v.as_u64())
1567 .unwrap_or(1); if min_trust_level_u64 > 4 {
1571 return Err(PolicyValidationError {
1572 policy_id: policy.id.clone(),
1573 policy_name: policy.name.clone(),
1574 reason: format!(
1575 "shadow_agent_check min_trust_level must be 0-4, got {}",
1576 min_trust_level_u64
1577 ),
1578 });
1579 }
1580
1581 let min_trust_level = min_trust_level_u64 as u8;
1582
1583 let deny_reason = format!("Shadow agent check failed for policy '{}'", policy.name);
1584
1585 Ok(CompiledContextCondition::ShadowAgentCheck {
1586 require_known_fingerprint,
1587 min_trust_level,
1588 deny_reason,
1589 })
1590 }
1591
1592 "schema_poisoning_check" => {
1593 tracing::warn!(
1596 policy_id = %policy.id,
1597 "Policy condition 'schema_poisoning_check' is not enforced at the engine level; \
1598 it requires the MCP proxy layer for enforcement"
1599 );
1600 let mutation_threshold = obj
1601 .get("mutation_threshold")
1602 .and_then(|v| v.as_f64())
1603 .map(|v| v as f32)
1604 .unwrap_or(0.1); if !mutation_threshold.is_finite() || !(0.0..=1.0).contains(&mutation_threshold) {
1608 return Err(PolicyValidationError {
1609 policy_id: policy.id.clone(),
1610 policy_name: policy.name.clone(),
1611 reason: format!(
1612 "schema_poisoning_check mutation_threshold must be in [0.0, 1.0], got {}",
1613 mutation_threshold
1614 ),
1615 });
1616 }
1617
1618 let deny_reason = format!("Schema poisoning detected for policy '{}'", policy.name);
1619
1620 Ok(CompiledContextCondition::SchemaPoisoningCheck {
1621 mutation_threshold,
1622 deny_reason,
1623 })
1624 }
1625
1626 "require_capability_token" => {
1627 const MAX_ISSUER_LIST_CAP: usize = 256;
1629
1630 let required_issuers_raw = obj
1632 .get("required_issuers")
1633 .and_then(|v| v.as_array())
1634 .map(|arr| arr.as_slice())
1635 .unwrap_or_default();
1636 if required_issuers_raw.len() > MAX_ISSUER_LIST_CAP {
1637 return Err(PolicyValidationError {
1638 policy_id: policy.id.clone(),
1639 policy_name: policy.name.clone(),
1640 reason: format!(
1641 "require_capability_token required_issuers count {} exceeds max {}",
1642 required_issuers_raw.len(),
1643 MAX_ISSUER_LIST_CAP
1644 ),
1645 });
1646 }
1647 let required_issuers: Vec<String> = required_issuers_raw
1650 .iter()
1651 .filter_map(|v| v.as_str().map(normalize_full))
1652 .collect();
1653
1654 let min_remaining_depth = obj
1656 .get("min_remaining_depth")
1657 .and_then(|v| v.as_u64())
1658 .unwrap_or(0);
1659 if min_remaining_depth > 16 {
1660 return Err(PolicyValidationError {
1661 policy_id: policy.id.clone(),
1662 policy_name: policy.name.clone(),
1663 reason: format!(
1664 "require_capability_token min_remaining_depth must be 0-16, got {}",
1665 min_remaining_depth
1666 ),
1667 });
1668 }
1669
1670 let deny_reason = format!("Capability token required for policy '{}'", policy.name);
1671
1672 Ok(CompiledContextCondition::RequireCapabilityToken {
1673 required_issuers,
1674 min_remaining_depth: min_remaining_depth as u8,
1675 deny_reason,
1676 })
1677 }
1678
1679 "min_verification_tier" => {
1680 let required_tier = if let Some(level_val) = obj.get("required_tier") {
1682 if let Some(level_u64) = level_val.as_u64() {
1683 if level_u64 > 4 {
1684 return Err(PolicyValidationError {
1685 policy_id: policy.id.clone(),
1686 policy_name: policy.name.clone(),
1687 reason: format!(
1688 "min_verification_tier required_tier must be 0-4, got {}",
1689 level_u64
1690 ),
1691 });
1692 }
1693 level_u64 as u8
1694 } else if let Some(name) = level_val.as_str() {
1695 vellaveto_types::VerificationTier::from_name(name)
1696 .map(|t| t.level())
1697 .ok_or_else(|| PolicyValidationError {
1698 policy_id: policy.id.clone(),
1699 policy_name: policy.name.clone(),
1700 reason: format!(
1701 "min_verification_tier unknown tier name '{}'",
1702 name
1703 ),
1704 })?
1705 } else {
1706 return Err(PolicyValidationError {
1707 policy_id: policy.id.clone(),
1708 policy_name: policy.name.clone(),
1709 reason: "min_verification_tier required_tier must be an integer (0-4) or tier name string".to_string(),
1710 });
1711 }
1712 } else {
1713 return Err(PolicyValidationError {
1714 policy_id: policy.id.clone(),
1715 policy_name: policy.name.clone(),
1716 reason: "min_verification_tier missing 'required_tier' field".to_string(),
1717 });
1718 };
1719
1720 let deny_reason = format!(
1721 "Verification tier below minimum (required level {}) for policy '{}'",
1722 required_tier, policy.name
1723 );
1724
1725 Ok(CompiledContextCondition::MinVerificationTier {
1726 required_tier,
1727 deny_reason,
1728 })
1729 }
1730
1731 "session_state_required" => {
1732 const MAX_ALLOWED_STATES: usize = 1000;
1735 const MAX_STATE_NAME_LEN: usize = 256;
1736
1737 let raw_arr = obj
1739 .get("allowed_states")
1740 .and_then(|v| v.as_array())
1741 .cloned()
1742 .unwrap_or_default();
1743
1744 if raw_arr.len() > MAX_ALLOWED_STATES {
1745 return Err(PolicyValidationError {
1746 policy_id: policy.id.clone(),
1747 policy_name: policy.name.clone(),
1748 reason: format!(
1749 "session_state_required allowed_states has {} entries, max {}",
1750 raw_arr.len(),
1751 MAX_ALLOWED_STATES,
1752 ),
1753 });
1754 }
1755
1756 let mut allowed_states = Vec::with_capacity(raw_arr.len());
1757 for entry in &raw_arr {
1758 if let Some(s) = entry.as_str() {
1759 if s.len() > MAX_STATE_NAME_LEN {
1760 return Err(PolicyValidationError {
1761 policy_id: policy.id.clone(),
1762 policy_name: policy.name.clone(),
1763 reason: format!(
1764 "session_state_required allowed_states entry length {} exceeds max {}",
1765 s.len(),
1766 MAX_STATE_NAME_LEN,
1767 ),
1768 });
1769 }
1770 allowed_states.push(normalize_full(s));
1773 }
1774 }
1775
1776 if allowed_states.is_empty() {
1777 return Err(PolicyValidationError {
1778 policy_id: policy.id.clone(),
1779 policy_name: policy.name.clone(),
1780 reason:
1781 "session_state_required must have at least one allowed_states entry"
1782 .to_string(),
1783 });
1784 }
1785
1786 let deny_reason = format!(
1787 "Session state not in allowed states for policy '{}'",
1788 policy.name
1789 );
1790
1791 Ok(CompiledContextCondition::SessionStateRequired {
1792 allowed_states,
1793 deny_reason,
1794 })
1795 }
1796
1797 "required_action_sequence" => Self::compile_action_sequence(obj, policy, true),
1801
1802 "forbidden_action_sequence" => Self::compile_action_sequence(obj, policy, false),
1803
1804 "workflow_template" => Self::compile_workflow_template(obj, policy),
1805
1806 _ => Err(PolicyValidationError {
1807 policy_id: policy.id.clone(),
1808 policy_name: policy.name.clone(),
1809 reason: format!("Unknown context condition type '{}'", kind),
1810 }),
1811 }
1812 }
1813
1814 fn compile_action_sequence(
1818 obj: &serde_json::Map<String, serde_json::Value>,
1819 policy: &vellaveto_types::Policy,
1820 is_required: bool,
1821 ) -> Result<CompiledContextCondition, PolicyValidationError> {
1822 const MAX_SEQUENCE_STEPS: usize = 20;
1823 const MAX_TOOL_NAME_LEN: usize = 256;
1825
1826 let kind = if is_required {
1827 "required_action_sequence"
1828 } else {
1829 "forbidden_action_sequence"
1830 };
1831
1832 let arr = obj
1833 .get("sequence")
1834 .and_then(|v| v.as_array())
1835 .ok_or_else(|| PolicyValidationError {
1836 policy_id: policy.id.clone(),
1837 policy_name: policy.name.clone(),
1838 reason: format!("{kind} requires a 'sequence' array"),
1839 })?;
1840
1841 if arr.is_empty() {
1842 return Err(PolicyValidationError {
1843 policy_id: policy.id.clone(),
1844 policy_name: policy.name.clone(),
1845 reason: format!("{kind} sequence must not be empty"),
1846 });
1847 }
1848
1849 if arr.len() > MAX_SEQUENCE_STEPS {
1850 return Err(PolicyValidationError {
1851 policy_id: policy.id.clone(),
1852 policy_name: policy.name.clone(),
1853 reason: format!(
1854 "{kind} sequence has {} steps (max {MAX_SEQUENCE_STEPS})",
1855 arr.len()
1856 ),
1857 });
1858 }
1859
1860 let mut sequence = Vec::with_capacity(arr.len());
1861 for (i, val) in arr.iter().enumerate() {
1862 let s = val.as_str().ok_or_else(|| PolicyValidationError {
1863 policy_id: policy.id.clone(),
1864 policy_name: policy.name.clone(),
1865 reason: format!("{kind} sequence[{i}] must be a string"),
1866 })?;
1867
1868 if s.is_empty() {
1869 return Err(PolicyValidationError {
1870 policy_id: policy.id.clone(),
1871 policy_name: policy.name.clone(),
1872 reason: format!("{kind} sequence[{i}] must not be empty"),
1873 });
1874 }
1875
1876 if s.chars()
1878 .any(|c| c.is_control() || vellaveto_types::is_unicode_format_char(c))
1879 {
1880 return Err(PolicyValidationError {
1881 policy_id: policy.id.clone(),
1882 policy_name: policy.name.clone(),
1883 reason: format!("{kind} sequence[{i}] contains control or format characters"),
1884 });
1885 }
1886
1887 if s.len() > MAX_TOOL_NAME_LEN {
1889 return Err(PolicyValidationError {
1890 policy_id: policy.id.clone(),
1891 policy_name: policy.name.clone(),
1892 reason: format!(
1893 "{kind} sequence[{i}] tool name is {} bytes, max is {MAX_TOOL_NAME_LEN}",
1894 s.len()
1895 ),
1896 });
1897 }
1898
1899 sequence.push(normalize_full(s));
1902 }
1903
1904 let ordered = obj.get("ordered").and_then(|v| v.as_bool()).unwrap_or(true);
1905
1906 if is_required {
1907 let deny_reason = format!(
1908 "Required action sequence not satisfied for policy '{}'",
1909 policy.name
1910 );
1911 Ok(CompiledContextCondition::RequiredActionSequence {
1912 sequence,
1913 ordered,
1914 deny_reason,
1915 })
1916 } else {
1917 let deny_reason = format!(
1918 "Forbidden action sequence detected for policy '{}'",
1919 policy.name
1920 );
1921 Ok(CompiledContextCondition::ForbiddenActionSequence {
1922 sequence,
1923 ordered,
1924 deny_reason,
1925 })
1926 }
1927 }
1928
1929 fn compile_workflow_template(
1934 obj: &serde_json::Map<String, serde_json::Value>,
1935 policy: &vellaveto_types::Policy,
1936 ) -> Result<CompiledContextCondition, PolicyValidationError> {
1937 use std::collections::{HashMap, HashSet, VecDeque};
1938
1939 const MAX_WORKFLOW_STEPS: usize = 50;
1940 const MAX_SUCCESSORS_PER_STEP: usize = 50;
1942 const MAX_TOOL_NAME_LEN: usize = 256;
1944
1945 let steps =
1946 obj.get("steps")
1947 .and_then(|v| v.as_array())
1948 .ok_or_else(|| PolicyValidationError {
1949 policy_id: policy.id.clone(),
1950 policy_name: policy.name.clone(),
1951 reason: "workflow_template requires a 'steps' array".to_string(),
1952 })?;
1953
1954 if steps.is_empty() {
1955 return Err(PolicyValidationError {
1956 policy_id: policy.id.clone(),
1957 policy_name: policy.name.clone(),
1958 reason: "workflow_template steps must not be empty".to_string(),
1959 });
1960 }
1961
1962 if steps.len() > MAX_WORKFLOW_STEPS {
1963 return Err(PolicyValidationError {
1964 policy_id: policy.id.clone(),
1965 policy_name: policy.name.clone(),
1966 reason: format!(
1967 "workflow_template has {} steps (max {MAX_WORKFLOW_STEPS})",
1968 steps.len()
1969 ),
1970 });
1971 }
1972
1973 let enforce = obj
1974 .get("enforce")
1975 .and_then(|v| v.as_str())
1976 .unwrap_or("strict");
1977
1978 let strict = match enforce {
1979 "strict" => true,
1980 "warn" => false,
1981 other => {
1982 return Err(PolicyValidationError {
1983 policy_id: policy.id.clone(),
1984 policy_name: policy.name.clone(),
1985 reason: format!(
1986 "workflow_template enforce must be 'strict' or 'warn', got '{other}'"
1987 ),
1988 });
1989 }
1990 };
1991
1992 let mut adjacency: HashMap<String, Vec<String>> = HashMap::new();
1993 let mut governed_tools: HashSet<String> = HashSet::new();
1994 let mut seen_tools: HashSet<String> = HashSet::new();
1995
1996 for (i, step) in steps.iter().enumerate() {
1997 let step_obj = step.as_object().ok_or_else(|| PolicyValidationError {
1998 policy_id: policy.id.clone(),
1999 policy_name: policy.name.clone(),
2000 reason: format!("workflow_template steps[{i}] must be an object"),
2001 })?;
2002
2003 let tool = step_obj
2004 .get("tool")
2005 .and_then(|v| v.as_str())
2006 .ok_or_else(|| PolicyValidationError {
2007 policy_id: policy.id.clone(),
2008 policy_name: policy.name.clone(),
2009 reason: format!("workflow_template steps[{i}] requires a 'tool' string"),
2010 })?;
2011
2012 if tool.is_empty() {
2013 return Err(PolicyValidationError {
2014 policy_id: policy.id.clone(),
2015 policy_name: policy.name.clone(),
2016 reason: format!("workflow_template steps[{i}].tool must not be empty"),
2017 });
2018 }
2019
2020 if tool
2022 .chars()
2023 .any(|c| c.is_control() || vellaveto_types::is_unicode_format_char(c))
2024 {
2025 return Err(PolicyValidationError {
2026 policy_id: policy.id.clone(),
2027 policy_name: policy.name.clone(),
2028 reason: format!(
2029 "workflow_template steps[{i}].tool contains control or format characters"
2030 ),
2031 });
2032 }
2033
2034 if tool.len() > MAX_TOOL_NAME_LEN {
2036 return Err(PolicyValidationError {
2037 policy_id: policy.id.clone(),
2038 policy_name: policy.name.clone(),
2039 reason: format!(
2040 "workflow_template steps[{i}].tool is {} bytes, max is {MAX_TOOL_NAME_LEN}",
2041 tool.len()
2042 ),
2043 });
2044 }
2045
2046 let tool_lower = normalize_full(tool);
2048
2049 if !seen_tools.insert(tool_lower.clone()) {
2050 return Err(PolicyValidationError {
2051 policy_id: policy.id.clone(),
2052 policy_name: policy.name.clone(),
2053 reason: format!("workflow_template has duplicate step tool '{tool}'"),
2054 });
2055 }
2056
2057 let then_arr = step_obj
2058 .get("then")
2059 .and_then(|v| v.as_array())
2060 .ok_or_else(|| PolicyValidationError {
2061 policy_id: policy.id.clone(),
2062 policy_name: policy.name.clone(),
2063 reason: format!("workflow_template steps[{i}] requires a 'then' array"),
2064 })?;
2065
2066 if then_arr.len() > MAX_SUCCESSORS_PER_STEP {
2069 return Err(PolicyValidationError {
2070 policy_id: policy.id.clone(),
2071 policy_name: policy.name.clone(),
2072 reason: format!(
2073 "workflow_template steps[{i}].then has {} entries, max is {MAX_SUCCESSORS_PER_STEP}",
2074 then_arr.len()
2075 ),
2076 });
2077 }
2078
2079 let mut successors = Vec::with_capacity(then_arr.len());
2080 let mut seen_successors: HashSet<String> = HashSet::new();
2082 for (j, v) in then_arr.iter().enumerate() {
2083 let s = v.as_str().ok_or_else(|| PolicyValidationError {
2084 policy_id: policy.id.clone(),
2085 policy_name: policy.name.clone(),
2086 reason: format!("workflow_template steps[{i}].then[{j}] must be a string"),
2087 })?;
2088
2089 if s.is_empty() {
2090 return Err(PolicyValidationError {
2091 policy_id: policy.id.clone(),
2092 policy_name: policy.name.clone(),
2093 reason: format!("workflow_template steps[{i}].then[{j}] must not be empty"),
2094 });
2095 }
2096
2097 if s.chars()
2099 .any(|c| c.is_control() || vellaveto_types::is_unicode_format_char(c))
2100 {
2101 return Err(PolicyValidationError {
2102 policy_id: policy.id.clone(),
2103 policy_name: policy.name.clone(),
2104 reason: format!(
2105 "workflow_template steps[{i}].then[{j}] contains control or format characters"
2106 ),
2107 });
2108 }
2109
2110 if s.len() > MAX_TOOL_NAME_LEN {
2112 return Err(PolicyValidationError {
2113 policy_id: policy.id.clone(),
2114 policy_name: policy.name.clone(),
2115 reason: format!(
2116 "workflow_template steps[{i}].then[{j}] is {} bytes, max is {MAX_TOOL_NAME_LEN}",
2117 s.len()
2118 ),
2119 });
2120 }
2121
2122 let lowered = normalize_full(s);
2124 if seen_successors.insert(lowered.clone()) {
2127 successors.push(lowered);
2128 }
2129 }
2130
2131 governed_tools.insert(tool_lower.clone());
2132 for succ in &successors {
2133 governed_tools.insert(succ.clone());
2134 }
2135
2136 adjacency.insert(tool_lower, successors);
2137 }
2138
2139 let mut has_predecessor: HashSet<&str> = HashSet::new();
2141 for successors in adjacency.values() {
2142 for succ in successors {
2143 has_predecessor.insert(succ.as_str());
2144 }
2145 }
2146 let mut entry_points: Vec<String> = governed_tools
2149 .iter()
2150 .filter(|t| !has_predecessor.contains(t.as_str()))
2151 .cloned()
2152 .collect();
2153 entry_points.sort();
2154
2155 if entry_points.is_empty() {
2156 return Err(PolicyValidationError {
2157 policy_id: policy.id.clone(),
2158 policy_name: policy.name.clone(),
2159 reason: "workflow_template has no entry points (implies cycle)".to_string(),
2160 });
2161 }
2162
2163 let mut in_degree: HashMap<&str, usize> = HashMap::new();
2165 for tool in &governed_tools {
2166 in_degree.insert(tool.as_str(), 0);
2167 }
2168 for successors in adjacency.values() {
2169 for succ in successors {
2170 if let Some(deg) = in_degree.get_mut(succ.as_str()) {
2171 *deg = deg.saturating_add(1);
2172 }
2173 }
2174 }
2175
2176 let mut queue: VecDeque<&str> = VecDeque::new();
2177 for (tool, deg) in &in_degree {
2178 if *deg == 0 {
2179 queue.push_back(tool);
2180 }
2181 }
2182
2183 let mut visited_count: usize = 0;
2184 while let Some(node) = queue.pop_front() {
2185 visited_count += 1;
2186 if let Some(successors) = adjacency.get(node) {
2187 for succ in successors {
2188 if let Some(deg) = in_degree.get_mut(succ.as_str()) {
2189 *deg = deg.saturating_sub(1);
2190 if *deg == 0 {
2191 queue.push_back(succ.as_str());
2192 }
2193 }
2194 }
2195 }
2196 }
2197
2198 if visited_count < governed_tools.len() {
2199 return Err(PolicyValidationError {
2200 policy_id: policy.id.clone(),
2201 policy_name: policy.name.clone(),
2202 reason: "workflow_template contains a cycle".to_string(),
2203 });
2204 }
2205
2206 let deny_reason = format!("Workflow template violation for policy '{}'", policy.name);
2207
2208 Ok(CompiledContextCondition::WorkflowTemplate {
2209 adjacency,
2210 governed_tools,
2211 entry_points,
2212 strict,
2213 deny_reason,
2214 })
2215 }
2216}