assay_core/
policy_engine.rs1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
5#[serde(rename_all = "snake_case")]
6pub enum VerdictStatus {
7 Allowed,
8 Blocked,
9}
10
11#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
12pub struct Verdict {
13 pub status: VerdictStatus,
14 pub reason_code: String, pub details: Value, }
17
18pub fn evaluate_tool_args(policy: &Value, tool_name: &str, tool_args: &Value) -> Verdict {
21 let schema_val = match policy.get(tool_name) {
23 Some(s) => s,
24 None => {
25 let mut message = format!("Tool '{}' not defined in policy", tool_name);
27 if let Some(obj) = policy.as_object() {
28 if let Some(match_) =
30 crate::errors::similarity::closest_prompt(tool_name, obj.keys())
31 {
32 message.push_str(&format!(". Did you mean '{}'?", match_.prompt));
33 }
34 }
35
36 return Verdict {
37 status: VerdictStatus::Blocked,
38 reason_code: "E_POLICY_MISSING_TOOL".to_string(),
39 details: serde_json::json!({
40 "message": message
41 }),
42 };
43 }
44 };
45
46 let compiled = match jsonschema::validator_for(schema_val) {
55 Ok(c) => c,
56 Err(e) => {
57 return Verdict {
58 status: VerdictStatus::Blocked,
59 reason_code: "E_SCHEMA_COMPILE".to_string(),
60 details: serde_json::json!({
61 "message": format!("Invalid schema for tool '{}': {}", tool_name, e)
62 }),
63 };
64 }
65 };
66
67 evaluate_schema(&compiled, tool_args)
69}
70
71pub fn evaluate_schema(compiled: &jsonschema::Validator, tool_args: &Value) -> Verdict {
73 if compiled.is_valid(tool_args) {
74 return Verdict {
75 status: VerdictStatus::Allowed,
76 reason_code: "OK".to_string(),
77 details: serde_json::json!({}),
78 };
79 }
80 let violations: Vec<Value> = compiled
81 .iter_errors(tool_args)
82 .map(|e| {
83 serde_json::json!({
84 "path": e.instance_path().to_string(),
85 "constraint": e.to_string(),
86 "message": e.to_string()
87 })
88 })
89 .collect();
90 Verdict {
91 status: VerdictStatus::Blocked,
92 reason_code: "E_ARG_SCHEMA".to_string(),
93 details: serde_json::json!({
94 "violations": violations
95 }),
96 }
97}
98
99pub fn evaluate_sequence(policy_regex: &str, tool_names: &[String]) -> Verdict {
104 let trace_str = tool_names.join(" ");
108
109 let re = match regex::Regex::new(policy_regex) {
112 Ok(r) => r,
113 Err(e) => {
114 return Verdict {
115 status: VerdictStatus::Blocked,
116 reason_code: "E_POLICY_REGEX_INVALID".to_string(),
117 details: serde_json::json!({
118 "message": format!("Invalid regex policy '{}': {}", policy_regex, e)
119 }),
120 };
121 }
122 };
123
124 if re.is_match(&trace_str) {
126 Verdict {
127 status: VerdictStatus::Allowed,
128 reason_code: "OK".to_string(),
129 details: serde_json::json!({}),
130 }
131 } else {
132 Verdict {
133 status: VerdictStatus::Blocked,
134 reason_code: "E_SEQUENCE_VIOLATION".to_string(),
135 details: serde_json::json!({
136 "expected": policy_regex,
137 "found": trace_str
138 }),
139 }
140 }
141}