use anyhow::{bail, Context, Result};
use blake3::Hasher;
use std::fs::File;
use std::io::{BufReader, Read};
use std::path::{Path, PathBuf};
use tokio::process::Command;
use tokio::time::{self, Duration};
use tracing::{debug, warn};
pub struct ModelSecurityValidator;
impl ModelSecurityValidator {
pub fn compute_digest(path: &Path) -> Result<String> {
let file =
File::open(path).with_context(|| format!("Unable to open {}", path.display()))?;
let mut reader = BufReader::new(file);
let mut hasher = Hasher::new();
let mut buffer = [0u8; 8192];
loop {
let read = reader.read(&mut buffer)?;
if read == 0 {
break;
}
hasher.update(&buffer[..read]);
}
Ok(hasher.finalize().to_hex().to_string())
}
pub fn persist_digest(path: &Path, digest: &str) -> Result<()> {
std::fs::write(Self::digest_path(path), digest)?;
Ok(())
}
pub fn load_stored_digest(path: &Path) -> Result<Option<String>> {
let digest_path = Self::digest_path(path);
if digest_path.exists() {
let value = std::fs::read_to_string(digest_path)?;
return Ok(Some(value.trim().to_string()));
}
Ok(None)
}
pub async fn sandbox_smoke_test(path: &Path) -> Result<()> {
let command = "kandil-sandbox";
let child = match Command::new(command)
.arg("load-test")
.arg(path)
.kill_on_drop(true)
.spawn()
{
Ok(child) => child,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
debug!(
"Sandbox binary '{}' not found. Skipping sandbox test.",
command
);
return Ok(());
}
Err(err) => return Err(err.into()),
};
let output = time::timeout(Duration::from_secs(30), child.wait_with_output()).await;
match output {
Ok(Ok(result)) if result.status.success() => Ok(()),
Ok(Ok(result)) => {
bail!(
"Sandbox test failed with status {}: {}",
result.status,
String::from_utf8_lossy(&result.stderr)
)
}
Ok(Err(err)) => Err(err.into()),
Err(_) => Err(anyhow::anyhow!(
"Sandbox test timed out. The downloaded model may be corrupted."
)),
}
}
pub async fn verify_artifact(path: &Path, expected_digest: Option<&str>) -> Result<String> {
let digest = Self::compute_digest(path)?;
if let Some(expected) = expected_digest {
if !expected.eq_ignore_ascii_case(&digest) {
bail!(
"Checksum mismatch for {}.\nExpected: {}\nActual: {}",
path.display(),
expected,
digest
);
}
}
Self::sandbox_smoke_test(path).await?;
Ok(digest)
}
fn digest_path(path: &Path) -> PathBuf {
let mut output = path.to_path_buf();
output.set_extension("blake3");
output
}
}