From c91e252e917ff219be8ba9e722b232745172b322 Mon Sep 17 00:00:00 2001 From: Vault Sovereign Date: Sat, 27 Dec 2025 01:52:03 +0000 Subject: [PATCH] chore: pre-migration snapshot Fleet monitoring/control plane: UI, node agent, ops wiring --- .gitignore | 2 + ASSURANCE.md | 16 ++ command-center/Cargo.toml | 4 + command-center/src/bin/cc-receipt.rs | 79 ++++++++++ .../map_event_envelope_to_receipt.rs | 72 +++++++++ command-center/src/contracts/mod.rs | 2 + command-center/src/contracts/receipt_v1.rs | 146 ++++++++++++++++++ command-center/src/lib.rs | 5 + command-center/src/main.rs | 12 +- command-center/tests/receipt_adapter.rs | 41 +++++ 10 files changed, 371 insertions(+), 8 deletions(-) create mode 100644 ASSURANCE.md create mode 100644 command-center/src/bin/cc-receipt.rs create mode 100644 command-center/src/contracts/map_event_envelope_to_receipt.rs create mode 100644 command-center/src/contracts/mod.rs create mode 100644 command-center/src/contracts/receipt_v1.rs create mode 100644 command-center/src/lib.rs create mode 100644 command-center/tests/receipt_adapter.rs diff --git a/.gitignore b/.gitignore index 2defd7c..6c0647e 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ Cargo.lock __pycache__/ *.pyc *.env +/vm-cloud-v0.2.0-vaultmesh.zip +/Untitled diff --git a/ASSURANCE.md b/ASSURANCE.md new file mode 100644 index 0000000..f427657 --- /dev/null +++ b/ASSURANCE.md @@ -0,0 +1,16 @@ +# Assurance Run — 2025-12-18 + +- Commit: 352a178aff92b35d0771965bea99c94984c492a4 +- Toolchain: `rustc 1.92.0 (ded5c06cf 2025-12-08)`, `cargo 1.92.0 (344c4567c 2025-10-21)` +- Host: `macOS / Apple Silicon` + +| Check | Status | Notes | +| --- | --- | --- | +| `cargo fmt --check` | ❌ | rustfmt wants to wrap long chains and struct literals (e.g. `command-center/src/routes.rs`, `command-center/src/logs.rs`, `node-agent/src/main.rs`). No changes applied; rerun `cargo fmt` to adopt the default style. | +| `cargo clippy --all-targets --all-features -- -D warnings` | ❌ | Fails on unused variables (`command-center/src/routes.rs:954`), unused field/method (`state.rs:108`, `state.rs:735`), log reader patterns (`logs.rs:48`, `logs.rs:371`), derivable `Default`, `io_other_error`, etc. Exact diagnostics captured from the run. | +| `cargo test` | ✅ (warn) | Tests pass; same unused-field/unused-variable warnings as above remain. | +| `cargo check --release` | ✅ (warn) | Release build succeeds with the same warnings. | + +Notes: +- Removed stale `target/` directory (now rebuilt by `cargo test`). +- No changes were saved to source files. diff --git a/command-center/Cargo.toml b/command-center/Cargo.toml index bd6334f..c3a998e 100644 --- a/command-center/Cargo.toml +++ b/command-center/Cargo.toml @@ -8,6 +8,7 @@ axum = { version = "0.7", features = ["macros", "json"] } tokio = { version = "1", features = ["rt-multi-thread", "macros"] } serde = { version = "1", features = ["derive"] } serde_json = "1" +serde_jcs = "0.1" tower = "0.4" tower-http = { version = "0.5", features = ["trace"] } tracing = "0.1" @@ -21,3 +22,6 @@ anyhow = "1" clap = { version = "4", features = ["derive"] } async-stream = "0.3" futures-core = "0.3" +blake3 = "1" +sha2 = "0.10" +hex = "0.4" diff --git a/command-center/src/bin/cc-receipt.rs b/command-center/src/bin/cc-receipt.rs new file mode 100644 index 0000000..bf3e0a7 --- /dev/null +++ b/command-center/src/bin/cc-receipt.rs @@ -0,0 +1,79 @@ +use std::{fs, path::PathBuf}; + +use anyhow::{anyhow, Context, Result}; +use clap::{Parser, Subcommand}; +use vaultmesh_command_center::contracts::map_event_envelope_to_receipt::map_event_envelope_to_receipt; +use vaultmesh_command_center::contracts::receipt_v1::ReceiptV1; +use vaultmesh_command_center::state::EventEnvelope; + +#[derive(Parser)] +#[command(name = "cc-receipt", about = "Export and verify VaultMesh Receipt v1 from EventEnvelope")] +struct Cli { + #[command(subcommand)] + cmd: Command, +} + +#[derive(Subcommand)] +enum Command { + /// Export a Receipt v1 JSON from an EventEnvelope JSON or JSONL line. + Export { + /// Path to EventEnvelope JSON (or JSONL if --line is provided). + #[arg(long)] + event_json: PathBuf, + /// If set, treat event_json as JSONL and read this 1-based line. + #[arg(long)] + line: Option, + /// Output file for Receipt v1 JSON. + #[arg(long)] + out: PathBuf, + }, + + /// Verify hashes of a Receipt v1 JSON. + Verify { + #[arg(long)] + receipt: PathBuf, + }, +} + +fn main() -> Result<()> { + let cli = Cli::parse(); + + match cli.cmd { + Command::Export { + event_json, + line, + out, + } => { + let env = load_event_envelope(&event_json, line)?; + let receipt = map_event_envelope_to_receipt(&env); + fs::write(&out, serde_json::to_vec_pretty(&receipt)?) + .with_context(|| format!("write {}", out.display()))?; + println!("wrote {}", out.display()); + } + Command::Verify { receipt } => { + let bytes = fs::read(&receipt).with_context(|| format!("read {}", receipt.display()))?; + let r: ReceiptV1 = serde_json::from_slice(&bytes)?; + r.verify_hashes()?; + println!("OK: hashes verify"); + } + } + + Ok(()) +} + +fn load_event_envelope(path: &PathBuf, line: Option) -> Result { + let bytes = fs::read(path).with_context(|| format!("read {}", path.display()))?; + + if let Some(n) = line { + let s = std::str::from_utf8(&bytes)?; + let line_str = s + .lines() + .nth(n.saturating_sub(1)) + .ok_or_else(|| anyhow!("JSONL line {} not found", n))?; + let env: EventEnvelope = serde_json::from_str(line_str)?; + Ok(env) + } else { + let env: EventEnvelope = serde_json::from_slice(&bytes)?; + Ok(env) + } +} diff --git a/command-center/src/contracts/map_event_envelope_to_receipt.rs b/command-center/src/contracts/map_event_envelope_to_receipt.rs new file mode 100644 index 0000000..a1c7c11 --- /dev/null +++ b/command-center/src/contracts/map_event_envelope_to_receipt.rs @@ -0,0 +1,72 @@ +use serde_json::Value; +use time::format_description::well_known::Rfc3339; + +use crate::state::EventEnvelope; + +use super::receipt_v1::{ + genesis_prev_blake3, ReceiptRequest, ReceiptResponse, ReceiptTarget, ReceiptV1, +}; + +/// Map an existing canonical EventEnvelope into a schema-valid Receipt V1. +pub fn map_event_envelope_to_receipt(envelope: &EventEnvelope) -> ReceiptV1 { + let mut canon = envelope.clone(); + canon.canonicalize_in_place(); + + let created_at = canon + .ts + .format(&Rfc3339) + .expect("EventEnvelope ts should format to RFC3339"); + + // Payload is already canonicalized by EventEnvelope rules (sorted object keys, arrays preserved). + let payload: Value = canon.payload.clone(); + + let target = canon + .node_id + .as_ref() + .map(|id| ReceiptTarget { + id: id.to_string(), + name: None, + ip: None, + }); + + let mut receipt = ReceiptV1 { + receipt_version: "1".to_string(), + created_at, + source: "command-center".to_string(), + action: canon.kind.clone(), + reason: None, + target, + request: ReceiptRequest { + method: "POST".to_string(), + path: "/api/events".to_string(), + body: payload.clone(), + }, + response: ReceiptResponse { + status: 200, + ok: true, + data: payload, + raw: String::new(), + }, + prev_blake3: genesis_prev_blake3(), + hash_alg: String::new(), + blake3: String::new(), + sha256: String::new(), + plan_file: None, + plan_blake3: None, + plan_sha256: None, + lock_file: None, + lock_started_at: None, + force: None, + cwd: None, + user: None, + hostname: None, + argv: None, + sig_alg: None, + signer_pub: None, + signature: None, + signed_at: None, + }; + + receipt.compute_hashes().expect("hash computation must succeed"); + receipt +} diff --git a/command-center/src/contracts/mod.rs b/command-center/src/contracts/mod.rs new file mode 100644 index 0000000..3506810 --- /dev/null +++ b/command-center/src/contracts/mod.rs @@ -0,0 +1,2 @@ +pub mod map_event_envelope_to_receipt; +pub mod receipt_v1; diff --git a/command-center/src/contracts/receipt_v1.rs b/command-center/src/contracts/receipt_v1.rs new file mode 100644 index 0000000..41561cb --- /dev/null +++ b/command-center/src/contracts/receipt_v1.rs @@ -0,0 +1,146 @@ +use anyhow::{anyhow, Result}; +use blake3::Hasher as Blake3Hasher; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use sha2::{Digest, Sha256}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ReceiptTarget { + pub id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub ip: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ReceiptRequest { + pub method: String, + pub path: String, + pub body: Value, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ReceiptResponse { + pub status: u16, + pub ok: bool, + pub data: Value, + pub raw: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ReceiptV1 { + pub receipt_version: String, + pub created_at: String, + pub source: String, + pub action: String, + + #[serde(skip_serializing_if = "Option::is_none")] + pub reason: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub target: Option, + + pub request: ReceiptRequest, + pub response: ReceiptResponse, + + pub prev_blake3: String, + + pub hash_alg: String, + pub blake3: String, + pub sha256: String, + + #[serde(skip_serializing_if = "Option::is_none")] + pub plan_file: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub plan_blake3: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub plan_sha256: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub lock_file: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub lock_started_at: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub force: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub cwd: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub user: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub hostname: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub argv: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub sig_alg: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub signer_pub: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub signature: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub signed_at: Option, +} + +pub fn genesis_prev_blake3() -> String { + "0".repeat(64) +} + +impl ReceiptV1 { + fn strip_field(v: &mut Value, key: &str) { + if let Value::Object(map) = v { + map.remove(key); + } + } + + /// Produce canonical JCS bytes without self-referential hash/signature fields. + pub fn canonical_body_bytes(&self) -> Result> { + let mut v = serde_json::to_value(self)?; + Self::strip_field(&mut v, "hash_alg"); + Self::strip_field(&mut v, "blake3"); + Self::strip_field(&mut v, "sha256"); + Self::strip_field(&mut v, "sig_alg"); + Self::strip_field(&mut v, "signer_pub"); + Self::strip_field(&mut v, "signature"); + Self::strip_field(&mut v, "signed_at"); + + let bytes = serde_jcs::to_vec(&v).map_err(|e| anyhow!("serde_jcs: {e}"))?; + Ok(bytes) + } + + pub fn compute_hashes(&mut self) -> Result<()> { + let bytes = self.canonical_body_bytes()?; + + let mut b3 = Blake3Hasher::new(); + b3.update(&bytes); + let b3_hex = b3.finalize().to_hex().to_string(); + + let mut s = Sha256::new(); + s.update(&bytes); + let sha_hex = hex::encode(s.finalize()); + + self.hash_alg = "blake3+sha256".to_string(); + self.blake3 = b3_hex; + self.sha256 = sha_hex; + Ok(()) + } + + pub fn verify_hashes(&self) -> Result<()> { + let mut tmp = self.clone(); + tmp.compute_hashes()?; + if tmp.blake3 != self.blake3 { + return Err(anyhow!( + "blake3 mismatch: expected {}, got {}", + self.blake3, tmp.blake3 + )); + } + if tmp.sha256 != self.sha256 { + return Err(anyhow!( + "sha256 mismatch: expected {}, got {}", + self.sha256, tmp.sha256 + )); + } + Ok(()) + } +} diff --git a/command-center/src/lib.rs b/command-center/src/lib.rs new file mode 100644 index 0000000..fab9c56 --- /dev/null +++ b/command-center/src/lib.rs @@ -0,0 +1,5 @@ +pub mod cli; +pub mod contracts; +pub mod logs; +pub mod routes; +pub mod state; diff --git a/command-center/src/main.rs b/command-center/src/main.rs index 97b5dbb..0c5d789 100644 --- a/command-center/src/main.rs +++ b/command-center/src/main.rs @@ -1,11 +1,3 @@ -mod cli; -mod logs; -mod routes; -mod state; - -use crate::cli::{Cli, Commands, LogsAction}; -use crate::routes::app; -use crate::state::{now_utc_seconds, AppState, CommandPayload, SignedCommand}; use base64::Engine; use clap::Parser; use ed25519_dalek::{Signer, SigningKey}; @@ -15,6 +7,10 @@ use std::time::Duration; use tokio::net::TcpListener; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; use uuid::Uuid; +use vaultmesh_command_center::cli::{Cli, Commands, LogsAction}; +use vaultmesh_command_center::logs; +use vaultmesh_command_center::routes::app; +use vaultmesh_command_center::state::{now_utc_seconds, AppState, CommandPayload, SignedCommand}; /// Load an existing Ed25519 signing key or generate a new one. fn load_or_init_signing_key() -> SigningKey { diff --git a/command-center/tests/receipt_adapter.rs b/command-center/tests/receipt_adapter.rs new file mode 100644 index 0000000..cbb95fb --- /dev/null +++ b/command-center/tests/receipt_adapter.rs @@ -0,0 +1,41 @@ +use time::OffsetDateTime; +use uuid::Uuid; +use vaultmesh_command_center::contracts::map_event_envelope_to_receipt::map_event_envelope_to_receipt; +use vaultmesh_command_center::contracts::receipt_v1::{genesis_prev_blake3, ReceiptV1}; +use vaultmesh_command_center::state::EventEnvelope; + +#[test] +fn maps_envelope_to_receipt_and_verifies_hashes() { + let id = Uuid::parse_str("00000000-0000-0000-0000-0000000000aa").unwrap(); + let ts = OffsetDateTime::parse( + "2025-12-17T23:07:10Z", + &time::format_description::well_known::Rfc3339, + ) + .unwrap(); + + let payload = serde_json::json!({ + "note": "hello", + "details": {"x": 2, "a": 1}, + }); + + let env = EventEnvelope::new( + id, + ts, + "note".to_string(), + None, + "operator".to_string(), + payload, + ); + + let receipt: ReceiptV1 = map_event_envelope_to_receipt(&env); + + assert_eq!(receipt.receipt_version, "1"); + assert_eq!(receipt.source, "command-center"); + assert_eq!(receipt.action, "note"); + assert_eq!(receipt.prev_blake3, genesis_prev_blake3()); + assert!(!receipt.blake3.is_empty()); + assert!(!receipt.sha256.is_empty()); + + // Hashes must verify against canonical recomputation. + receipt.verify_hashes().expect("hashes should verify"); +}