assay_core/
policy_engine.rs1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use std::collections::HashMap;
4
5#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
6#[serde(rename_all = "snake_case")]
7pub enum VerdictStatus {
8 Allowed,
9 Blocked,
10}
11
12#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
13pub struct Verdict {
14 pub status: VerdictStatus,
15 pub reason_code: String, pub details: Value, }
18
19pub fn evaluate_tool_args(policy: &Value, tool_name: &str, tool_args: &Value) -> Verdict {
22 let schema_val = match policy.get(tool_name) {
24 Some(s) => s,
25 None => {
26 let mut message = format!("Tool '{}' not defined in policy", tool_name);
28 if let Some(obj) = policy.as_object() {
29 if let Some(match_) =
31 crate::errors::similarity::closest_prompt(tool_name, obj.keys())
32 {
33 message.push_str(&format!(". Did you mean '{}'?", match_.prompt));
34 }
35 }
36
37 return Verdict {
38 status: VerdictStatus::Blocked,
39 reason_code: "E_POLICY_MISSING_TOOL".to_string(),
40 details: serde_json::json!({
41 "message": message
42 }),
43 };
44 }
45 };
46
47 let compiled = match jsonschema::validator_for(schema_val) {
56 Ok(c) => c,
57 Err(e) => {
58 return Verdict {
59 status: VerdictStatus::Blocked,
60 reason_code: "E_SCHEMA_COMPILE".to_string(),
61 details: serde_json::json!({
62 "message": format!("Invalid schema for tool '{}': {}", tool_name, e)
63 }),
64 };
65 }
66 };
67
68 evaluate_schema(&compiled, tool_args)
70}
71
72pub fn evaluate_schema(compiled: &jsonschema::Validator, tool_args: &Value) -> Verdict {
74 if compiled.is_valid(tool_args) {
75 return Verdict {
76 status: VerdictStatus::Allowed,
77 reason_code: "OK".to_string(),
78 details: serde_json::json!({}),
79 };
80 }
81 let violations: Vec<Value> = compiled
82 .iter_errors(tool_args)
83 .map(|e| {
84 serde_json::json!({
85 "path": e.instance_path().to_string(),
86 "constraint": e.to_string(),
87 "message": e.to_string()
88 })
89 })
90 .collect();
91 Verdict {
92 status: VerdictStatus::Blocked,
93 reason_code: "E_ARG_SCHEMA".to_string(),
94 details: serde_json::json!({
95 "violations": violations
96 }),
97 }
98}
99
100pub struct PolicyState {
106 validators: HashMap<String, Result<jsonschema::Validator, String>>,
107 tool_names: Vec<String>,
108}
109
110impl PolicyState {
111 pub fn compile(policy: &Value) -> Self {
115 let mut validators = HashMap::new();
116 let mut tool_names = Vec::new();
117 if let Some(obj) = policy.as_object() {
118 for (tool, schema_val) in obj {
119 tool_names.push(tool.clone());
120 validators.insert(
121 tool.clone(),
122 jsonschema::validator_for(schema_val).map_err(|e| e.to_string()),
123 );
124 }
125 }
126 Self {
127 validators,
128 tool_names,
129 }
130 }
131
132 pub fn evaluate(&self, tool_name: &str, tool_args: &Value) -> Verdict {
134 match self.validators.get(tool_name) {
135 None => {
136 let mut message = format!("Tool '{}' not defined in policy", tool_name);
137 if let Some(match_) =
138 crate::errors::similarity::closest_prompt(tool_name, self.tool_names.iter())
139 {
140 message.push_str(&format!(". Did you mean '{}'?", match_.prompt));
141 }
142 Verdict {
143 status: VerdictStatus::Blocked,
144 reason_code: "E_POLICY_MISSING_TOOL".to_string(),
145 details: serde_json::json!({ "message": message }),
146 }
147 }
148 Some(Err(e)) => Verdict {
149 status: VerdictStatus::Blocked,
150 reason_code: "E_SCHEMA_COMPILE".to_string(),
151 details: serde_json::json!({
152 "message": format!("Invalid schema for tool '{}': {}", tool_name, e)
153 }),
154 },
155 Some(Ok(compiled)) => evaluate_schema(compiled, tool_args),
156 }
157 }
158}
159
160pub fn evaluate_sequence(policy_regex: &str, tool_names: &[String]) -> Verdict {
165 let trace_str = tool_names.join(" ");
169
170 let re = match regex::Regex::new(policy_regex) {
173 Ok(r) => r,
174 Err(e) => {
175 return Verdict {
176 status: VerdictStatus::Blocked,
177 reason_code: "E_POLICY_REGEX_INVALID".to_string(),
178 details: serde_json::json!({
179 "message": format!("Invalid regex policy '{}': {}", policy_regex, e)
180 }),
181 };
182 }
183 };
184
185 if re.is_match(&trace_str) {
187 Verdict {
188 status: VerdictStatus::Allowed,
189 reason_code: "OK".to_string(),
190 details: serde_json::json!({}),
191 }
192 } else {
193 Verdict {
194 status: VerdictStatus::Blocked,
195 reason_code: "E_SEQUENCE_VIOLATION".to_string(),
196 details: serde_json::json!({
197 "expected": policy_regex,
198 "found": trace_str
199 }),
200 }
201 }
202}