deepstrike_core/governance/
constraint.rs1use crate::types::message::ToolCall;
2use crate::types::policy::GovernanceVerdict;
3
4#[derive(Debug, Clone)]
11pub struct ParamConstraint {
12 pub tool_name: String,
13 pub param_path: String,
14 pub rule: ConstraintRule,
15}
16
17#[derive(Debug, Clone)]
18pub enum ConstraintRule {
19 Range { min: Option<f64>, max: Option<f64> },
21 Enum(Vec<String>),
23 Required,
25}
26
27pub struct ConstraintValidator {
29 constraints: Vec<ParamConstraint>,
30}
31
32impl ConstraintValidator {
33 pub fn new() -> Self {
34 Self {
35 constraints: Vec::new(),
36 }
37 }
38
39 pub fn add(&mut self, constraint: ParamConstraint) {
40 self.constraints.push(constraint);
41 }
42
43 pub fn constraint_count(&self) -> usize {
44 self.constraints.len()
45 }
46
47 pub fn validate(&self, call: &ToolCall) -> Option<GovernanceVerdict> {
48 for c in &self.constraints {
49 if c.tool_name != call.name.as_str() {
50 continue;
51 }
52 let value = call
53 .arguments
54 .pointer(&format!("/{}", c.param_path.replace('.', "/")));
55
56 match &c.rule {
57 ConstraintRule::Required => {
58 if value.is_none() || value == Some(&serde_json::Value::Null) {
59 return Some(GovernanceVerdict::Deny {
60 stage: "constraint",
61 reason: format!(
62 "parameter '{}' is required for '{}'",
63 c.param_path, c.tool_name
64 ),
65 });
66 }
67 }
68 ConstraintRule::Enum(allowed) => {
69 if let Some(val) = value.and_then(|v| v.as_str()) {
70 if !allowed.iter().any(|a| a == val) {
71 return Some(GovernanceVerdict::Deny {
72 stage: "constraint",
73 reason: format!(
74 "parameter '{}' value '{}' not in allowed: {:?}",
75 c.param_path, val, allowed
76 ),
77 });
78 }
79 }
80 }
81 ConstraintRule::Range { min, max } => {
82 if let Some(val) = value.and_then(|v| v.as_f64()) {
83 if let Some(lo) = min {
84 if val < *lo {
85 return Some(GovernanceVerdict::Deny {
86 stage: "constraint",
87 reason: format!(
88 "parameter '{}' value {} below minimum {}",
89 c.param_path, val, lo
90 ),
91 });
92 }
93 }
94 if let Some(hi) = max {
95 if val > *hi {
96 return Some(GovernanceVerdict::Deny {
97 stage: "constraint",
98 reason: format!(
99 "parameter '{}' value {} above maximum {}",
100 c.param_path, val, hi
101 ),
102 });
103 }
104 }
105 }
106 }
107 }
108 }
109 None
110 }
111}
112
113impl Default for ConstraintValidator {
114 fn default() -> Self {
115 Self::new()
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122 use compact_str::CompactString;
123
124 fn call(name: &str, args: serde_json::Value) -> ToolCall {
125 ToolCall {
126 id: CompactString::new("c1"),
127 name: CompactString::new(name),
128 arguments: args,
129 }
130 }
131
132 #[test]
133 fn required_param_missing_denies() {
134 let mut v = ConstraintValidator::new();
135 v.add(ParamConstraint {
136 tool_name: "writefile".into(),
137 param_path: "path".into(),
138 rule: ConstraintRule::Required,
139 });
140 let verdict = v.validate(&call("writefile", serde_json::json!({})));
141 assert!(matches!(
142 verdict,
143 Some(GovernanceVerdict::Deny {
144 stage: "constraint",
145 ..
146 })
147 ));
148 }
149
150 #[test]
151 fn enum_rule_rejects_unknown_value() {
152 let mut v = ConstraintValidator::new();
153 v.add(ParamConstraint {
154 tool_name: "set_mode".into(),
155 param_path: "mode".into(),
156 rule: ConstraintRule::Enum(vec!["read".into(), "write".into()]),
157 });
158 let verdict = v.validate(&call("set_mode", serde_json::json!({"mode": "exec"})));
159 assert!(matches!(verdict, Some(GovernanceVerdict::Deny { .. })));
160 }
161
162 #[test]
163 fn range_rule_enforces_bounds() {
164 let mut v = ConstraintValidator::new();
165 v.add(ParamConstraint {
166 tool_name: "sleep".into(),
167 param_path: "seconds".into(),
168 rule: ConstraintRule::Range {
169 min: Some(0.0),
170 max: Some(10.0),
171 },
172 });
173 assert!(
174 v.validate(&call("sleep", serde_json::json!({"seconds": 5})))
175 .is_none()
176 );
177 assert!(
178 v.validate(&call("sleep", serde_json::json!({"seconds": 100})))
179 .is_some()
180 );
181 }
182}