chore: pre-migration snapshot

Fleet monitoring/control plane: UI, node agent, ops wiring
This commit is contained in:
Vault Sovereign
2025-12-27 01:52:03 +00:00
parent 352a178aff
commit c91e252e91
10 changed files with 371 additions and 8 deletions

2
.gitignore vendored
View File

@@ -7,3 +7,5 @@ Cargo.lock
__pycache__/
*.pyc
*.env
/vm-cloud-v0.2.0-vaultmesh.zip
/Untitled

16
ASSURANCE.md Normal file
View File

@@ -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.

View File

@@ -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"

View File

@@ -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<usize>,
/// 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<usize>) -> Result<EventEnvelope> {
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)
}
}

View File

@@ -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
}

View File

@@ -0,0 +1,2 @@
pub mod map_event_envelope_to_receipt;
pub mod receipt_v1;

View File

@@ -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<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ip: Option<String>,
}
#[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<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub target: Option<ReceiptTarget>,
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<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub plan_blake3: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub plan_sha256: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub lock_file: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub lock_started_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub force: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cwd: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub user: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hostname: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub argv: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sig_alg: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub signer_pub: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub signature: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub signed_at: Option<String>,
}
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<Vec<u8>> {
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(())
}
}

View File

@@ -0,0 +1,5 @@
pub mod cli;
pub mod contracts;
pub mod logs;
pub mod routes;
pub mod state;

View File

@@ -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 {

View File

@@ -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");
}