oxios_kernel/audit_persistence.rs
1//! StateStore-backed AuditPersistence for oxi-sdk's AuditTrail.
2//!
3//! Bridges the `oxi_sdk::observability::AuditPersistence` trait to oxios's
4//! filesystem-based `StateStore`. The trail JSON is written to
5//! `<base_path>/audit/trail.json`, matching the legacy layout used before
6//! the SDK migration (RFC-014 Phase F).
7//!
8//! See: <https://github.com/a7garden/oxios/blob/main/docs/rfc-014/phase-f-audit-trail.md>
9
10use anyhow::Result;
11use oxi_sdk::observability::{AuditPersistence, TrailEntry};
12
13use crate::state_store::StateStore;
14
15impl AuditPersistence for StateStore {
16 fn save(&self, entries: &[TrailEntry]) -> Result<()> {
17 let path = self.audit_path();
18 if let Some(parent) = path.parent() {
19 std::fs::create_dir_all(parent)?;
20 }
21 let json = serde_json::to_string_pretty(entries)?;
22
23 // Durable write: write to a unique temp file, fsync it, atomically
24 // rename, then best-effort fsync the directory. Without the fsync
25 // steps, a crash (OOM/SIGKILL/power loss) between the write and the
26 // rename's metadata commit can leave trail.json truncated or empty,
27 // losing the entire hash-chained audit trail. (state-area F3.)
28 let temp_path = path
29 .parent()
30 .unwrap_or(std::path::Path::new("."))
31 .join(format!(
32 "trail.json.{}.{}.tmp",
33 std::process::id(),
34 uuid::Uuid::new_v4()
35 ));
36 {
37 use std::io::Write;
38 let mut file = std::fs::File::create(&temp_path)?;
39 file.write_all(json.as_bytes())?;
40 file.sync_all()?;
41 }
42 std::fs::rename(&temp_path, &path)?;
43 if let Some(parent) = path.parent()
44 && let Ok(dir) = std::fs::File::open(parent)
45 {
46 // Best-effort directory fsync so the rename is durable.
47 // Ignore errors: not all platforms/fs support dir fsync,
48 // and we've already done the file fsync + rename.
49 let _ = dir.sync_all();
50 }
51 Ok(())
52 }
53
54 fn load(&self) -> Result<Vec<TrailEntry>> {
55 let path = self.audit_path();
56 if !path.exists() {
57 return Ok(Vec::new());
58 }
59 let json = std::fs::read_to_string(&path)?;
60 let entries: Vec<TrailEntry> = serde_json::from_str(&json)?;
61 Ok(entries)
62 }
63}
64
65impl StateStore {
66 /// Path to the persisted audit trail file.
67 ///
68 /// Layout: `<base_path>/audit/trail.json`
69 fn audit_path(&self) -> std::path::PathBuf {
70 self.base_path.join("audit").join("trail.json")
71 }
72}