Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions crates/mofa-kernel/src/hitl/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,22 @@ pub struct Change {
pub new_value: Option<serde_json::Value>,
}


/// Structured auditing data for high-integrity workflows
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditingData {
/// The core intent of the action
pub intent: String,
/// The final result or proposal
pub result: String,
/// Steps taken during the reasoning process
pub relevant_trace_steps: Vec<String>,
/// Domain-specific financial/technical metadata
pub metadata: HashMap<String, serde_json::Value>,
/// The status of the internal policy check
pub policy_status: String,
}

/// Review context
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReviewContext {
Expand Down Expand Up @@ -105,7 +121,56 @@ impl ReviewContext {
self.telemetry = telemetry;
self
}

/// Injects structured auditing data into the additional metadata map
pub fn with_auditing_data(mut self, data: AuditingData) -> Self {
if let Ok(value) = serde_json::to_value(data) {
self.additional.insert("audit_trail".to_string(), value);
}
self
}




}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;

#[test]
fn test_auditing_data_integration() {
// 1. Create a "dummy" trace for the context
let trace = ExecutionTrace {
steps: vec![],
duration_ms: 0,
};

// 2. Create your new AuditingData
let audit = AuditingData {
intent: "High-Value Trade".to_string(),
result: "Execute Buy".to_string(),
relevant_trace_steps: vec!["step_1".to_string()],
metadata: HashMap::from([
("asset".to_string(), json!("SOL")),
("amount".to_string(), json!(10.5))
]),
policy_status: "Pass".to_string(),
};

// 3. Use the Builder Pattern to inject it
let context = ReviewContext::new(trace, json!({}))
.with_auditing_data(audit);

// 4. Verify it was stored correctly in the 'additional' HashMap
let stored_audit = context.additional.get("audit_trail").expect("Audit trail should exist");

assert_eq!(stored_audit["intent"], "High-Value Trade");
assert_eq!(stored_audit["policy_status"], "Pass");

println!("✅ Audit Trail successfully serialized into ReviewContext!");
}
}
/// Review metadata (re-exported from types for convenience)
pub use crate::hitl::types::ReviewMetadata;
24 changes: 24 additions & 0 deletions crates/mofa-kernel/src/hitl/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,27 @@ impl From<HitlError> for KernelError {

/// Result type for HITL operations
pub type HitlResult<T> = Result<T, HitlError>;

/// The "Rulebook" for the Auditing Security System.
/// This tells the AI exactly why a transaction was stopped.
#[derive(Error, Debug, PartialEq, Eq)]
pub enum AuditError {
// 1. The "Broken Seal" Rule (Integrity)
#[error("Audit integrity check failed: The digital seal (hash) does not match!")]
IntegrityMismatch,

// 2. The "Incomplete Form" Rule
#[error("Audit failed: You forgot to fill in the '{0}' field.")]
MissingRequiredField(String),

// 3. The "Time Travel" Rule
#[error("Audit failed: The clock says this happened in the future!")]
InvalidTimestamp,

// 4. The "VIP Only" Rule
#[error("Audit failed: This person does not have the right security key (Level {required} needed, but has Level {actual}).")]
InsufficientSecurityLevel {
required: u8,
actual: u8,
},
}
76 changes: 68 additions & 8 deletions crates/mofa-kernel/src/hitl/policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,15 @@ use crate::hitl::{HitlResult, ReviewContext, ReviewRequest, ReviewType};
use async_trait::async_trait;

/// Review policy trait
///
/// Policies determine when reviews should be requested and whether
/// they can be auto-approved.
#[async_trait]
pub trait ReviewPolicy: Send + Sync {
/// Determine if a review should be requested for the given context
async fn should_request_review(
&self,
context: &ReviewContext,
) -> HitlResult<Option<ReviewRequest>>;

/// Check if a review request can be auto-approved
async fn can_auto_approve(&self, request: &ReviewRequest) -> HitlResult<bool>;

/// Get policy name (for logging/debugging)
fn name(&self) -> &str;
}

Expand All @@ -33,11 +27,11 @@ impl ReviewPolicy for AlwaysReviewPolicy {
context: &ReviewContext,
) -> HitlResult<Option<ReviewRequest>> {
let request = ReviewRequest::new(
"unknown", // execution_id will be set by caller
"unknown",
ReviewType::Approval,
context.clone(),
);
Ok(Some(request))
Ok(Some(request))
}

async fn can_auto_approve(&self, _request: &ReviewRequest) -> HitlResult<bool> {
Expand Down Expand Up @@ -69,3 +63,69 @@ impl ReviewPolicy for NeverReviewPolicy {
"NeverReviewPolicy"
}
}

/// NEW: Audit-Aware Policy
pub struct AuditValidationPolicy;

#[async_trait]
impl ReviewPolicy for AuditValidationPolicy {
async fn should_request_review(
&self,
context: &ReviewContext,
) -> HitlResult<Option<ReviewRequest>> {

// Match the key name used in context.rs ("audit_trail")
if let Some(_audit_val) = context.additional.get("audit_trail") {
let request = ReviewRequest::new(
"audit_check".to_string(),
ReviewType::Approval,
context.clone(),
);
return Ok(Some(request));
}

Ok(None)
}

async fn can_auto_approve(&self, _request: &ReviewRequest) -> HitlResult<bool> {
Ok(false) // Humans must sign off on luxury/fintech audits
}

fn name(&self) -> &str {
"AuditValidationPolicy"
}
}

#[cfg(test)]
mod policy_tests {
use super::*;
use crate::hitl::ReviewContext;
use crate::hitl::context::{ExecutionTrace, AuditingData};
use serde_json::json;
use std::collections::HashMap;

#[tokio::test]
async fn test_audit_validation_policy_triggers() {
let policy = AuditValidationPolicy;

let trace = ExecutionTrace { steps: vec![], duration_ms: 0 };
let audit = AuditingData {
intent: "Luxury Purchase".to_string(),
result: "Approved".to_string(),
relevant_trace_steps: vec![],
metadata: HashMap::new(),
policy_status: "Pass".to_string(),
};

let context = ReviewContext::new(trace, json!({}))
.with_auditing_data(audit);

let result = policy.should_request_review(&context).await.unwrap();

assert!(result.is_some());
let request = result.unwrap();
assert_eq!(request.execution_id, "audit_check");

println!("✅ Audit Guard successfully caught the transaction!");
}
}