Skip to main content

agentic_planning/
audit.rs

1use serde::{Deserialize, Serialize};
2use uuid::Uuid;
3
4use crate::Timestamp;
5
6/// Actions that can be recorded in the audit log.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
8pub enum AuditAction {
9    Create,
10    Read,
11    Update,
12    Delete,
13    StatusChange,
14    Crystallize,
15    Fulfill,
16    Break,
17}
18
19/// The entity type that was acted upon.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
21pub enum AuditEntityType {
22    Goal,
23    Decision,
24    Commitment,
25    Dream,
26    Federation,
27}
28
29/// A single audit log entry recording an operation on the planning engine.
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct AuditEntry {
32    pub timestamp: Timestamp,
33    pub session_id: Uuid,
34    pub operation: String,
35    pub entity_type: AuditEntityType,
36    pub entity_id: String,
37    pub action: AuditAction,
38    pub details: Option<String>,
39    pub success: bool,
40    pub error: Option<String>,
41}
42
43/// Append-only audit log for tracking all planning engine mutations.
44#[derive(Debug, Clone, Default, Serialize, Deserialize)]
45pub struct AuditLog {
46    pub entries: Vec<AuditEntry>,
47}
48
49impl AuditLog {
50    pub fn new() -> Self {
51        Self {
52            entries: Vec::new(),
53        }
54    }
55
56    pub fn append(&mut self, entry: AuditEntry) {
57        self.entries.push(entry);
58    }
59
60    #[allow(clippy::too_many_arguments)]
61    pub fn record(
62        &mut self,
63        session_id: Uuid,
64        operation: impl Into<String>,
65        entity_type: AuditEntityType,
66        entity_id: impl Into<String>,
67        action: AuditAction,
68        success: bool,
69        details: Option<String>,
70        error: Option<String>,
71    ) {
72        self.entries.push(AuditEntry {
73            timestamp: Timestamp::now(),
74            session_id,
75            operation: operation.into(),
76            entity_type,
77            entity_id: entity_id.into(),
78            action,
79            details,
80            success,
81            error,
82        });
83    }
84
85    pub fn len(&self) -> usize {
86        self.entries.len()
87    }
88
89    pub fn is_empty(&self) -> bool {
90        self.entries.is_empty()
91    }
92
93    pub fn entries_for_entity(&self, entity_id: &str) -> Vec<&AuditEntry> {
94        self.entries
95            .iter()
96            .filter(|e| e.entity_id == entity_id)
97            .collect()
98    }
99
100    pub fn entries_by_action(&self, action: AuditAction) -> Vec<&AuditEntry> {
101        self.entries.iter().filter(|e| e.action == action).collect()
102    }
103
104    pub fn entries_since(&self, since: Timestamp) -> Vec<&AuditEntry> {
105        self.entries
106            .iter()
107            .filter(|e| e.timestamp.0 >= since.0)
108            .collect()
109    }
110
111    pub fn failures(&self) -> Vec<&AuditEntry> {
112        self.entries.iter().filter(|e| !e.success).collect()
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn test_audit_log_append_and_query() {
122        let mut log = AuditLog::new();
123        assert!(log.is_empty());
124
125        let session = Uuid::new_v4();
126        log.record(
127            session,
128            "create_goal",
129            AuditEntityType::Goal,
130            "goal-123",
131            AuditAction::Create,
132            true,
133            Some("Created goal".into()),
134            None,
135        );
136
137        assert_eq!(log.len(), 1);
138        assert!(!log.is_empty());
139        assert_eq!(log.entries_for_entity("goal-123").len(), 1);
140        assert_eq!(log.entries_by_action(AuditAction::Create).len(), 1);
141        assert!(log.failures().is_empty());
142    }
143
144    #[test]
145    fn test_audit_log_failures() {
146        let mut log = AuditLog::new();
147        let session = Uuid::new_v4();
148
149        log.record(
150            session,
151            "create_goal",
152            AuditEntityType::Goal,
153            "goal-1",
154            AuditAction::Create,
155            true,
156            None,
157            None,
158        );
159
160        log.record(
161            session,
162            "update_goal",
163            AuditEntityType::Goal,
164            "goal-2",
165            AuditAction::Update,
166            false,
167            None,
168            Some("Goal not found".into()),
169        );
170
171        assert_eq!(log.len(), 2);
172        assert_eq!(log.failures().len(), 1);
173        assert_eq!(log.failures()[0].entity_id, "goal-2");
174    }
175
176    #[test]
177    fn test_audit_log_entries_since() {
178        let mut log = AuditLog::new();
179        let session = Uuid::new_v4();
180        let before = Timestamp::now();
181
182        log.record(
183            session,
184            "create_decision",
185            AuditEntityType::Decision,
186            "dec-1",
187            AuditAction::Create,
188            true,
189            None,
190            None,
191        );
192
193        log.record(
194            session,
195            "crystallize_decision",
196            AuditEntityType::Decision,
197            "dec-1",
198            AuditAction::Crystallize,
199            true,
200            None,
201            None,
202        );
203
204        assert_eq!(log.entries_since(before).len(), 2);
205        assert_eq!(log.entries_by_action(AuditAction::Crystallize).len(), 1);
206    }
207
208    #[test]
209    fn test_audit_entity_types_and_actions() {
210        let mut log = AuditLog::new();
211        let session = Uuid::new_v4();
212
213        let actions = [
214            (AuditAction::Create, AuditEntityType::Goal),
215            (AuditAction::Read, AuditEntityType::Decision),
216            (AuditAction::Update, AuditEntityType::Commitment),
217            (AuditAction::Delete, AuditEntityType::Dream),
218            (AuditAction::StatusChange, AuditEntityType::Goal),
219            (AuditAction::Crystallize, AuditEntityType::Decision),
220            (AuditAction::Fulfill, AuditEntityType::Commitment),
221            (AuditAction::Break, AuditEntityType::Federation),
222        ];
223
224        for (i, (action, entity_type)) in actions.iter().enumerate() {
225            log.record(
226                session,
227                format!("op_{}", i),
228                *entity_type,
229                format!("id-{}", i),
230                *action,
231                true,
232                None,
233                None,
234            );
235        }
236
237        assert_eq!(log.len(), 8);
238        assert_eq!(log.entries_by_action(AuditAction::Create).len(), 1);
239        assert_eq!(log.entries_by_action(AuditAction::Fulfill).len(), 1);
240    }
241}