use serde::{Deserialize, Serialize};
use crate::id::MemoryId;
use crate::revision::{LogicalMemoryId, RevisionId};
use crate::timestamp::Timestamp;
use crate::types::AgentId;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum AuditAction {
ShareMemory {
memory_id: MemoryId,
source_namespace: String,
target_namespace: String,
},
PromoteToShared { memory_id: MemoryId },
Quarantine {
memory_id: MemoryId,
anomaly_score: f32,
reason: String,
},
QuarantineApproved { memory_id: MemoryId },
QuarantineRejected { memory_id: MemoryId },
QuarantineRolledBack {
memory_id: MemoryId,
removed_memory_ids: Vec<MemoryId>,
restored_memory_ids: Vec<MemoryId>,
reason: String,
},
CrossAgentMerge {
source_ids: Vec<MemoryId>,
result_id: MemoryId,
source_agents: Vec<AgentId>,
},
AgentRegistered { agent_id: AgentId },
AgentDeregistered { agent_id: AgentId },
NamespaceCreated { namespace: String },
NamespaceDeleted { namespace: String },
AgentAddedToTeam { agent_id: AgentId, team: String },
AgentRemovedFromTeam { agent_id: AgentId, team: String },
AgentRateLimited {
agent_id: AgentId,
quarantined_count: usize,
window_seconds: u64,
},
AgentPurged {
agent_id: AgentId,
episodic_deleted: usize,
semantic_deleted: usize,
procedural_deleted: usize,
edges_removed: usize,
},
EdgeLimitExceeded {
node_id: MemoryId,
current_count: usize,
limit: usize,
},
AccessGranted {
action: String,
realm: String,
namespace: String,
policy_ids: Vec<String>,
},
AccessDenied {
action: String,
realm: String,
namespace: String,
reasons: Vec<String>,
policy_ids: Vec<String>,
},
PolicyChanged {
policy_name: String,
change_type: String,
#[serde(default)]
policy_content: String,
},
BeliefOverride {
logical_memory_id: LogicalMemoryId,
prior_revision_id: RevisionId,
override_revision_id: RevisionId,
namespace: String,
reason: String,
},
BeliefReconcileApproved {
conflict_id: String,
action: String,
namespace: String,
logical_memory_ids: Vec<LogicalMemoryId>,
applied_memory_ids: Vec<MemoryId>,
rationale: String,
},
AbaResolution {
winner_id: MemoryId,
loser_id: MemoryId,
revised_confidence: f32,
reason: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditEntry {
pub id: MemoryId,
pub timestamp: Timestamp,
pub actor: Option<AgentId>,
pub action: AuditAction,
}
impl AuditEntry {
pub fn new(actor: Option<AgentId>, action: AuditAction) -> Self {
Self {
id: MemoryId::new(),
timestamp: Timestamp::now(),
actor,
action,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn agent(name: &str) -> AgentId {
AgentId::new(name).unwrap()
}
fn mid() -> MemoryId {
MemoryId::new()
}
fn assert_round_trip(action: AuditAction) {
let entry = AuditEntry::new(Some(agent("agent_a")), action.clone());
let bytes = bincode::serialize(&entry).unwrap();
let back: AuditEntry = bincode::deserialize(&bytes).unwrap();
assert_eq!(back.action, action);
assert!(back.actor.is_some());
}
#[test]
fn share_memory() {
assert_round_trip(AuditAction::ShareMemory {
memory_id: mid(),
source_namespace: "private:agent_a".into(),
target_namespace: "shared".into(),
});
}
#[test]
fn promote_to_shared() {
assert_round_trip(AuditAction::PromoteToShared { memory_id: mid() });
}
#[test]
fn quarantine() {
assert_round_trip(AuditAction::Quarantine {
memory_id: mid(),
anomaly_score: 0.95,
reason: "anomalous embedding".into(),
});
}
#[test]
fn quarantine_approved() {
assert_round_trip(AuditAction::QuarantineApproved { memory_id: mid() });
}
#[test]
fn quarantine_rejected() {
assert_round_trip(AuditAction::QuarantineRejected { memory_id: mid() });
}
#[test]
fn quarantine_rolled_back() {
assert_round_trip(AuditAction::QuarantineRolledBack {
memory_id: mid(),
removed_memory_ids: vec![mid()],
restored_memory_ids: vec![mid()],
reason: "manual rollback".into(),
});
}
#[test]
fn cross_agent_merge() {
assert_round_trip(AuditAction::CrossAgentMerge {
source_ids: vec![mid(), mid()],
result_id: mid(),
source_agents: vec![agent("a"), agent("b")],
});
}
#[test]
fn agent_registered() {
assert_round_trip(AuditAction::AgentRegistered {
agent_id: agent("new_agent"),
});
}
#[test]
fn agent_deregistered() {
assert_round_trip(AuditAction::AgentDeregistered {
agent_id: agent("old_agent"),
});
}
#[test]
fn namespace_created() {
assert_round_trip(AuditAction::NamespaceCreated {
namespace: "shared".into(),
});
}
#[test]
fn namespace_deleted() {
assert_round_trip(AuditAction::NamespaceDeleted {
namespace: "old_team".into(),
});
}
#[test]
fn agent_added_to_team() {
assert_round_trip(AuditAction::AgentAddedToTeam {
agent_id: agent("agent_b"),
team: "team_backend".into(),
});
}
#[test]
fn agent_removed_from_team() {
assert_round_trip(AuditAction::AgentRemovedFromTeam {
agent_id: agent("agent_b"),
team: "team_backend".into(),
});
}
#[test]
fn agent_rate_limited() {
assert_round_trip(AuditAction::AgentRateLimited {
agent_id: agent("spammer"),
quarantined_count: 15,
window_seconds: 60,
});
}
#[test]
fn agent_purged() {
assert_round_trip(AuditAction::AgentPurged {
agent_id: agent("deleted_agent"),
episodic_deleted: 100,
semantic_deleted: 50,
procedural_deleted: 10,
edges_removed: 200,
});
}
#[test]
fn edge_limit_exceeded() {
assert_round_trip(AuditAction::EdgeLimitExceeded {
node_id: mid(),
current_count: 512,
limit: 512,
});
}
#[test]
fn access_granted() {
assert_round_trip(AuditAction::AccessGranted {
action: "recall".into(),
realm: "default".into(),
namespace: "shared".into(),
policy_ids: vec!["policy_01".into()],
});
}
#[test]
fn access_denied() {
assert_round_trip(AuditAction::AccessDenied {
action: "remember".into(),
realm: "default".into(),
namespace: "private:other".into(),
reasons: vec!["namespace mismatch".into()],
policy_ids: vec!["deny_cross_ns".into()],
});
}
#[test]
fn policy_changed() {
assert_round_trip(AuditAction::PolicyChanged {
policy_name: "allow_shared_recall".into(),
change_type: "added".into(),
policy_content: "permit(...)".into(),
});
}
#[test]
fn belief_override() {
assert_round_trip(AuditAction::BeliefOverride {
logical_memory_id: LogicalMemoryId::new(),
prior_revision_id: RevisionId::new(),
override_revision_id: RevisionId::new(),
namespace: "default".into(),
reason: "trusted operator override".into(),
});
assert_round_trip(AuditAction::BeliefReconcileApproved {
conflict_id: "conflict-1".into(),
action: "retract".into(),
namespace: "default".into(),
logical_memory_ids: vec![LogicalMemoryId::new()],
applied_memory_ids: vec![mid()],
rationale: "approved offline reconcile".into(),
});
}
#[test]
fn entry_without_actor() {
let entry = AuditEntry::new(
None,
AuditAction::NamespaceCreated {
namespace: "system".into(),
},
);
assert!(entry.actor.is_none());
let bytes = bincode::serialize(&entry).unwrap();
let back: AuditEntry = bincode::deserialize(&bytes).unwrap();
assert!(back.actor.is_none());
}
}