Initialize repository snapshot

This commit is contained in:
Vault Sovereign
2025-12-27 00:10:32 +00:00
commit 110d644e10
281 changed files with 40331 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
[package]
name = "vaultmesh-guardian"
version = "0.1.0"
edition = "2021"
[dependencies]
vaultmesh-core = { path = "../vaultmesh-core" }
vaultmesh-observability = { path = "../vaultmesh-observability", optional = true }
chrono = { version = "0.4", features = ["serde"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
[features]
default = []
metrics = ["vaultmesh-observability"]
[dev-dependencies]
tempfile = "3.8"
tokio = { version = "1.28", features = ["rt-multi-thread", "macros", "time"] }
reqwest = { version = "0.11", features = ["json", "rustls-tls"] }
vaultmesh-observability = { path = "../vaultmesh-observability" }

View File

@@ -0,0 +1,339 @@
//! VaultMesh Guardian Engine - Merkle root anchoring for the Civilization Ledger
//!
//! The Guardian engine computes Merkle roots over scroll JSONL files and
//! emits anchor receipts that cryptographically bind all receipts at a point in time.
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs::{self, OpenOptions};
use std::io::{BufRead, BufReader, Write};
use std::path::{Path, PathBuf};
#[cfg(feature = "metrics")]
use std::sync::Arc;
#[cfg(feature = "metrics")]
use std::time::Instant;
use vaultmesh_core::{merkle_root, Scroll, VmHash};
#[cfg(feature = "metrics")]
use vaultmesh_observability::ObservabilityEngine;
/// Schema version for guardian receipts
pub const SCHEMA_VERSION: &str = "2.0.0";
/// Guardian anchor receipt body
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnchorReceipt {
pub schema_version: String,
#[serde(rename = "type")]
pub receipt_type: String,
pub timestamp: DateTime<Utc>,
pub anchor_id: String,
pub backend: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub anchor_by: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub anchor_epoch: Option<u64>,
pub roots: HashMap<String, String>,
pub scrolls: Vec<String>,
pub anchor_hash: String,
}
/// Result of computing a scroll's Merkle root
#[derive(Debug, Clone)]
pub struct ScrollRoot {
pub scroll: Scroll,
pub root: VmHash,
pub leaf_count: usize,
}
/// Guardian engine for anchoring scroll Merkle roots
pub struct GuardianEngine {
/// Path to the VaultMesh root directory
pub vaultmesh_root: PathBuf,
/// DID of the guardian performing anchors
pub guardian_did: String,
/// Backend identifier (e.g., "local", "ethereum", "stellar")
pub backend: String,
/// Optional observability engine for metrics
#[cfg(feature = "metrics")]
pub observability: Option<Arc<ObservabilityEngine>>,
}
impl GuardianEngine {
/// Create a new Guardian engine
pub fn new(vaultmesh_root: impl AsRef<Path>, guardian_did: &str) -> Self {
Self {
vaultmesh_root: vaultmesh_root.as_ref().to_path_buf(),
guardian_did: guardian_did.to_string(),
backend: "local".to_string(),
#[cfg(feature = "metrics")]
observability: None,
}
}
/// Set the backend for this engine
pub fn with_backend(mut self, backend: &str) -> Self {
self.backend = backend.to_string();
self
}
/// Set the observability engine for metrics
#[cfg(feature = "metrics")]
pub fn with_observability(mut self, obs: Arc<ObservabilityEngine>) -> Self {
self.observability = Some(obs);
self
}
/// Get the path to a scroll's JSONL file
fn scroll_path(&self, scroll: &Scroll) -> PathBuf {
self.vaultmesh_root.join(scroll.jsonl_path())
}
/// Compute the Merkle root for a single scroll
pub fn compute_scroll_root(&self, scroll: &Scroll) -> std::io::Result<ScrollRoot> {
let path = self.scroll_path(scroll);
if !path.exists() {
return Ok(ScrollRoot {
scroll: scroll.clone(),
root: VmHash::blake3(b"empty"),
leaf_count: 0,
});
}
let file = fs::File::open(&path)?;
let reader = BufReader::new(file);
let mut hashes = Vec::new();
for line in reader.lines() {
let line = line?;
if line.trim().is_empty() {
continue;
}
hashes.push(VmHash::blake3(line.as_bytes()));
}
let leaf_count = hashes.len();
let root = merkle_root(&hashes);
Ok(ScrollRoot {
scroll: scroll.clone(),
root,
leaf_count,
})
}
/// Compute roots for all provided scrolls
pub fn compute_all_roots(&self, scrolls: &[Scroll]) -> std::io::Result<Vec<ScrollRoot>> {
scrolls.iter().map(|s| self.compute_scroll_root(s)).collect()
}
/// Generate an anchor ID based on current timestamp
fn generate_anchor_id() -> String {
let now = Utc::now();
format!("anchor-{}", now.format("%Y%m%d%H%M%S"))
}
/// Anchor all provided scrolls and emit a guardian receipt
pub fn anchor(&self, scrolls: &[Scroll]) -> std::io::Result<AnchorReceipt> {
let roots = self.compute_all_roots(scrolls)?;
// Build roots map
let mut roots_map = HashMap::new();
for sr in &roots {
let scroll_name = match sr.scroll {
Scroll::Drills => "drills",
Scroll::Compliance => "compliance",
Scroll::Guardian => "guardian",
Scroll::Treasury => "treasury",
Scroll::Mesh => "mesh",
Scroll::OffSec => "offsec",
Scroll::Identity => "identity",
Scroll::Observability => "observability",
Scroll::Automation => "automation",
Scroll::PsiField => "psi",
};
roots_map.insert(scroll_name.to_string(), sr.root.as_str().to_string());
}
// Compute anchor hash over all roots
let roots_json = serde_json::to_vec(&roots_map)?;
let anchor_hash = VmHash::blake3(&roots_json);
let now = Utc::now();
let anchor_epoch = now.timestamp() as u64;
let receipt = AnchorReceipt {
schema_version: SCHEMA_VERSION.to_string(),
receipt_type: "guardian_anchor".to_string(),
timestamp: now,
anchor_id: Self::generate_anchor_id(),
backend: self.backend.clone(),
anchor_by: Some(self.guardian_did.clone()),
anchor_epoch: Some(anchor_epoch),
roots: roots_map,
scrolls: scrolls
.iter()
.map(|s| match s {
Scroll::Drills => "drills",
Scroll::Compliance => "compliance",
Scroll::Guardian => "guardian",
Scroll::Treasury => "treasury",
Scroll::Mesh => "mesh",
Scroll::OffSec => "offsec",
Scroll::Identity => "identity",
Scroll::Observability => "observability",
Scroll::Automation => "automation",
Scroll::PsiField => "psi",
})
.map(String::from)
.collect(),
anchor_hash: anchor_hash.as_str().to_string(),
};
// Emit to guardian scroll
self.emit_receipt(&receipt)?;
Ok(receipt)
}
/// Write receipt to the guardian JSONL file
fn emit_receipt(&self, receipt: &AnchorReceipt) -> std::io::Result<()> {
#[cfg(feature = "metrics")]
let start = Instant::now();
let path = self.scroll_path(&Scroll::Guardian);
// Ensure parent directory exists
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(&path)?;
let json = serde_json::to_string(receipt)?;
writeln!(file, "{}", json)?;
// Update ROOT file
self.update_root_file(&Scroll::Guardian)?;
// Record metrics if observability is enabled
#[cfg(feature = "metrics")]
if let Some(ref obs) = self.observability {
let elapsed = start.elapsed().as_secs_f64();
obs.observe_emitted("guardian", elapsed);
// Set anchor age to 0 to indicate fresh anchor just occurred
obs.set_anchor_age(0.0);
}
Ok(())
}
/// Update the ROOT.*.txt file for a scroll
fn update_root_file(&self, scroll: &Scroll) -> std::io::Result<()> {
let root_result = self.compute_scroll_root(scroll)?;
let root_path = self.vaultmesh_root.join(scroll.root_file());
fs::write(&root_path, root_result.root.as_str())?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn setup_test_env() -> (TempDir, GuardianEngine) {
let tmp = TempDir::new().unwrap();
let receipts_dir = tmp.path().join("receipts/guardian");
fs::create_dir_all(&receipts_dir).unwrap();
let engine = GuardianEngine::new(tmp.path(), "did:vm:guardian:test");
(tmp, engine)
}
#[test]
fn test_compute_scroll_root_empty() {
let (_tmp, engine) = setup_test_env();
let result = engine.compute_scroll_root(&Scroll::Drills).unwrap();
assert_eq!(result.leaf_count, 0);
assert_eq!(result.root, VmHash::blake3(b"empty"));
}
#[test]
fn test_compute_scroll_root_single_line() {
let (tmp, engine) = setup_test_env();
// Create a test JSONL file
let path = tmp.path().join("receipts/drills/drill_runs.jsonl");
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(&path, "{\"test\": \"data\"}\n").unwrap();
let result = engine.compute_scroll_root(&Scroll::Drills).unwrap();
assert_eq!(result.leaf_count, 1);
// Single leaf = its own hash
let expected = VmHash::blake3(b"{\"test\": \"data\"}");
assert_eq!(result.root, expected);
}
#[test]
fn test_compute_scroll_root_multiple_lines() {
let (tmp, engine) = setup_test_env();
let path = tmp.path().join("receipts/drills/drill_runs.jsonl");
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(&path, "{\"line\": 1}\n{\"line\": 2}\n{\"line\": 3}\n").unwrap();
let result = engine.compute_scroll_root(&Scroll::Drills).unwrap();
assert_eq!(result.leaf_count, 3);
// Root should be deterministic
let result2 = engine.compute_scroll_root(&Scroll::Drills).unwrap();
assert_eq!(result.root, result2.root);
}
#[test]
fn test_anchor_creates_receipt() {
let (tmp, engine) = setup_test_env();
let receipt = engine.anchor(&[Scroll::Guardian]).unwrap();
assert_eq!(receipt.receipt_type, "guardian_anchor");
assert_eq!(receipt.backend, "local");
assert!(receipt.anchor_by.is_some());
assert!(receipt.anchor_id.starts_with("anchor-"));
// Verify receipt was written to JSONL
let path = tmp.path().join("receipts/guardian/anchor_events.jsonl");
assert!(path.exists());
let content = fs::read_to_string(&path).unwrap();
assert!(content.contains("guardian_anchor"));
}
#[test]
fn test_anchor_hash_deterministic() {
let (tmp, engine) = setup_test_env();
// Create some test data
let drills_path = tmp.path().join("receipts/drills/drill_runs.jsonl");
fs::create_dir_all(drills_path.parent().unwrap()).unwrap();
fs::write(&drills_path, "{\"drill\": 1}\n").unwrap();
// Compute roots twice
let roots1 = engine.compute_all_roots(&[Scroll::Drills]).unwrap();
let roots2 = engine.compute_all_roots(&[Scroll::Drills]).unwrap();
// Same input = same roots
assert_eq!(roots1[0].root, roots2[0].root);
}
}

View File

@@ -0,0 +1,75 @@
//! Integration test: Guardian anchor updates observability metrics
//!
//! Run with: cargo test -p vaultmesh-guardian --features metrics --test metrics_integration
#![cfg(feature = "metrics")]
use std::net::TcpListener;
use std::sync::Arc;
use tokio::time::{sleep, Duration};
use vaultmesh_core::Scroll;
use vaultmesh_guardian::GuardianEngine;
use vaultmesh_observability::ObservabilityEngine;
#[tokio::test]
async fn test_anchor_updates_observability_metrics() {
// Create temp directory for test
let tmp = tempfile::TempDir::new().unwrap();
let receipts_dir = tmp.path().join("receipts/guardian");
std::fs::create_dir_all(&receipts_dir).unwrap();
// Dynamic port allocation - bind to port 0 to get an available port
let listener = TcpListener::bind("127.0.0.1:0").expect("bind to port 0");
let addr = listener.local_addr().expect("get local addr");
drop(listener); // Release so ObservabilityEngine can bind
// Create Observability engine and run its HTTP server on dynamic port
let obs = Arc::new(ObservabilityEngine::new());
obs.clone().serve(&addr).await.expect("serve failed");
// Give the server time to start
sleep(Duration::from_millis(100)).await;
// Build guardian with observability (using the metrics feature)
let guardian = GuardianEngine::new(tmp.path(), "did:vm:guardian:test")
.with_observability(obs.clone());
// Perform an anchor
let scrolls = vec![Scroll::Guardian, Scroll::Treasury];
guardian.anchor(&scrolls).expect("anchor failed");
// Give time for metrics to update
sleep(Duration::from_millis(100)).await;
// Fetch metrics from Observability server using dynamic address
let metrics_url = format!("http://{}/metrics", addr);
let resp = reqwest::get(&metrics_url)
.await
.expect("request failed");
let body = resp.text().await.expect("body read failed");
// Assert metrics contain receipts counter
assert!(
body.contains("vaultmesh_receipts_total"),
"metrics should expose receipts counter"
);
// Assert metrics contain anchor age gauge
assert!(
body.contains("vaultmesh_anchor_age_seconds"),
"metrics should expose anchor age gauge"
);
// Assert guardian module appears in metrics
assert!(
body.contains("guardian"),
"guardian module should appear in metrics"
);
// Anchor age should be 0 (just anchored)
assert!(
body.contains("vaultmesh_anchor_age_seconds 0"),
"anchor age should be 0 after fresh anchor"
);
}