use anyhow::{Context, Result};
use smith_attestation::{
ProvenanceGenerator, SlsaProvenance,
provenance::{ProvenanceConfig, BuildArtifact, BuildInfo}
};
use smith_protocol::{Intent, IntentResult};
use std::collections::HashMap;
use tracing::{debug, info};
use super::types::RuntimeAttestationResults;
pub struct RuntimeProvenanceGenerator {
generator: ProvenanceGenerator,
}
impl RuntimeProvenanceGenerator {
pub fn new(output_dir: String) -> Self {
let provenance_config = ProvenanceConfig {
build_environment: std::env::var("EXECUTION_ENVIRONMENT")
.unwrap_or_else(|_| "runtime".to_string()),
builder_id: "smith-executor".to_string(),
repository_url: std::env::var("REPOSITORY_URL")
.unwrap_or_else(|_| "https://github.com/smith-rs/smith".to_string()),
build_trigger: "runtime-execution".to_string(),
output_dir,
};
let generator = ProvenanceGenerator::new(provenance_config);
Self { generator }
}
pub fn with_config(config: ProvenanceConfig) -> Self {
let generator = ProvenanceGenerator::new(config);
Self { generator }
}
pub async fn generate_runtime_provenance(
&self,
intent: &Intent,
result: &IntentResult,
_attestation_results: &RuntimeAttestationResults,
) -> Result<SlsaProvenance> {
debug!("Generating runtime provenance for intent: {}", intent.id);
let build_info = self.collect_runtime_build_info().await?;
let artifacts = self.create_execution_artifacts(intent, result).await?;
let provenance = self
.generator
.generate_provenance(&build_info, artifacts)
.await
.context("Failed to generate runtime provenance")?;
let provenance_file = format!("runtime-provenance-{}.json", intent.id);
self.save_provenance(&provenance, &provenance_file).await?;
info!("Runtime provenance generated for intent: {}", intent.id);
Ok(provenance)
}
pub async fn save_provenance(&self, provenance: &SlsaProvenance, filename: &str) -> Result<()> {
self.generator
.save_provenance(provenance, filename)
.await
.context("Failed to save runtime provenance")
}
async fn collect_runtime_build_info(&self) -> Result<BuildInfo> {
ProvenanceGenerator::collect_build_info()
.await
.context("Failed to collect runtime build information")
}
async fn create_execution_artifacts(
&self,
intent: &Intent,
result: &IntentResult,
) -> Result<Vec<BuildArtifact>> {
let mut artifacts = Vec::new();
let intent_artifact = self.create_intent_artifact(intent).await?;
artifacts.push(intent_artifact);
let result_artifact = self.create_result_artifact(result, &intent.id).await?;
artifacts.push(result_artifact);
Ok(artifacts)
}
async fn create_intent_artifact(&self, intent: &Intent) -> Result<BuildArtifact> {
let intent_bytes = serde_json::to_vec(intent).context("Failed to serialize intent")?;
let intent_digest = self.generate_sha256_digest(&intent_bytes)?;
let mut digest_map = HashMap::new();
digest_map.insert("sha256".to_string(), intent_digest);
Ok(BuildArtifact {
name: format!("intent-{}.json", intent.id),
path: format!("runtime/intents/{}", intent.id),
digest: digest_map,
})
}
async fn create_result_artifact(&self, result: &IntentResult, intent_id: &str) -> Result<BuildArtifact> {
let result_bytes = serde_json::to_vec(result).context("Failed to serialize result")?;
let result_digest = self.generate_sha256_digest(&result_bytes)?;
let mut digest_map = HashMap::new();
digest_map.insert("sha256".to_string(), result_digest);
Ok(BuildArtifact {
name: format!("result-{}.json", intent_id),
path: format!("runtime/results/{}", intent_id),
digest: digest_map,
})
}
fn generate_sha256_digest(&self, bytes: &[u8]) -> Result<String> {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(bytes);
Ok(format!("{:x}", hasher.finalize()))
}
pub fn get_output_dir(&self) -> &str {
&self.generator.config.output_dir
}
pub fn get_builder_id(&self) -> &str {
&self.generator.config.builder_id
}
}
#[cfg(test)]
mod tests {
use super::*;
use smith_protocol::{Intent, IntentResult, ExecutionStatus};
use tempfile::tempdir;
fn create_test_intent() -> Intent {
Intent {
id: "test-intent-123".to_string(),
capability: "fs.read.v1".to_string(),
params: serde_json::json!({"path": "/test/file.txt"}),
actor: "test-actor".to_string(),
created_at: chrono::Utc::now(),
timeout_seconds: Some(30),
}
}
fn create_test_result() -> IntentResult {
IntentResult {
id: "test-intent-123".to_string(),
status: ExecutionStatus::Ok,
data: serde_json::json!({"content": "file content"}),
artifacts: vec![],
created_at: chrono::Utc::now(),
duration_ms: Some(150),
}
}
fn create_test_attestation_results() -> RuntimeAttestationResults {
use super::super::types::VerificationDetails;
let details = VerificationDetails::new(
HashMap::new(),
vec![],
vec![],
HashMap::new(),
);
RuntimeAttestationResults::new(
"test-digest".to_string(),
Some("test-image-digest".to_string()),
true,
true,
true,
details,
)
}
#[test]
fn test_runtime_provenance_generator_creation() {
let temp_dir = tempdir().unwrap();
let output_dir = temp_dir.path().to_string_lossy().to_string();
let generator = RuntimeProvenanceGenerator::new(output_dir.clone());
assert_eq!(generator.get_output_dir(), output_dir);
assert_eq!(generator.get_builder_id(), "smith-executor");
}
#[test]
fn test_runtime_provenance_generator_with_config() {
let temp_dir = tempdir().unwrap();
let config = ProvenanceConfig {
build_environment: "test-env".to_string(),
builder_id: "custom-builder".to_string(),
repository_url: "https://example.com/repo".to_string(),
build_trigger: "custom-trigger".to_string(),
output_dir: temp_dir.path().to_string_lossy().to_string(),
};
let generator = RuntimeProvenanceGenerator::with_config(config.clone());
assert_eq!(generator.get_output_dir(), config.output_dir);
assert_eq!(generator.get_builder_id(), "custom-builder");
}
#[test]
fn test_generate_sha256_digest() {
let temp_dir = tempdir().unwrap();
let generator = RuntimeProvenanceGenerator::new(
temp_dir.path().to_string_lossy().to_string()
);
let test_data = b"hello world";
let digest = generator.generate_sha256_digest(test_data).unwrap();
assert_eq!(digest, "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9");
}
#[tokio::test]
async fn test_create_intent_artifact() {
let temp_dir = tempdir().unwrap();
let generator = RuntimeProvenanceGenerator::new(
temp_dir.path().to_string_lossy().to_string()
);
let intent = create_test_intent();
let artifact = generator.create_intent_artifact(&intent).await.unwrap();
assert_eq!(artifact.name, "intent-test-intent-123.json");
assert_eq!(artifact.path, "runtime/intents/test-intent-123");
assert!(artifact.digest.contains_key("sha256"));
assert!(!artifact.digest["sha256"].is_empty());
}
#[tokio::test]
async fn test_create_result_artifact() {
let temp_dir = tempdir().unwrap();
let generator = RuntimeProvenanceGenerator::new(
temp_dir.path().to_string_lossy().to_string()
);
let result = create_test_result();
let artifact = generator.create_result_artifact(&result, "test-intent-123").await.unwrap();
assert_eq!(artifact.name, "result-test-intent-123.json");
assert_eq!(artifact.path, "runtime/results/test-intent-123");
assert!(artifact.digest.contains_key("sha256"));
assert!(!artifact.digest["sha256"].is_empty());
}
#[tokio::test]
async fn test_create_execution_artifacts() {
let temp_dir = tempdir().unwrap();
let generator = RuntimeProvenanceGenerator::new(
temp_dir.path().to_string_lossy().to_string()
);
let intent = create_test_intent();
let result = create_test_result();
let artifacts = generator.create_execution_artifacts(&intent, &result).await.unwrap();
assert_eq!(artifacts.len(), 2);
assert!(artifacts.iter().any(|a| a.name.starts_with("intent-")));
assert!(artifacts.iter().any(|a| a.name.starts_with("result-")));
}
}