diff --git a/crates/mofa-kernel/src/hitl/context.rs b/crates/mofa-kernel/src/hitl/context.rs index dce30184f..18ddd30c1 100644 --- a/crates/mofa-kernel/src/hitl/context.rs +++ b/crates/mofa-kernel/src/hitl/context.rs @@ -58,6 +58,22 @@ pub struct Change { pub new_value: Option, } + +/// 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, + /// Domain-specific financial/technical metadata + pub metadata: HashMap, + /// The status of the internal policy check + pub policy_status: String, +} + /// Review context #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ReviewContext { @@ -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; diff --git a/crates/mofa-kernel/src/hitl/error.rs b/crates/mofa-kernel/src/hitl/error.rs index 89b37b911..906d68fe9 100644 --- a/crates/mofa-kernel/src/hitl/error.rs +++ b/crates/mofa-kernel/src/hitl/error.rs @@ -70,3 +70,27 @@ impl From for KernelError { /// Result type for HITL operations pub type HitlResult = Result; + +/// 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, + }, +} \ No newline at end of file diff --git a/crates/mofa-kernel/src/hitl/policy.rs b/crates/mofa-kernel/src/hitl/policy.rs index be3ecfc2d..965c20156 100644 --- a/crates/mofa-kernel/src/hitl/policy.rs +++ b/crates/mofa-kernel/src/hitl/policy.rs @@ -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>; - /// Check if a review request can be auto-approved async fn can_auto_approve(&self, request: &ReviewRequest) -> HitlResult; - /// Get policy name (for logging/debugging) fn name(&self) -> &str; } @@ -33,11 +27,11 @@ impl ReviewPolicy for AlwaysReviewPolicy { context: &ReviewContext, ) -> HitlResult> { 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 { @@ -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> { + + // 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 { + 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!"); + } +} \ No newline at end of file