Skip to main content

wm_dispatch/
speculative.rs

1//! Speculative Execution Validator — pre-validates outputs before dispatch.
2//!
3//! Ported from v2's optimization/speculative_exec.py.
4//! Validates AI-generated code or text using a hierarchy of cheap checks:
5//! 1. Bracket/brace balance check (< 1ms)
6//! 2. Security heuristics — regex scan for SQLi, hardcoded secrets, dangerous calls
7//! 3. (Future) Local LLM sanity check
8//!
9//! Prevents invalid or unsafe outputs from reaching expensive downstream
10//! processes or the user.
11
12use regex::Regex;
13use std::sync::OnceLock;
14
15static SQL_INJECTION_RE: OnceLock<Regex> = OnceLock::new();
16static HARDCODED_SECRET_RE: OnceLock<Regex> = OnceLock::new();
17static DANGEROUS_EXEC_RE: OnceLock<Regex> = OnceLock::new();
18
19fn sql_injection_re() -> &'static Regex {
20    SQL_INJECTION_RE.get_or_init(|| Regex::new(r#"(?i)execute\(\s*f["']"#).unwrap())
21}
22
23fn hardcoded_secret_re() -> &'static Regex {
24    HARDCODED_SECRET_RE.get_or_init(|| {
25        Regex::new(r#"(?i)(api_key|secret|password|token)\s*=\s*["'][A-Za-z0-9\-_]{20,}["']"#)
26            .unwrap()
27    })
28}
29
30fn dangerous_exec_re() -> &'static Regex {
31    DANGEROUS_EXEC_RE.get_or_init(|| Regex::new(r"\b(exec|eval|system|popen)\s*\(").unwrap())
32}
33
34/// Result of a single validation check.
35#[derive(Debug, Clone)]
36pub struct CheckResult {
37    /// Check name
38    pub name: &'static str,
39    /// Whether the check passed
40    pub passed: bool,
41    /// Error message if failed
42    pub error: Option<String>,
43    /// Issues found (for security checks)
44    pub issues: Vec<String>,
45}
46
47/// Full validation result.
48#[derive(Debug, Clone)]
49pub struct ValidationResult {
50    /// Overall validity — true only if all checks passed
51    pub valid: bool,
52    /// Individual check results
53    pub checks: Vec<CheckResult>,
54    /// All error messages
55    pub errors: Vec<String>,
56}
57
58impl ValidationResult {
59    /// Create a new passing result.
60    #[must_use]
61    pub const fn passing() -> Self {
62        Self {
63            valid: true,
64            checks: Vec::new(),
65            errors: Vec::new(),
66        }
67    }
68}
69
70/// Speculative executor — validates code/text candidates.
71///
72/// Uses a hierarchy of cheap-to-expensive checks. Fails fast: if a cheap
73/// check fails, more expensive checks are skipped.
74pub struct SpeculativeExecutor {
75    /// Whether to run security heuristics
76    pub check_security: bool,
77    /// Whether to run bracket balance check
78    pub check_brackets: bool,
79}
80
81impl Default for SpeculativeExecutor {
82    fn default() -> Self {
83        Self {
84            check_security: true,
85            check_brackets: true,
86        }
87    }
88}
89
90impl SpeculativeExecutor {
91    /// Create a new executor with all checks enabled.
92    #[must_use]
93    pub const fn new() -> Self {
94        Self {
95            check_security: true,
96            check_brackets: true,
97        }
98    }
99
100    /// Check bracket/brace/paren balance in code.
101    ///
102    /// Returns `(balanced, error_message)`.
103    #[must_use]
104    pub fn check_bracket_balance(code: &str) -> (bool, Option<String>) {
105        let mut paren = 0i32;
106        let mut bracket = 0i32;
107        let mut brace = 0i32;
108        let mut in_string = false;
109        let mut string_char = '\0';
110        let mut escaped = false;
111
112        for ch in code.chars() {
113            if escaped {
114                escaped = false;
115                continue;
116            }
117            if ch == '\\' && in_string {
118                escaped = true;
119                continue;
120            }
121            if in_string {
122                if ch == string_char {
123                    in_string = false;
124                }
125                continue;
126            }
127            match ch {
128                '"' | '\'' => {
129                    in_string = true;
130                    string_char = ch;
131                }
132                '(' => paren += 1,
133                ')' => paren -= 1,
134                '[' => bracket += 1,
135                ']' => bracket -= 1,
136                '{' => brace += 1,
137                '}' => brace -= 1,
138                _ => {}
139            }
140            if paren < 0 || bracket < 0 || brace < 0 {
141                return (false, Some(format!("Unmatched closing delimiter at: {ch}")));
142            }
143        }
144
145        if paren != 0 {
146            return (
147                false,
148                Some(format!("Unbalanced parentheses: offset {paren}")),
149            );
150        }
151        if bracket != 0 {
152            return (
153                false,
154                Some(format!("Unbalanced brackets: offset {bracket}")),
155            );
156        }
157        if brace != 0 {
158            return (false, Some(format!("Unbalanced braces: offset {brace}")));
159        }
160
161        (true, None)
162    }
163
164    /// Fast regex scan for obvious security issues.
165    ///
166    /// Returns `(clean, issues)`.
167    #[must_use]
168    pub fn check_security_heuristics(code: &str) -> (bool, Vec<String>) {
169        let mut issues = Vec::new();
170
171        if sql_injection_re().is_match(code) {
172            issues.push("Potential SQL Injection (f-string in execute)".to_string());
173        }
174
175        if hardcoded_secret_re().is_match(code) {
176            issues.push("Potential hardcoded secret".to_string());
177        }
178
179        if dangerous_exec_re().is_match(code) {
180            issues.push("Dangerous usage of exec/eval/system/popen".to_string());
181        }
182
183        (issues.is_empty(), issues)
184    }
185
186    /// Run full validation pipeline.
187    #[must_use]
188    pub fn validate(&self, content: &str) -> ValidationResult {
189        let mut result = ValidationResult::passing();
190
191        if self.check_brackets {
192            let (balanced, err) = Self::check_bracket_balance(content);
193            let check = CheckResult {
194                name: "bracket_balance",
195                passed: balanced,
196                error: err.clone(),
197                issues: Vec::new(),
198            };
199            result.checks.push(check);
200            if !balanced {
201                result.valid = false;
202                if let Some(e) = err {
203                    result.errors.push(e);
204                }
205                return result; // Fail fast
206            }
207        }
208
209        if self.check_security {
210            let (clean, issues) = Self::check_security_heuristics(content);
211            let check = CheckResult {
212                name: "security",
213                passed: clean,
214                error: None,
215                issues: issues.clone(),
216            };
217            result.checks.push(check);
218            if !clean {
219                result.valid = false;
220                result.errors.extend(issues);
221            }
222        }
223
224        result
225    }
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231
232    #[test]
233    fn bracket_balance_ok() {
234        let (ok, err) = SpeculativeExecutor::check_bracket_balance("fn main() { let x = [1, 2]; }");
235        assert!(ok);
236        assert!(err.is_none());
237    }
238
239    #[test]
240    fn bracket_balance_unmatched() {
241        let (ok, err) = SpeculativeExecutor::check_bracket_balance("fn main() {");
242        assert!(!ok);
243        assert!(err.is_some());
244    }
245
246    #[test]
247    fn bracket_balance_ignores_strings() {
248        let (ok, _) =
249            SpeculativeExecutor::check_bracket_balance("let s = \"(unmatched in string\";");
250        assert!(ok);
251    }
252
253    #[test]
254    fn bracket_balance_ignores_escapes() {
255        let (ok, _) = SpeculativeExecutor::check_bracket_balance("let s = \"\\\"escaped\\\"\";");
256        assert!(ok);
257    }
258
259    #[test]
260    fn security_clean_code() {
261        let (clean, issues) = SpeculativeExecutor::check_security_heuristics("let x = 1 + 2;");
262        assert!(clean);
263        assert!(issues.is_empty());
264    }
265
266    #[test]
267    fn security_detects_sql_injection() {
268        let (clean, issues) = SpeculativeExecutor::check_security_heuristics(
269            "cursor.execute(f\"SELECT * FROM {table}\")",
270        );
271        assert!(!clean);
272        assert!(issues.iter().any(|i| i.contains("SQL Injection")));
273    }
274
275    #[test]
276    fn security_detects_hardcoded_secret() {
277        let (clean, issues) = SpeculativeExecutor::check_security_heuristics(
278            "api_key = \"abcdefghijklmnopqrstuvwxyz123456\"",
279        );
280        assert!(!clean);
281        assert!(issues.iter().any(|i| i.contains("secret")));
282    }
283
284    #[test]
285    fn security_detects_dangerous_exec() {
286        let (clean, issues) = SpeculativeExecutor::check_security_heuristics("eval(user_input)");
287        assert!(!clean);
288        assert!(issues.iter().any(|i| i.contains("exec/eval")));
289    }
290
291    #[test]
292    fn validate_passes_clean_code() {
293        let executor = SpeculativeExecutor::default();
294        let result = executor.validate("fn add(a: i32, b: i32) -> i32 { a + b }");
295        assert!(result.valid);
296        assert!(result.errors.is_empty());
297    }
298
299    #[test]
300    fn validate_fails_on_unbalanced() {
301        let executor = SpeculativeExecutor::default();
302        let result = executor.validate("fn add(a: i32 {");
303        assert!(!result.valid);
304        assert!(!result.errors.is_empty());
305    }
306
307    #[test]
308    fn validate_fails_on_security() {
309        let executor = SpeculativeExecutor::default();
310        let result = executor.validate("eval(\"dangerous code\")");
311        assert!(!result.valid);
312        assert!(result.errors.iter().any(|e| e.contains("exec/eval")));
313    }
314
315    #[test]
316    fn validate_fail_fast_on_brackets() {
317        let executor = SpeculativeExecutor::default();
318        let result = executor.validate("fn { eval(\"bad\")");
319        // Should fail on brackets before security
320        assert!(!result.valid);
321        // Only bracket check should have run
322        assert_eq!(result.checks.len(), 1);
323        assert_eq!(result.checks[0].name, "bracket_balance");
324    }
325
326    #[test]
327    fn validate_passes_with_security_disabled() {
328        let mut executor = SpeculativeExecutor::new();
329        executor.check_security = false;
330        let result = executor.validate("let x = 1;");
331        assert!(result.valid);
332        // Only bracket check should run
333        assert_eq!(result.checks.len(), 1);
334    }
335}