//! Fail-closed first-release-candidate completeness and evidence audit. #![allow(clippy::too_many_lines)] // The gate records every invariant in one ordered audit. use super::{MatrixError, Result}; use serde::{Deserialize, Serialize}; use serde_json::Value; use sha2::{Digest, Sha256}; use std::collections::{BTreeMap, BTreeSet}; use std::ffi::OsStr; use std::fs::{self, OpenOptions}; use std::io::Write as _; use std::path::{Path, PathBuf}; use std::process::Command; use std::time::{SystemTime, UNIX_EPOCH}; const POLICY_PATH: &str = "ci/release-candidate-policy.json"; const PARITY_PATH: &str = "tests/upstream-tests.json"; const ARTIFACT_POLICY_PATH: &str = "ci/artifact-policy.json"; const FORBIDDEN_LIVE_PHRASES: [&str; 4] = [ "return;", "skipping assertion", "transient server issue", "proceeding with tests", ]; const SMOKE_STAGES: [&str; 9] = [ "completeness", "login", "capabilities-simulator", "im-chat", "movement-teleport", "inventory-folder", "object-properties", "asset-texture", "logout", ]; #[derive(Deserialize)] struct Policy { schema: u32, release: String, current_rust_prefix: String, upstream_commit: String, expected: Expected, required_evidence: Vec, live_test_files: Vec, fake_grid_evidence: String, live_grid_evidence: String, release_notes: String, changelog: String, checksums: String, signature_required: bool, #[serde(rename = "signature_policy")] authenticity_description: String, deferred_by_user: Vec, } #[derive(Deserialize)] struct Expected { release_packages: usize, shipped_binaries: usize, mapped_public_types: u64, mapped_public_members: u64, parity_cases: u64, translated_cases: u64, live_cases: u64, benchmark_cases: u64, live_smoke_records: usize, } #[derive(Deserialize)] struct EvidenceRequirement { id: String, path: String, assertions: Vec, } #[derive(Deserialize)] struct JsonAssertion { pointer: String, equals: Value, } #[derive(Deserialize)] struct Metadata { packages: Vec, workspace_members: Vec, } #[derive(Deserialize)] struct MetadataPackage { id: String, name: String, version: String, manifest_path: PathBuf, } #[derive(Deserialize)] struct ParityLedger { schema_version: u32, upstream_commit: String, expected_cases: u64, report: ParityReport, } #[derive(Deserialize, Serialize)] struct ParityReport { pending: u64, translated: u64, ignored_live: u64, benchmark: u64, drifted: u64, unreviewed: u64, } #[derive(Serialize)] struct AuditEvidence { schema: u32, release: String, source_commit: String, rustc: String, recorded_unix_seconds: u64, package_count: usize, binary_count: usize, parity: ParityReport, stub_findings: BTreeMap, silent_live_skip_findings: BTreeMap>, required_evidence: Vec, fake_grid_records: usize, live_grid_records: usize, signature_required: bool, signature_policy: String, deferred_by_user: Vec, checks: Vec, status: &'static str, } #[derive(Serialize)] struct VerifiedEvidence { id: String, path: String, sha256: Option, valid: bool, } #[derive(Serialize)] struct Check { id: &'static str, passed: bool, detail: String, } /// Audits the complete non-fuzz first-release-candidate gate. /// /// The evidence file is written even when blockers are found so CI preserves a /// precise failure inventory. Existing evidence is never overwritten. /// /// # Errors /// /// Returns an error after recording evidence when any release invariant fails, /// or immediately for an invalid policy, unreadable input, or existing output. pub fn audit_release_candidate(root: &Path, evidence_path: &Path) -> Result<()> { if evidence_path.exists() { return Err(MatrixError::new(format!( "{} already exists; preserve or remove it before rerunning the audit", evidence_path.display() ))); } let policy: Policy = serde_json::from_slice(&fs::read(root.join(POLICY_PATH))?)?; validate_policy(&policy)?; let rustc = command_output(root, "rustc", &["--version"])?; let source_commit = command_output(root, "git", &["rev-parse", "--verify", "HEAD"])?; let metadata = cargo_metadata(root)?; let (package_count, versions_ok) = release_versions(root, &metadata, &policy)?; let artifact_policy: Value = serde_json::from_slice(&fs::read(root.join(ARTIFACT_POLICY_PATH))?)?; let binary_count = artifact_policy .pointer("/binaries") .and_then(Value::as_array) .map_or(0, Vec::len); let artifact_package_count = artifact_policy .pointer("/release_packages") .and_then(Value::as_array) .map_or(0, Vec::len); let parity: ParityLedger = serde_json::from_slice(&fs::read(root.join(PARITY_PATH))?)?; let parity_ok = parity.schema_version == 2 && parity.upstream_commit == policy.upstream_commit && parity.expected_cases == policy.expected.parity_cases && parity.report.pending == 0 && parity.report.unreviewed == 0 && parity.report.drifted == 0 && parity.report.translated == policy.expected.translated_cases && parity.report.ignored_live == policy.expected.live_cases && parity.report.benchmark == policy.expected.benchmark_cases; let stub_findings = scan_stub_findings(root)?; let silent_live_skip_findings = scan_live_skip_findings(root, &policy.live_test_files)?; let required_evidence = verify_evidence(root, &policy.required_evidence)?; let fake_grid = validate_smoke_evidence( &root.join(&policy.fake_grid_evidence), policy.expected.live_smoke_records, "fake", &policy.release, ); let live_grid = validate_smoke_evidence( &root.join(&policy.live_grid_evidence), policy.expected.live_smoke_records, "open-sim-live", &policy.release, ); let release_notes_ok = release_text_has_version(root, &policy.release_notes, &policy.release); let changelog_ok = release_text_has_version(root, &policy.changelog, &policy.release); let checksums_ok = validate_checksums(root, &policy.checksums); let workflows_ok = ubuntu_only_workflows(root)?; let checks = vec![ Check { id: "current-rust", passed: rustc.starts_with(&policy.current_rust_prefix), detail: rustc.clone(), }, Check { id: "workspace-version", passed: versions_ok && package_count == policy.expected.release_packages, detail: format!( "{package_count} release packages at version {}", policy.release ), }, Check { id: "artifact-inventory", passed: artifact_package_count == policy.expected.release_packages && binary_count == policy.expected.shipped_binaries, detail: format!("{artifact_package_count} packages; {binary_count} binaries"), }, Check { id: "api-and-test-parity", passed: parity_ok, detail: format!( "{} cases; {} translated; {} live; {} benchmark; {} pending; {} unreviewed", parity.expected_cases, parity.report.translated, parity.report.ignored_live, parity.report.benchmark, parity.report.pending, parity.report.unreviewed ), }, Check { id: "zero-unimplemented-shims", passed: stub_findings.is_empty(), detail: format!( "{} callable failure markers", stub_findings.values().sum::() ), }, Check { id: "no-silent-live-skips", passed: silent_live_skip_findings.is_empty(), detail: format!( "{} files with silent-skip paths", silent_live_skip_findings.len() ), }, Check { id: "prior-audit-evidence", passed: required_evidence.iter().all(|item| item.valid), detail: format!("{} required evidence files", required_evidence.len()), }, Check { id: "fake-grid-smoke", passed: fake_grid.is_ok(), detail: smoke_detail(&fake_grid), }, Check { id: "opensim-live-smoke", passed: live_grid.is_ok(), detail: smoke_detail(&live_grid), }, Check { id: "release-notes", passed: release_notes_ok && changelog_ok, detail: format!("release notes and changelog cover {}", policy.release), }, Check { id: "artifact-checksums", passed: checksums_ok, detail: policy.authenticity_description.clone(), }, Check { id: "ubuntu-only-gitea-actions", passed: workflows_ok, detail: "every Gitea Actions job uses ubuntu-latest".into(), }, ]; let success = checks.iter().all(|check| check.passed); let evidence = AuditEvidence { schema: 1, release: policy.release, source_commit, rustc, recorded_unix_seconds: SystemTime::now() .duration_since(UNIX_EPOCH) .map_err(|_| MatrixError::new("system clock predates Unix epoch"))? .as_secs(), package_count, binary_count, parity: parity.report, stub_findings, silent_live_skip_findings, required_evidence, fake_grid_records: fake_grid.unwrap_or(0), live_grid_records: live_grid.unwrap_or(0), signature_required: policy.signature_required, signature_policy: policy.authenticity_description, deferred_by_user: policy.deferred_by_user, checks, status: if success { "ok" } else { "blocked" }, }; write_new_json(evidence_path, &evidence)?; if success { Ok(()) } else { Err(MatrixError::new(format!( "release candidate has {} failing non-fuzz gate(s); see {}", evidence.checks.iter().filter(|check| !check.passed).count(), evidence_path.display() ))) } } fn validate_policy(policy: &Policy) -> Result<()> { if policy.schema != 1 || policy.release.trim().is_empty() || !policy.current_rust_prefix.starts_with("rustc 1.97.") || policy.upstream_commit.len() != 40 || policy.required_evidence.is_empty() || policy.live_test_files.is_empty() || policy.authenticity_description.trim().is_empty() || policy.deferred_by_user != ["fuzz-smoke"] || policy.expected.mapped_public_types != 3_066 || policy.expected.mapped_public_members != 30_789 { return Err(MatrixError::new("invalid release-candidate policy")); } let ids = policy .required_evidence .iter() .map(|item| item.id.as_str()) .collect::>(); if ids.len() != policy.required_evidence.len() || policy .required_evidence .iter() .any(|item| item.assertions.is_empty()) { return Err(MatrixError::new( "duplicate or assertion-free release evidence policy", )); } Ok(()) } fn cargo_metadata(root: &Path) -> Result { let output = Command::new("cargo") .args(["metadata", "--format-version", "1", "--locked", "--no-deps"]) .current_dir(root) .output()?; if !output.status.success() { return Err(MatrixError::new(format!( "cargo metadata failed: {}", String::from_utf8_lossy(&output.stderr).trim() ))); } Ok(serde_json::from_slice(&output.stdout)?) } fn release_versions(root: &Path, metadata: &Metadata, policy: &Policy) -> Result<(usize, bool)> { let members = metadata .workspace_members .iter() .map(String::as_str) .collect::>(); let crates = root.canonicalize()?.join("crates"); let programs = root.canonicalize()?.join("programs/Cargo.toml"); let release = metadata .packages .iter() .filter(|package| members.contains(package.id.as_str())) .filter(|package| { package .manifest_path .canonicalize() .is_ok_and(|manifest| manifest.starts_with(&crates) || manifest == programs) }) .collect::>(); let versions_ok = release.iter().all(|package| { package.version == policy.release && package.name.starts_with("libremetaverse") }); Ok((release.len(), versions_ok)) } fn scan_stub_findings(root: &Path) -> Result> { let mut findings = BTreeMap::new(); let crates = root.join("crates"); walk_rust(&crates, &mut |path, text| { if path.ends_with("libremetaverse-types/src/shim.rs") { return Ok(()); } let count = text.lines().filter(|line| stub_markers(line) > 0).count(); if count > 0 { findings.insert(relative(root, path)?, count); } Ok(()) })?; Ok(findings) } fn stub_markers(text: &str) -> usize { [ "not_implemented(", "unimplemented_api!(", "todo!(", "unimplemented!(", ] .iter() .filter(|marker| text.contains(**marker)) .count() } fn walk_rust(directory: &Path, visitor: &mut impl FnMut(&Path, &str) -> Result<()>) -> Result<()> { let mut entries = fs::read_dir(directory)?.collect::>>()?; entries.sort_by_key(std::fs::DirEntry::file_name); for entry in entries { let file_type = entry.file_type()?; if file_type.is_symlink() { return Err(MatrixError::new(format!( "release source contains symlink {}", entry.path().display() ))); } if file_type.is_dir() { walk_rust(&entry.path(), visitor)?; } else if file_type.is_file() && entry.path().extension() == Some(OsStr::new("rs")) { visitor(&entry.path(), &fs::read_to_string(entry.path())?)?; } } Ok(()) } fn scan_live_skip_findings(root: &Path, paths: &[String]) -> Result>> { let mut findings = BTreeMap::new(); for relative in paths { let text = fs::read_to_string(root.join(relative))?; let found = FORBIDDEN_LIVE_PHRASES .iter() .filter(|phrase| text.to_ascii_lowercase().contains(**phrase)) .map(|phrase| (*phrase).to_owned()) .collect::>(); if !found.is_empty() { findings.insert(relative.clone(), found); } } Ok(findings) } fn verify_evidence( root: &Path, requirements: &[EvidenceRequirement], ) -> Result> { requirements .iter() .map(|requirement| { let path = root.join(&requirement.path); let bytes = fs::read(&path).ok(); let value = bytes .as_deref() .and_then(|bytes| serde_json::from_slice::(bytes).ok()); let valid = value.as_ref().is_some_and(|value| { requirement .assertions .iter() .all(|assertion| value.pointer(&assertion.pointer) == Some(&assertion.equals)) }); Ok(VerifiedEvidence { id: requirement.id.clone(), path: requirement.path.clone(), sha256: bytes.as_deref().map(hash_bytes), valid, }) }) .collect() } fn validate_smoke_evidence( path: &Path, expected: usize, expected_mode: &str, release: &str, ) -> Result { let text = fs::read_to_string(path)?; let mut stages = BTreeSet::new(); let mut count = 0; for (index, line) in text.lines().enumerate() { let value: Value = serde_json::from_str(line)?; let schema = value.pointer("/schema").and_then(Value::as_u64); let mode = value.pointer("/mode").and_then(Value::as_str); let sequence = value.pointer("/sequence").and_then(Value::as_u64); let status = value.pointer("/status").and_then(Value::as_str); let stage = value.pointer("/stage").and_then(Value::as_str); let program_version = value.pointer("/program_version").and_then(Value::as_str); let rust_commit = value.pointer("/rust_commit").and_then(Value::as_str); let recorded = value .pointer("/recorded_unix_seconds") .and_then(Value::as_u64); let metrics = value.pointer("/metrics").and_then(Value::as_object); let expected_stage = SMOKE_STAGES.get(index).copied(); let timestamp_valid = recorded.is_some_and(|seconds| { seconds >= 1_577_836_800 && SystemTime::now() .duration_since(UNIX_EPOCH) .is_ok_and(|now| seconds <= now.as_secs().saturating_add(300)) }); let commit_valid = rust_commit.is_some_and(|commit| { commit.len() == 40 && commit.bytes().all(|byte| byte.is_ascii_hexdigit()) }); let metrics_valid = metrics .is_some_and(|metrics| !metrics.is_empty() && metrics.values().all(Value::is_u64)); if schema != Some(1) || mode != Some(expected_mode) || sequence != u64::try_from(index + 1).ok() || status != Some("ok") || stage != expected_stage || program_version != Some(release) || !timestamp_valid || !commit_valid || !metrics_valid || !stages.insert(stage.unwrap_or_default().to_owned()) || line.contains("://") || line.to_ascii_lowercase().contains("password") || line.to_ascii_lowercase().contains("token") { return Err(MatrixError::new(format!( "invalid sanitized smoke evidence {} at record {}", path.display(), index + 1 ))); } count += 1; } if count != expected { return Err(MatrixError::new(format!( "{} has {count} smoke records, expected {expected}", path.display() ))); } Ok(count) } fn smoke_detail(result: &Result) -> String { match result { Ok(count) => format!("{count} successful sanitized mode-bound records"), Err(error) => error.to_string(), } } fn release_text_has_version(root: &Path, relative: &str, release: &str) -> bool { fs::read_to_string(root.join(relative)).is_ok_and(|text| text.contains(release)) } fn validate_checksums(root: &Path, relative: &str) -> bool { let Ok(text) = fs::read_to_string(root.join(relative)) else { return false; }; let mut count = 0; for line in text.lines().filter(|line| !line.trim().is_empty()) { let Some((hash, path)) = line.split_once(" ") else { return false; }; if hash.len() != 64 || !hash.bytes().all(|byte| byte.is_ascii_hexdigit()) { return false; } let path = root.join(path); let Ok(bytes) = fs::read(path) else { return false; }; if hash_bytes(&bytes) != hash { return false; } count += 1; } count >= 6 } fn ubuntu_only_workflows(root: &Path) -> Result { let directory = root.join(".gitea/workflows"); for entry in fs::read_dir(directory)? { let entry = entry?; if entry.path().extension() != Some(OsStr::new("yml")) { continue; } for line in fs::read_to_string(entry.path())?.lines() { if let Some(runner) = line.trim().strip_prefix("runs-on:") && runner.trim() != "ubuntu-latest" { return Ok(false); } } } Ok(true) } fn command_output(root: &Path, program: &str, args: &[&str]) -> Result { let output = Command::new(program) .args(args) .current_dir(root) .output()?; if !output.status.success() { return Err(MatrixError::new(format!("{program} command failed"))); } String::from_utf8(output.stdout) .map(|output| output.trim().to_owned()) .map_err(|_| MatrixError::new(format!("{program} output was not UTF-8"))) } fn relative(root: &Path, path: &Path) -> Result { path.strip_prefix(root) .map_err(|_| MatrixError::new(format!("{} is outside the workspace", path.display())))? .to_str() .map(str::to_owned) .ok_or_else(|| MatrixError::new(format!("{} is not UTF-8", path.display()))) } fn hash_bytes(bytes: &[u8]) -> String { Sha256::digest(bytes) .iter() .fold(String::with_capacity(64), |mut output, byte| { use std::fmt::Write as _; write!(output, "{byte:02x}").expect("writing to a String cannot fail"); output }) } fn write_new_json(path: &Path, value: &impl Serialize) -> Result<()> { if let Some(parent) = path.parent() { fs::create_dir_all(parent)?; } let mut file = OpenOptions::new().write(true).create_new(true).open(path)?; serde_json::to_writer_pretty(&mut file, value)?; file.write_all(b"\n")?; file.sync_all()?; Ok(()) } #[cfg(test)] mod tests { use super::*; #[test] fn stub_marker_scan_rejects_every_failure_construct() { assert_eq!(stub_markers("value.not_implemented()"), 1); assert_eq!(stub_markers("unimplemented_api!(\"M:x\")"), 1); assert_eq!(stub_markers("todo!()"), 1); assert_eq!(stub_markers("unimplemented!()"), 1); assert_eq!(stub_markers("implemented()"), 0); } #[test] fn checked_in_release_candidate_policy_is_well_formed() { let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../.."); let policy: Policy = serde_json::from_slice(&fs::read(root.join(POLICY_PATH)).unwrap()).unwrap(); validate_policy(&policy).unwrap(); assert_eq!(policy.expected.release_packages, 16); assert_eq!(policy.expected.shipped_binaries, 10); } #[test] fn smoke_evidence_is_bound_to_mode_stage_version_and_commit() { use std::fmt::Write as _; let directory = std::env::temp_dir().join(format!("metacrate-rc-smoke-{}", std::process::id())); fs::create_dir_all(&directory).unwrap(); let path = directory.join("evidence.jsonl"); let recorded = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_secs(); let mut contents = String::new(); for (index, stage) in SMOKE_STAGES.iter().enumerate() { writeln!( contents, "{{\"schema\":1,\"mode\":\"fake\",\"sequence\":{},\"recorded_unix_seconds\":{recorded},\"program_version\":\"0.0.1\",\"rust_commit\":\"0123456789abcdef0123456789abcdef01234567\",\"stage\":\"{stage}\",\"status\":\"ok\",\"metrics\":{{\"proved\":1}}}}", index + 1 ) .unwrap(); } fs::write(&path, &contents).unwrap(); assert_eq!( validate_smoke_evidence(&path, 9, "fake", "0.0.1").unwrap(), 9 ); assert!(validate_smoke_evidence(&path, 9, "open-sim-live", "0.0.1").is_err()); fs::remove_file(path).unwrap(); fs::remove_dir(directory).unwrap(); } }