1use crate::policy::{ActionClass, TrustTier};
2use crate::tool::ToolDescriptor;
3use serde::{Deserialize, Serialize};
4use std::collections::BTreeMap;
5
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
7#[serde(rename_all = "snake_case")]
8pub enum PolicySourceKind {
9 OneLine,
10 Json,
11 HostProvided,
12}
13
14#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
15pub struct PolicySource {
16 pub kind: PolicySourceKind,
17 pub text: String,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
21pub struct PolicyContract {
22 pub id: String,
23 pub version: String,
24 pub source: PolicySource,
25 pub rules: Vec<PolicyRule>,
26 pub defaults: PolicyDefaults,
27 #[serde(default)]
28 pub diagnostics: Vec<PolicyDiagnostic>,
29 #[serde(default, skip_serializing_if = "Option::is_none")]
30 pub created_at: Option<String>,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
34pub struct PolicyDefaults {
35 pub deny_by_default: bool,
36 pub deny_missing_capabilities: bool,
37}
38
39impl Default for PolicyDefaults {
40 fn default() -> Self {
41 Self {
42 deny_by_default: true,
43 deny_missing_capabilities: true,
44 }
45 }
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
49pub struct PolicyRule {
50 pub id: String,
51 pub effect: PolicyEffect,
52 pub selector: PolicySelector,
53 #[serde(default, skip_serializing_if = "Option::is_none")]
54 pub condition: Option<PolicyCondition>,
55 pub reason: String,
56}
57
58#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
59#[serde(rename_all = "snake_case")]
60pub enum PolicyEffect {
61 Allow,
62 Deny,
63 RequireConfirmation,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
67pub struct PolicySelector {
68 #[serde(default)]
69 pub capabilities: Vec<CapabilityPattern>,
70 #[serde(default)]
71 pub resources: Vec<ResourcePattern>,
72 #[serde(default)]
73 pub actions: Vec<ActionPattern>,
74 #[serde(default)]
75 pub tools: Vec<ToolPattern>,
76 #[serde(default)]
77 pub scopes: Vec<ScopePattern>,
78 #[serde(default)]
79 pub trust_tiers: Vec<TrustTier>,
80 #[serde(default)]
81 pub action_classes: Vec<ActionClass>,
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
85pub struct CapabilityPattern(pub String);
86
87#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
88pub struct ResourcePattern(pub String);
89
90#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
91pub struct ActionPattern(pub String);
92
93#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
94pub struct ToolPattern(pub String);
95
96#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
97pub struct ScopePattern(pub String);
98
99#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
100pub struct PolicyCondition {
101 #[serde(default, skip_serializing_if = "Option::is_none")]
102 pub requires_confirmation: Option<bool>,
103 #[serde(default, skip_serializing_if = "Option::is_none")]
104 pub max_payload_bytes: Option<usize>,
105 #[serde(default)]
106 pub allowed_hosts: Vec<String>,
107 #[serde(default, skip_serializing_if = "Option::is_none")]
108 pub trust_tier_at_most: Option<TrustTier>,
109 #[serde(default, skip_serializing_if = "Option::is_none")]
110 pub action_class_is: Option<ActionClass>,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
114pub struct PolicyDiagnostic {
115 pub severity: PolicyDiagnosticSeverity,
116 pub code: PolicyDiagnosticCode,
117 pub message: String,
118}
119
120#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
121#[serde(rename_all = "snake_case")]
122pub enum PolicyDiagnosticSeverity {
123 Info,
124 Warning,
125 Error,
126}
127
128#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
129#[serde(rename_all = "snake_case")]
130pub enum PolicyDiagnosticCode {
131 UnknownResource,
132 UnknownOperation,
133 AmbiguousVerb,
134 ConflictingRule,
135 OverBroadAllow,
136 MissingCatalogMapping,
137 DenyOverridesAllow,
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
141pub struct MatchedPolicyRule {
142 pub rule_id: String,
143 pub effect: PolicyEffect,
144 pub reason: String,
145}
146
147#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
148#[serde(rename_all = "snake_case")]
149pub enum PolicyEvaluationDecision {
150 Allowed,
151 Denied,
152 ConfirmationRequired,
153 NoMatchingAllow,
154 DescriptorMissingCapabilities,
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
158pub struct PolicyEvaluation {
159 pub decision: PolicyEvaluationDecision,
160 #[serde(default)]
161 pub matched_rules: Vec<MatchedPolicyRule>,
162 pub explanation: String,
163}
164
165impl PolicyContract {
166 pub fn evaluate_tool(&self, tool: &ToolDescriptor, confirmed: bool) -> PolicyEvaluation {
167 if self.defaults.deny_missing_capabilities && tool.capabilities.is_empty() {
168 return PolicyEvaluation {
169 decision: PolicyEvaluationDecision::DescriptorMissingCapabilities,
170 matched_rules: Vec::new(),
171 explanation: format!(
172 "Denied because '{}' does not declare semantic capability metadata.",
173 tool.name
174 ),
175 };
176 }
177
178 let mut matches = Vec::new();
179 for rule in &self.rules {
180 if rule.selector.matches_tool(tool) {
181 matches.push(MatchedPolicyRule {
182 rule_id: rule.id.clone(),
183 effect: rule.effect,
184 reason: rule.reason.clone(),
185 });
186 }
187 }
188
189 if let Some(rule) = matches
190 .iter()
191 .find(|rule| matches!(rule.effect, PolicyEffect::Deny))
192 .cloned()
193 {
194 return PolicyEvaluation {
195 decision: PolicyEvaluationDecision::Denied,
196 matched_rules: matches,
197 explanation: format!("Denied by policy rule '{}': {}", rule.rule_id, rule.reason),
198 };
199 }
200
201 if let Some(rule) = matches
202 .iter()
203 .find(|rule| matches!(rule.effect, PolicyEffect::RequireConfirmation))
204 .cloned()
205 && !confirmed
206 {
207 return PolicyEvaluation {
208 decision: PolicyEvaluationDecision::ConfirmationRequired,
209 matched_rules: matches,
210 explanation: format!(
211 "Confirmation required by policy rule '{}': {}",
212 rule.rule_id, rule.reason
213 ),
214 };
215 }
216
217 if matches
218 .iter()
219 .any(|rule| matches!(rule.effect, PolicyEffect::Allow))
220 {
221 return PolicyEvaluation {
222 decision: PolicyEvaluationDecision::Allowed,
223 matched_rules: matches,
224 explanation: format!("Allowed by policy contract '{}'.", self.id),
225 };
226 }
227
228 if self.defaults.deny_by_default {
229 PolicyEvaluation {
230 decision: PolicyEvaluationDecision::NoMatchingAllow,
231 matched_rules: matches,
232 explanation: format!(
233 "Denied because '{}' did not match any allow rule in policy contract '{}'.",
234 tool.name, self.id
235 ),
236 }
237 } else {
238 PolicyEvaluation {
239 decision: PolicyEvaluationDecision::Allowed,
240 matched_rules: matches,
241 explanation: format!(
242 "Allowed by policy contract '{}' default allow behavior.",
243 self.id
244 ),
245 }
246 }
247 }
248}
249
250impl PolicySelector {
251 pub fn matches_tool(&self, tool: &ToolDescriptor) -> bool {
252 self.matches_tools(tool)
253 && self.matches_action_classes(tool)
254 && self.matches_trust_tiers(tool)
255 && self.matches_scopes(tool)
256 && self.matches_capabilities(tool)
257 && self.matches_resources(tool)
258 && self.matches_actions(tool)
259 }
260
261 fn matches_tools(&self, tool: &ToolDescriptor) -> bool {
262 self.tools.is_empty()
263 || self
264 .tools
265 .iter()
266 .any(|pattern| pattern_matches(&pattern.0, &tool.name))
267 }
268
269 fn matches_action_classes(&self, tool: &ToolDescriptor) -> bool {
270 self.action_classes.is_empty() || self.action_classes.contains(&tool.action_class)
271 }
272
273 fn matches_trust_tiers(&self, tool: &ToolDescriptor) -> bool {
274 self.trust_tiers.is_empty() || self.trust_tiers.contains(&tool.trust_tier)
275 }
276
277 fn matches_scopes(&self, tool: &ToolDescriptor) -> bool {
278 self.scopes.is_empty()
279 || self.scopes.iter().any(|pattern| {
280 tool.scopes
281 .iter()
282 .any(|scope| pattern_matches(&pattern.0, scope))
283 })
284 }
285
286 fn matches_capabilities(&self, tool: &ToolDescriptor) -> bool {
287 self.capabilities.is_empty()
288 || self.capabilities.iter().any(|pattern| {
289 tool.capabilities
290 .iter()
291 .any(|capability| pattern_matches(&pattern.0, &capability.id))
292 })
293 }
294
295 fn matches_resources(&self, tool: &ToolDescriptor) -> bool {
296 self.resources.is_empty()
297 || self.resources.iter().any(|pattern| {
298 tool.capabilities.iter().any(|capability| {
299 pattern_matches(&pattern.0, &capability.resource_path.join("."))
300 || pattern_matches(&pattern.0, &capability.resource)
301 })
302 })
303 }
304
305 fn matches_actions(&self, tool: &ToolDescriptor) -> bool {
306 self.actions.is_empty()
307 || self.actions.iter().any(|pattern| {
308 tool.capabilities
309 .iter()
310 .any(|capability| pattern_matches(&pattern.0, &capability.operation))
311 })
312 }
313}
314
315pub fn pattern_matches(pattern: &str, value: &str) -> bool {
316 let pattern = pattern.trim();
317 let value = value.trim();
318 if pattern == "*" || pattern == value {
319 return true;
320 }
321 if let Some(prefix) = pattern.strip_suffix(".*") {
322 return value == prefix || value.starts_with(&format!("{prefix}."));
323 }
324 if let Some(suffix) = pattern.strip_prefix("*.") {
325 return value == suffix || value.ends_with(&format!(".{suffix}"));
326 }
327 false
328}
329
330pub fn matched_rules_metadata(rules: &[MatchedPolicyRule]) -> BTreeMap<String, String> {
331 let mut metadata = BTreeMap::new();
332 if !rules.is_empty() {
333 metadata.insert(
334 "policy.matched_rules".to_string(),
335 rules
336 .iter()
337 .map(|rule| rule.rule_id.as_str())
338 .collect::<Vec<_>>()
339 .join(","),
340 );
341 }
342 metadata
343}