chore: pre-migration snapshot
Fleet monitoring/control plane: UI, node agent, ops wiring
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -7,3 +7,5 @@ Cargo.lock
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
*.env
|
*.env
|
||||||
|
/vm-cloud-v0.2.0-vaultmesh.zip
|
||||||
|
/Untitled
|
||||||
|
|||||||
16
ASSURANCE.md
Normal file
16
ASSURANCE.md
Normal 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.
|
||||||
@@ -8,6 +8,7 @@ axum = { version = "0.7", features = ["macros", "json"] }
|
|||||||
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
|
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
serde_jcs = "0.1"
|
||||||
tower = "0.4"
|
tower = "0.4"
|
||||||
tower-http = { version = "0.5", features = ["trace"] }
|
tower-http = { version = "0.5", features = ["trace"] }
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
@@ -21,3 +22,6 @@ anyhow = "1"
|
|||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
async-stream = "0.3"
|
async-stream = "0.3"
|
||||||
futures-core = "0.3"
|
futures-core = "0.3"
|
||||||
|
blake3 = "1"
|
||||||
|
sha2 = "0.10"
|
||||||
|
hex = "0.4"
|
||||||
|
|||||||
79
command-center/src/bin/cc-receipt.rs
Normal file
79
command-center/src/bin/cc-receipt.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
2
command-center/src/contracts/mod.rs
Normal file
2
command-center/src/contracts/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod map_event_envelope_to_receipt;
|
||||||
|
pub mod receipt_v1;
|
||||||
146
command-center/src/contracts/receipt_v1.rs
Normal file
146
command-center/src/contracts/receipt_v1.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
5
command-center/src/lib.rs
Normal file
5
command-center/src/lib.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
pub mod cli;
|
||||||
|
pub mod contracts;
|
||||||
|
pub mod logs;
|
||||||
|
pub mod routes;
|
||||||
|
pub mod state;
|
||||||
@@ -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 base64::Engine;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use ed25519_dalek::{Signer, SigningKey};
|
use ed25519_dalek::{Signer, SigningKey};
|
||||||
@@ -15,6 +7,10 @@ use std::time::Duration;
|
|||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||||
use uuid::Uuid;
|
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.
|
/// Load an existing Ed25519 signing key or generate a new one.
|
||||||
fn load_or_init_signing_key() -> SigningKey {
|
fn load_or_init_signing_key() -> SigningKey {
|
||||||
|
|||||||
41
command-center/tests/receipt_adapter.rs
Normal file
41
command-center/tests/receipt_adapter.rs
Normal 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");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user