use crate::merkle::Hash;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[cfg(feature = "algoswitch")]
use vex_algoswitch as algoswitch;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum AuditEventType {
AgentCreated,
AgentExecuted,
DebateStarted,
DebateRound,
DebateConcluded,
ConsensusReached,
ContextStored,
PaymentInitiated,
PaymentCompleted,
PolicyUpdate,
ModelUpgrade,
AnomalousBehavior,
HumanOverride,
#[serde(rename = "CHORA_GATE_DECISION")]
GateDecision,
#[serde(untagged)]
Custom(String),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct EvidenceCapsule {
pub capsule_id: String,
pub outcome: String, pub reason_code: String,
#[serde(skip_serializing_if = "serde_json::Value::is_null")]
pub sensors: serde_json::Value,
pub reproducibility_context: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "type", content = "id", rename_all = "lowercase")]
pub enum ActorType {
Bot(Uuid),
Human(String),
System(String),
}
impl Default for ActorType {
fn default() -> Self {
ActorType::System("vex_core".to_string())
}
}
impl ActorType {
pub fn pseudonymize(&self) -> Self {
match self {
Self::Human(id) => {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(id.as_bytes());
Self::Human(hex::encode(hasher.finalize()))
}
other => other.clone(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Signature {
pub signer_id: String,
pub signed_at: DateTime<Utc>,
pub signature_hex: String,
}
impl Signature {
pub fn create(
signer_id: impl Into<String>,
message: &[u8],
signing_key: &ed25519_dalek::SigningKey,
) -> Self {
use ed25519_dalek::Signer;
let signature = signing_key.sign(message);
Self {
signer_id: signer_id.into(),
signed_at: Utc::now(),
signature_hex: hex::encode(signature.to_bytes()),
}
}
pub fn verify(
&self,
message: &[u8],
verifying_key: &ed25519_dalek::VerifyingKey,
) -> Result<bool, String> {
let sig_bytes = match hex::decode(&self.signature_hex) {
Ok(bytes) => bytes,
Err(_) => return Ok(false),
};
let sig_array: [u8; 64] = match sig_bytes.try_into() {
Ok(arr) => arr,
Err(_) => return Ok(false),
};
let signature = ed25519_dalek::Signature::from_bytes(&sig_array);
match verifying_key.verify_strict(message, &signature) {
Ok(()) => Ok(true),
Err(e) => Err(format!("Signature verification failed: {}", e)),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditEvent {
pub id: Uuid,
pub event_type: AuditEventType,
pub timestamp: DateTime<Utc>,
pub agent_id: Option<Uuid>,
pub data: serde_json::Value,
pub hash: Hash,
pub previous_hash: Option<Hash>,
pub sequence_number: u64,
pub actor: ActorType,
pub rationale: Option<String>,
pub policy_version: Option<String>,
pub data_provenance_hash: Option<Hash>,
pub human_review_required: bool,
pub approval_signatures: Vec<Signature>,
#[serde(skip_serializing_if = "Option::is_none")]
pub evidence_capsule: Option<EvidenceCapsule>,
pub schema_version: String,
}
#[derive(Serialize)]
pub struct HashParams<'a> {
pub event_type: &'a AuditEventType,
pub timestamp: i64, pub sequence_number: u64,
pub data: &'a serde_json::Value,
pub actor: &'a ActorType,
#[serde(skip_serializing_if = "Option::is_none")]
pub rationale: &'a Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub policy_version: &'a Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data_provenance_hash: &'a Option<Hash>,
pub human_review_required: bool,
pub approval_count: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub evidence_capsule: &'a Option<EvidenceCapsule>,
pub schema_version: &'a str,
}
impl AuditEvent {
const SENSITIVE_FIELDS: &'static [&'static str] = &[
"password",
"secret",
"token",
"api_key",
"apikey",
"key",
"authorization",
"auth",
"credential",
"private_key",
"privatekey",
];
pub fn new(
event_type: AuditEventType,
agent_id: Option<Uuid>,
data: serde_json::Value,
sequence_number: u64,
) -> Self {
let id = Uuid::new_v4();
let timestamp = Utc::now();
let data = Self::sanitize_data(data);
let actor = ActorType::System("vex_core".to_string());
let rationale: Option<String> = None;
let policy_version: Option<String> = None;
let data_provenance_hash: Option<Hash> = None;
let human_review_required = false;
let approval_signatures: Vec<Signature> = Vec::new();
let evidence_capsule: Option<EvidenceCapsule> = None;
let schema_version = "1.0".to_string();
let hash = Self::compute_hash(HashParams {
event_type: &event_type,
timestamp: timestamp.timestamp(),
sequence_number,
data: &data,
actor: &actor,
rationale: &rationale,
policy_version: &policy_version,
data_provenance_hash: &data_provenance_hash,
human_review_required,
approval_count: approval_signatures.len(),
evidence_capsule: &evidence_capsule,
schema_version: &schema_version,
});
Self {
id,
event_type,
timestamp,
agent_id,
data,
hash,
previous_hash: None,
sequence_number,
actor,
rationale,
policy_version,
data_provenance_hash,
human_review_required,
approval_signatures,
evidence_capsule,
schema_version,
}
}
pub fn sanitize_data(value: serde_json::Value) -> serde_json::Value {
match value {
serde_json::Value::Object(mut map) => {
for key in map.keys().cloned().collect::<Vec<_>>() {
let lower_key = key.to_lowercase();
if Self::SENSITIVE_FIELDS.iter().any(|f| lower_key.contains(f)) {
map.insert(key, serde_json::Value::String("[REDACTED]".to_string()));
} else if let Some(v) = map.remove(&key) {
map.insert(key, Self::sanitize_data(v));
}
}
serde_json::Value::Object(map)
}
serde_json::Value::Array(arr) => {
serde_json::Value::Array(arr.into_iter().map(Self::sanitize_data).collect())
}
other => other,
}
}
pub fn chained(
event_type: AuditEventType,
agent_id: Option<Uuid>,
data: serde_json::Value,
previous_hash: Hash,
sequence_number: u64,
) -> Self {
let mut event = Self::new(event_type, agent_id, data, sequence_number);
event.previous_hash = Some(previous_hash.clone());
event.hash = Self::compute_chained_hash(&event.hash, &previous_hash, sequence_number);
event
}
pub fn compute_hash(params: HashParams) -> Hash {
match serde_jcs::to_vec(¶ms) {
Ok(jcs_bytes) => Hash::digest(&jcs_bytes),
Err(_) => {
let content = format!(
"{:?}:{}:{}:{:?}:{:?}:{:?}:{:?}:{:?}:{}:{}:{}",
params.event_type,
params.timestamp,
params.sequence_number,
params.data,
params.actor,
params.rationale,
params.policy_version,
params.data_provenance_hash.as_ref().map(|h| h.to_hex()),
params.human_review_required,
params.approval_count,
params.schema_version,
);
Hash::digest(content.as_bytes())
}
}
}
pub fn compute_chained_hash(base_hash: &Hash, prev_hash: &Hash, sequence: u64) -> Hash {
let content = format!("{}:{}:{}", base_hash, prev_hash, sequence);
Hash::digest(content.as_bytes())
}
#[cfg(feature = "algoswitch")]
pub fn compute_optimized_hash(params: HashParams) -> u64 {
match serde_jcs::to_vec(¶ms) {
Ok(jcs_bytes) => algoswitch::select_hash(&jcs_bytes).0,
Err(_) => {
let content = format!(
"{:?}:{}:{}:{:?}:{:?}:{:?}:{}",
params.event_type,
params.timestamp,
params.sequence_number,
params.data,
params.actor,
params.rationale,
params.approval_count,
);
algoswitch::select_hash(content.as_bytes()).0
}
}
}
}