gonzalo_domain/fleet/
audit.rs1use super::keys::{self, FleetKeyError};
6use super::{AUDIT_ENTRIES_COLLECTION, FLEET_AUDIT_NAMESPACE, FleetActor};
7use crate::codec::RecordCodec;
8use gonzalo_core::{RecordKey, RecordKind};
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13pub enum AuditResult {
14 Succeeded,
15 Denied,
16 Failed(String),
17}
18
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
24pub struct AuditEntry {
25 pub actor: FleetActor,
26 pub action: String,
28 pub target: String,
30 pub at: i64,
31 pub surface_ref: Option<String>,
33 pub result: AuditResult,
34}
35impl RecordCodec for AuditEntry {}
36impl AuditEntry {
37 pub const KIND: RecordKind = RecordKind::AuditEntry;
38
39 pub fn key(&self, nonce: &str) -> Result<RecordKey, FleetKeyError> {
41 Ok(RecordKey::new(
42 FLEET_AUDIT_NAMESPACE,
43 AUDIT_ENTRIES_COLLECTION,
44 keys::audit_id(self.at, nonce)?,
45 ))
46 }
47}
48
49#[cfg(test)]
50mod tests {
51 use super::*;
52 use crate::fleet::Authenticator;
53
54 #[test]
55 fn audit_entry_roundtrips_and_keys() {
56 let e = AuditEntry {
57 actor: FleetActor::Unlinked {
58 authenticator: Authenticator::Discord,
59 subject: "1234".into(),
60 },
61 action: "spawn".into(),
62 target: "caliban-ai/gonzalo".into(),
63 at: 1_700_000_000_000,
64 surface_ref: Some("https://discord.com/channels/1/2/3".into()),
65 result: AuditResult::Denied,
66 };
67 assert_eq!(AuditEntry::from_body(&e.to_body().unwrap()).unwrap(), e);
68 assert_eq!(AuditEntry::KIND, RecordKind::AuditEntry);
69 assert_eq!(
70 e.key("n1").unwrap(),
71 RecordKey::new("fleet-audit", "entries", "1700000000000-n1")
72 );
73 assert!(e.key("bad nonce").is_err());
74 }
75}