Skip to main content

deepstrike_core/governance/
constraint.rs

1use crate::types::message::ToolCall;
2use crate::types::policy::GovernanceVerdict;
3
4/// A parameter constraint for tool arguments.
5///
6/// **Scope**: built-in rules cover the structural validation cases
7/// (required / range / enum). For pattern matching or custom predicates,
8/// register a `VetoCheck` instead — keeps the kernel free of regex deps
9/// and lets the SDK use whatever pattern engine suits its host language.
10#[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    /// Numeric value in range
20    Range { min: Option<f64>, max: Option<f64> },
21    /// Value must be one of these
22    Enum(Vec<String>),
23    /// Value must not be empty
24    Required,
25}
26
27/// Validates tool call arguments against registered constraints.
28pub 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}