Skip to main content

agentic_contract/
file_format.rs

1//! `.acon` binary file format — portable contract store.
2
3use std::io::{Read, Write};
4use std::path::PathBuf;
5
6use chrono::Utc;
7use serde::{Deserialize, Serialize};
8
9use crate::approval::{ApprovalDecision, ApprovalRequest, ApprovalRule};
10use crate::condition::Condition;
11use crate::error::{ContractError, ContractResult};
12use crate::obligation::Obligation;
13use crate::policy::Policy;
14use crate::risk_limit::RiskLimit;
15use crate::violation::Violation;
16use crate::ContractId;
17
18/// Magic bytes identifying `.acon` files.
19pub const MAGIC: [u8; 4] = *b"ACON";
20
21/// Current format version.
22pub const VERSION: u32 = 1;
23
24/// File header (fixed size).
25#[derive(Debug, Clone)]
26#[repr(C)]
27pub struct FileHeader {
28    /// Magic bytes "ACON".
29    pub magic: [u8; 4],
30    /// Format version.
31    pub version: u32,
32    /// Flags (reserved).
33    pub flags: u32,
34    /// Number of policies.
35    pub policy_count: u64,
36    /// Number of risk limits.
37    pub risk_limit_count: u64,
38    /// Number of approval rules.
39    pub approval_rule_count: u64,
40    /// Number of approval requests.
41    pub approval_request_count: u64,
42    /// Number of conditions.
43    pub condition_count: u64,
44    /// Number of obligations.
45    pub obligation_count: u64,
46    /// Number of violations.
47    pub violation_count: u64,
48    /// File creation timestamp (Unix micros).
49    pub created_at: u64,
50    /// Last modified timestamp (Unix micros).
51    pub modified_at: u64,
52    /// BLAKE3 checksum.
53    pub checksum: [u8; 32],
54}
55
56impl Default for FileHeader {
57    fn default() -> Self {
58        Self::new()
59    }
60}
61
62impl FileHeader {
63    /// Create a new header with current timestamps.
64    pub fn new() -> Self {
65        let now = Utc::now().timestamp_micros() as u64;
66        Self {
67            magic: MAGIC,
68            version: VERSION,
69            flags: 0,
70            policy_count: 0,
71            risk_limit_count: 0,
72            approval_rule_count: 0,
73            approval_request_count: 0,
74            condition_count: 0,
75            obligation_count: 0,
76            violation_count: 0,
77            created_at: now,
78            modified_at: now,
79            checksum: [0; 32],
80        }
81    }
82
83    /// Write header to a writer.
84    pub fn write_to<W: Write>(&self, writer: &mut W) -> ContractResult<()> {
85        writer.write_all(&self.magic)?;
86        writer.write_all(&self.version.to_le_bytes())?;
87        writer.write_all(&self.flags.to_le_bytes())?;
88        writer.write_all(&self.policy_count.to_le_bytes())?;
89        writer.write_all(&self.risk_limit_count.to_le_bytes())?;
90        writer.write_all(&self.approval_rule_count.to_le_bytes())?;
91        writer.write_all(&self.approval_request_count.to_le_bytes())?;
92        writer.write_all(&self.condition_count.to_le_bytes())?;
93        writer.write_all(&self.obligation_count.to_le_bytes())?;
94        writer.write_all(&self.violation_count.to_le_bytes())?;
95        writer.write_all(&self.created_at.to_le_bytes())?;
96        writer.write_all(&self.modified_at.to_le_bytes())?;
97        writer.write_all(&self.checksum)?;
98        Ok(())
99    }
100
101    /// Read header from a reader.
102    pub fn read_from<R: Read>(reader: &mut R) -> ContractResult<Self> {
103        let mut magic = [0u8; 4];
104        reader.read_exact(&mut magic)?;
105
106        if magic != MAGIC {
107            return Err(ContractError::FileFormat(format!(
108                "Invalid magic bytes: {:?} (expected {:?}). File may be corrupted.",
109                magic, MAGIC
110            )));
111        }
112
113        let mut buf4 = [0u8; 4];
114        let mut buf8 = [0u8; 8];
115        let mut checksum = [0u8; 32];
116
117        reader.read_exact(&mut buf4)?;
118        let version = u32::from_le_bytes(buf4);
119
120        reader.read_exact(&mut buf4)?;
121        let flags = u32::from_le_bytes(buf4);
122
123        reader.read_exact(&mut buf8)?;
124        let policy_count = u64::from_le_bytes(buf8);
125
126        reader.read_exact(&mut buf8)?;
127        let risk_limit_count = u64::from_le_bytes(buf8);
128
129        reader.read_exact(&mut buf8)?;
130        let approval_rule_count = u64::from_le_bytes(buf8);
131
132        reader.read_exact(&mut buf8)?;
133        let approval_request_count = u64::from_le_bytes(buf8);
134
135        reader.read_exact(&mut buf8)?;
136        let condition_count = u64::from_le_bytes(buf8);
137
138        reader.read_exact(&mut buf8)?;
139        let obligation_count = u64::from_le_bytes(buf8);
140
141        reader.read_exact(&mut buf8)?;
142        let violation_count = u64::from_le_bytes(buf8);
143
144        reader.read_exact(&mut buf8)?;
145        let created_at = u64::from_le_bytes(buf8);
146
147        reader.read_exact(&mut buf8)?;
148        let modified_at = u64::from_le_bytes(buf8);
149
150        reader.read_exact(&mut checksum)?;
151
152        Ok(Self {
153            magic,
154            version,
155            flags,
156            policy_count,
157            risk_limit_count,
158            approval_rule_count,
159            approval_request_count,
160            condition_count,
161            obligation_count,
162            violation_count,
163            created_at,
164            modified_at,
165            checksum,
166        })
167    }
168}
169
170/// Entity types stored in file.
171#[derive(Debug, Clone, Copy, PartialEq, Eq)]
172#[repr(u8)]
173pub enum EntityType {
174    /// Policy rule.
175    Policy = 1,
176    /// Risk limit.
177    RiskLimit = 2,
178    /// Approval rule.
179    ApprovalRule = 3,
180    /// Approval request.
181    ApprovalRequest = 4,
182    /// Approval decision.
183    ApprovalDecision = 5,
184    /// Condition.
185    Condition = 6,
186    /// Obligation.
187    Obligation = 7,
188    /// Violation.
189    Violation = 8,
190}
191
192impl TryFrom<u8> for EntityType {
193    type Error = ContractError;
194
195    fn try_from(value: u8) -> Result<Self, Self::Error> {
196        match value {
197            1 => Ok(EntityType::Policy),
198            2 => Ok(EntityType::RiskLimit),
199            3 => Ok(EntityType::ApprovalRule),
200            4 => Ok(EntityType::ApprovalRequest),
201            5 => Ok(EntityType::ApprovalDecision),
202            6 => Ok(EntityType::Condition),
203            7 => Ok(EntityType::Obligation),
204            8 => Ok(EntityType::Violation),
205            _ => Err(ContractError::FileFormat(format!(
206                "Unknown entity type: {}",
207                value
208            ))),
209        }
210    }
211}
212
213/// In-memory representation of a `.acon` file.
214#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct ContractFile {
216    /// Policies.
217    #[serde(default)]
218    pub policies: Vec<Policy>,
219    /// Risk limits.
220    #[serde(default)]
221    pub risk_limits: Vec<RiskLimit>,
222    /// Approval rules.
223    #[serde(default)]
224    pub approval_rules: Vec<ApprovalRule>,
225    /// Approval requests.
226    #[serde(default)]
227    pub approval_requests: Vec<ApprovalRequest>,
228    /// Approval decisions.
229    #[serde(default)]
230    pub approval_decisions: Vec<ApprovalDecision>,
231    /// Conditions.
232    #[serde(default)]
233    pub conditions: Vec<Condition>,
234    /// Obligations.
235    #[serde(default)]
236    pub obligations: Vec<Obligation>,
237    /// Violations.
238    #[serde(default)]
239    pub violations: Vec<Violation>,
240    /// Source file path (not serialized in the file body).
241    #[serde(skip)]
242    pub path: Option<PathBuf>,
243}
244
245impl Default for ContractFile {
246    fn default() -> Self {
247        Self::new()
248    }
249}
250
251impl ContractFile {
252    /// Create a new empty contract file.
253    pub fn new() -> Self {
254        Self {
255            policies: vec![],
256            risk_limits: vec![],
257            approval_rules: vec![],
258            approval_requests: vec![],
259            approval_decisions: vec![],
260            conditions: vec![],
261            obligations: vec![],
262            violations: vec![],
263            path: None,
264        }
265    }
266
267    /// Create from a file path, loading if it exists.
268    pub fn open(path: impl Into<PathBuf>) -> ContractResult<Self> {
269        let path = path.into();
270        if path.exists() {
271            Self::load(&path)
272        } else {
273            let mut file = Self::new();
274            file.path = Some(path);
275            Ok(file)
276        }
277    }
278
279    /// Load from file.
280    pub fn load(path: &std::path::Path) -> ContractResult<Self> {
281        let mut reader = std::io::BufReader::new(std::fs::File::open(path)?);
282        let _header = FileHeader::read_from(&mut reader)?;
283
284        // Read JSON body after header
285        let mut body = String::new();
286        reader.read_to_string(&mut body)?;
287
288        let mut file: ContractFile = serde_json::from_str(&body)?;
289        file.path = Some(path.to_path_buf());
290        Ok(file)
291    }
292
293    /// Save to file.
294    pub fn save(&self) -> ContractResult<()> {
295        let path = self
296            .path
297            .as_ref()
298            .ok_or_else(|| ContractError::FileFormat("No file path set".to_string()))?;
299
300        // Ensure parent directory exists
301        if let Some(parent) = path.parent() {
302            std::fs::create_dir_all(parent)?;
303        }
304
305        let mut writer = std::io::BufWriter::new(std::fs::File::create(path)?);
306
307        let mut header = FileHeader::new();
308        header.policy_count = self.policies.len() as u64;
309        header.risk_limit_count = self.risk_limits.len() as u64;
310        header.approval_rule_count = self.approval_rules.len() as u64;
311        header.approval_request_count = self.approval_requests.len() as u64;
312        header.condition_count = self.conditions.len() as u64;
313        header.obligation_count = self.obligations.len() as u64;
314        header.violation_count = self.violations.len() as u64;
315
316        // Serialize body first to compute checksum
317        let body = serde_json::to_string(self)?;
318        header.checksum = *blake3::hash(body.as_bytes()).as_bytes();
319
320        header.write_to(&mut writer)?;
321        writer.write_all(body.as_bytes())?;
322
323        Ok(())
324    }
325
326    /// Get total entity count across all types.
327    pub fn total_entities(&self) -> usize {
328        self.policies.len()
329            + self.risk_limits.len()
330            + self.approval_rules.len()
331            + self.approval_requests.len()
332            + self.approval_decisions.len()
333            + self.conditions.len()
334            + self.obligations.len()
335            + self.violations.len()
336    }
337
338    /// Find a policy by ID.
339    pub fn find_policy(&self, id: ContractId) -> Option<&Policy> {
340        self.policies.iter().find(|p| p.id == id)
341    }
342
343    /// Find a risk limit by ID.
344    pub fn find_risk_limit(&self, id: ContractId) -> Option<&RiskLimit> {
345        self.risk_limits.iter().find(|r| r.id == id)
346    }
347
348    /// Find a mutable risk limit by ID.
349    pub fn find_risk_limit_mut(&mut self, id: ContractId) -> Option<&mut RiskLimit> {
350        self.risk_limits.iter_mut().find(|r| r.id == id)
351    }
352
353    /// Find an obligation by ID.
354    pub fn find_obligation(&self, id: ContractId) -> Option<&Obligation> {
355        self.obligations.iter().find(|o| o.id == id)
356    }
357
358    /// Find a mutable obligation by ID.
359    pub fn find_obligation_mut(&mut self, id: ContractId) -> Option<&mut Obligation> {
360        self.obligations.iter_mut().find(|o| o.id == id)
361    }
362}
363
364#[cfg(test)]
365mod tests {
366    use super::*;
367
368    #[test]
369    fn test_header_roundtrip() {
370        let header = FileHeader::new();
371        let mut buf = Vec::new();
372        header.write_to(&mut buf).unwrap();
373
374        let parsed = FileHeader::read_from(&mut buf.as_slice()).unwrap();
375        assert_eq!(parsed.magic, MAGIC);
376        assert_eq!(parsed.version, VERSION);
377    }
378
379    #[test]
380    fn test_contract_file_new() {
381        let file = ContractFile::new();
382        assert_eq!(file.total_entities(), 0);
383        assert!(file.policies.is_empty());
384    }
385
386    #[test]
387    fn test_bad_magic() {
388        let bad = b"BADMxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
389        let result = FileHeader::read_from(&mut bad.as_slice());
390        assert!(result.is_err());
391    }
392}