1use serde::{Deserialize, Serialize};
7use std::fmt;
8use thiserror::Error;
9
10pub const CURRENT_POLICY_ABI_VERSION: u32 = 1;
13
14#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
16pub struct PolicyAbiVersion {
17 pub major: u32,
19 pub minor: u32,
21 pub patch: u32,
23}
24
25impl PolicyAbiVersion {
26 pub fn new(major: u32, minor: u32, patch: u32) -> Self {
28 Self {
29 major,
30 minor,
31 patch,
32 }
33 }
34
35 pub fn current() -> Self {
37 Self::new(CURRENT_POLICY_ABI_VERSION, 0, 0)
38 }
39
40 pub fn is_compatible(&self) -> bool {
42 self.major == CURRENT_POLICY_ABI_VERSION
43 }
44
45 pub fn is_current(&self) -> bool {
47 *self == Self::current()
48 }
49
50 pub fn to_version_string(&self) -> String {
52 format!("{}.{}.{}", self.major, self.minor, self.patch)
53 }
54
55 pub fn from_version_string(version: &str) -> Result<Self, PolicyAbiError> {
57 let parts: Vec<&str> = version.split('.').collect();
58 if parts.len() != 3 {
59 return Err(PolicyAbiError::InvalidVersionFormat(version.to_string()));
60 }
61
62 let major = parts[0]
63 .parse::<u32>()
64 .map_err(|_| PolicyAbiError::InvalidVersionFormat(version.to_string()))?;
65 let minor = parts[1]
66 .parse::<u32>()
67 .map_err(|_| PolicyAbiError::InvalidVersionFormat(version.to_string()))?;
68 let patch = parts[2]
69 .parse::<u32>()
70 .map_err(|_| PolicyAbiError::InvalidVersionFormat(version.to_string()))?;
71
72 Ok(Self::new(major, minor, patch))
73 }
74}
75
76impl fmt::Display for PolicyAbiVersion {
77 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78 write!(f, "{}", self.to_version_string())
79 }
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct CapabilityBundleHeader {
85 pub abi_version: PolicyAbiVersion,
87 pub bundle_version: String,
89 pub created_at: chrono::DateTime<chrono::Utc>,
91 pub content_digest: String,
93 pub metadata: CapabilityBundleMetadata,
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct CapabilityBundleMetadata {
100 pub name: String,
102 pub description: Option<String>,
104 pub organization: Option<String>,
106 pub git_commit: Option<String>,
108 pub build_info: Option<String>,
110}
111
112#[derive(Debug, Error)]
114pub enum PolicyAbiError {
115 #[error(
116 "Incompatible Policy ABI version: bundle={bundle_version}, supported={supported_version}"
117 )]
118 IncompatibleVersion {
119 bundle_version: PolicyAbiVersion,
120 supported_version: PolicyAbiVersion,
121 },
122
123 #[error("Invalid version format: {0}")]
124 InvalidVersionFormat(String),
125
126 #[error("Missing ABI version in capability bundle")]
127 MissingAbiVersion,
128
129 #[error("Capability bundle validation failed: {0}")]
130 ValidationFailed(String),
131
132 #[error("Capability bundle deserialization failed: {0}")]
133 DeserializationFailed(String),
134}
135
136pub struct PolicyAbiValidator;
138
139impl PolicyAbiValidator {
140 pub fn validate_startup_compatibility(
146 bundle_json: &str,
147 ) -> Result<CapabilityBundleHeader, PolicyAbiError> {
148 let bundle_value: serde_json::Value = serde_json::from_str(bundle_json)
150 .map_err(|e| PolicyAbiError::DeserializationFailed(e.to_string()))?;
151
152 let abi_version = Self::extract_abi_version(&bundle_value)?;
154
155 if !abi_version.is_compatible() {
157 return Err(PolicyAbiError::IncompatibleVersion {
158 bundle_version: abi_version.clone(),
159 supported_version: PolicyAbiVersion::current(),
160 });
161 }
162
163 let header: CapabilityBundleHeader = serde_json::from_value(
165 bundle_value
166 .get("header")
167 .ok_or(PolicyAbiError::MissingAbiVersion)?
168 .clone(),
169 )
170 .map_err(|e| PolicyAbiError::DeserializationFailed(e.to_string()))?;
171
172 Ok(header)
173 }
174
175 fn extract_abi_version(
177 bundle_value: &serde_json::Value,
178 ) -> Result<PolicyAbiVersion, PolicyAbiError> {
179 let header = bundle_value
180 .get("header")
181 .ok_or(PolicyAbiError::MissingAbiVersion)?;
182
183 let abi_version: PolicyAbiVersion = serde_json::from_value(
184 header
185 .get("abi_version")
186 .ok_or(PolicyAbiError::MissingAbiVersion)?
187 .clone(),
188 )
189 .map_err(|e| PolicyAbiError::DeserializationFailed(e.to_string()))?;
190
191 Ok(abi_version)
192 }
193
194 pub fn generate_abi_hash() -> String {
199 use sha2::{Digest, Sha256};
200
201 let abi_repr = format!(
203 "POLICY_ABI_V{}_CURRENT_VERSION_{}_FIELDS_{}",
204 CURRENT_POLICY_ABI_VERSION,
205 PolicyAbiVersion::current().to_version_string(),
206 "header,abi_version,bundle_version,created_at,content_digest,metadata"
207 );
208
209 let mut hasher = Sha256::new();
210 hasher.update(abi_repr.as_bytes());
211 format!("{:x}", hasher.finalize())
212 }
213
214 pub fn validate_abi_stability(old_hash: &str, new_hash: &str) -> Result<(), PolicyAbiError> {
216 if old_hash != new_hash {
217 return Err(PolicyAbiError::ValidationFailed(format!(
218 "ABI hash mismatch: expected {} but got {}. This indicates a breaking change to the Policy ABI.",
219 old_hash, new_hash
220 )));
221 }
222 Ok(())
223 }
224}
225
226pub trait CapabilityBundleValidation {
228 fn validate_abi_compatibility(&self) -> Result<(), PolicyAbiError>;
230}
231
232impl CapabilityBundleValidation for String {
234 fn validate_abi_compatibility(&self) -> Result<(), PolicyAbiError> {
235 PolicyAbiValidator::validate_startup_compatibility(self)?;
236 Ok(())
237 }
238}
239
240impl CapabilityBundleValidation for &str {
241 fn validate_abi_compatibility(&self) -> Result<(), PolicyAbiError> {
242 PolicyAbiValidator::validate_startup_compatibility(self)?;
243 Ok(())
244 }
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250 use serde_json::json;
251
252 #[test]
253 fn test_policy_abi_version_creation() {
254 let version = PolicyAbiVersion::new(1, 2, 3);
255 assert_eq!(version.major, 1);
256 assert_eq!(version.minor, 2);
257 assert_eq!(version.patch, 3);
258 assert_eq!(version.to_version_string(), "1.2.3");
259 }
260
261 #[test]
262 fn test_current_version() {
263 let current = PolicyAbiVersion::current();
264 assert_eq!(current.major, CURRENT_POLICY_ABI_VERSION);
265 assert_eq!(current.minor, 0);
266 assert_eq!(current.patch, 0);
267 }
268
269 #[test]
270 fn test_version_compatibility() {
271 let current = PolicyAbiVersion::current();
272 assert!(current.is_compatible());
273 assert!(current.is_current());
274
275 let incompatible = PolicyAbiVersion::new(CURRENT_POLICY_ABI_VERSION + 1, 0, 0);
276 assert!(!incompatible.is_compatible());
277 assert!(!incompatible.is_current());
278
279 let older_compatible = PolicyAbiVersion::new(CURRENT_POLICY_ABI_VERSION, 1, 5);
280 assert!(older_compatible.is_compatible());
281 assert!(!older_compatible.is_current());
282 }
283
284 #[test]
285 fn test_version_string_parsing() {
286 let version = PolicyAbiVersion::from_version_string("2.1.5").unwrap();
287 assert_eq!(version.major, 2);
288 assert_eq!(version.minor, 1);
289 assert_eq!(version.patch, 5);
290
291 assert!(PolicyAbiVersion::from_version_string("1.2").is_err());
293 assert!(PolicyAbiVersion::from_version_string("invalid").is_err());
294 assert!(PolicyAbiVersion::from_version_string("1.x.3").is_err());
295 }
296
297 #[test]
298 fn test_compatible_bundle_validation() {
299 let current_version = PolicyAbiVersion::current();
300 let bundle_json = json!({
301 "header": {
302 "abi_version": current_version,
303 "bundle_version": "1.0.0",
304 "created_at": "2024-01-01T00:00:00Z",
305 "content_digest": "abc123",
306 "metadata": {
307 "name": "test-bundle",
308 "description": "Test capability bundle",
309 "organization": "Smith Team",
310 "git_commit": "abc123",
311 "build_info": "test-build"
312 }
313 },
314 "atoms": {},
315 "macros": {},
316 "playbooks": {}
317 })
318 .to_string();
319
320 let result = PolicyAbiValidator::validate_startup_compatibility(&bundle_json);
321 assert!(result.is_ok());
322
323 let header = result.unwrap();
324 assert_eq!(header.abi_version, current_version);
325 assert_eq!(header.metadata.name, "test-bundle");
326 }
327
328 #[test]
329 fn test_incompatible_bundle_validation() {
330 let incompatible_version = PolicyAbiVersion::new(CURRENT_POLICY_ABI_VERSION + 1, 0, 0);
331 let bundle_json = json!({
332 "header": {
333 "abi_version": incompatible_version,
334 "bundle_version": "2.0.0",
335 "created_at": "2024-01-01T00:00:00Z",
336 "content_digest": "def456",
337 "metadata": {
338 "name": "future-bundle"
339 }
340 }
341 })
342 .to_string();
343
344 let result = PolicyAbiValidator::validate_startup_compatibility(&bundle_json);
345 assert!(result.is_err());
346
347 match result.unwrap_err() {
348 PolicyAbiError::IncompatibleVersion {
349 bundle_version,
350 supported_version,
351 } => {
352 assert_eq!(bundle_version, incompatible_version);
353 assert_eq!(supported_version, PolicyAbiVersion::current());
354 }
355 _ => panic!("Expected IncompatibleVersion error"),
356 }
357 }
358
359 #[test]
360 fn test_missing_abi_version() {
361 let bundle_json = json!({
362 "header": {
363 "bundle_version": "1.0.0"
364 }
366 })
367 .to_string();
368
369 let result = PolicyAbiValidator::validate_startup_compatibility(&bundle_json);
370 assert!(result.is_err());
371 assert!(matches!(
372 result.unwrap_err(),
373 PolicyAbiError::MissingAbiVersion
374 ));
375 }
376
377 #[test]
378 fn test_abi_hash_generation() {
379 let hash1 = PolicyAbiValidator::generate_abi_hash();
380 let hash2 = PolicyAbiValidator::generate_abi_hash();
381
382 assert_eq!(hash1, hash2);
384
385 assert_eq!(hash1.len(), 64);
387 assert!(hash1.chars().all(|c| c.is_ascii_hexdigit()));
388 }
389
390 #[test]
391 fn test_abi_stability_validation() {
392 let hash = "abc123def456";
393
394 assert!(PolicyAbiValidator::validate_abi_stability(hash, hash).is_ok());
396
397 let result = PolicyAbiValidator::validate_abi_stability(hash, "different");
399 assert!(result.is_err());
400 assert!(matches!(
401 result.unwrap_err(),
402 PolicyAbiError::ValidationFailed(_)
403 ));
404 }
405
406 #[test]
407 fn test_bundle_validation_trait() {
408 let current_version = PolicyAbiVersion::current();
409 let bundle_json = json!({
410 "header": {
411 "abi_version": current_version,
412 "bundle_version": "1.0.0",
413 "created_at": "2024-01-01T00:00:00Z",
414 "content_digest": "test123",
415 "metadata": {
416 "name": "trait-test-bundle"
417 }
418 }
419 })
420 .to_string();
421
422 assert!(bundle_json.validate_abi_compatibility().is_ok());
424 assert!(bundle_json.as_str().validate_abi_compatibility().is_ok());
425 }
426
427 #[test]
428 fn test_capability_bundle_header_serialization() {
429 let header = CapabilityBundleHeader {
430 abi_version: PolicyAbiVersion::current(),
431 bundle_version: "1.0.0".to_string(),
432 created_at: chrono::Utc::now(),
433 content_digest: "test-digest".to_string(),
434 metadata: CapabilityBundleMetadata {
435 name: "test-bundle".to_string(),
436 description: Some("Test description".to_string()),
437 organization: Some("Smith Team".to_string()),
438 git_commit: Some("abc123".to_string()),
439 build_info: Some("test-build".to_string()),
440 },
441 };
442
443 let json = serde_json::to_string(&header).unwrap();
445 let deserialized: CapabilityBundleHeader = serde_json::from_str(&json).unwrap();
446
447 assert_eq!(deserialized.abi_version, header.abi_version);
448 assert_eq!(deserialized.bundle_version, header.bundle_version);
449 assert_eq!(deserialized.metadata.name, header.metadata.name);
450 }
451}