File size: 10,763 Bytes
9425aed | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 | use anyhow::Result;
use blake3;
use chrono::Utc;
use ed25519_dalek::SigningKey;
use git2::{Repository, Signature as GitSignature};
use hex;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::sync::Arc;
use tracing::{debug, info, warn};
/// Cryptographic approval certificate
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApprovalCertificate {
/// Change ID being approved
pub change_id: String,
/// Human reviewer's name
pub reviewer: String,
/// ISO8601 timestamp of approval
pub approval_time: String,
/// Blake3 hash of the change evidence
pub evidence_hash: String,
/// Ed25519 signature of (change_id || reviewer || time)
pub signature: String,
}
/// Commit metadata including human approval
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HumanApprovedCommit {
/// Change ID
pub change_id: String,
/// Human reviewer who approved
pub approved_by: String,
/// Approval timestamp
pub approval_date: String,
/// URL or link to evidence
pub evidence_url: String,
/// Link to review decision
pub review_link: Option<String>,
}
/// Commit gateway enforcing human approval
#[derive(Clone)]
pub struct CommitGateway {
repo_path: PathBuf,
approval_timeout_secs: u64,
/// Signing key for certificates (in production, would be secured)
signing_key: Arc<Option<SigningKey>>,
}
impl CommitGateway {
/// Create a new commit gateway
pub fn new(repo_path: PathBuf, approval_timeout_secs: u64) -> Result<Self> {
info!(
"π CommitGateway initialized (repo: {:?}, timeout: {}s)",
repo_path, approval_timeout_secs
);
// In production: load signing key from secure storage
// For now: use a placeholder
let key_seed = [0u8; 32];
let signing_key = SigningKey::from_bytes(&key_seed);
Ok(CommitGateway {
repo_path,
approval_timeout_secs,
signing_key: Arc::new(Some(signing_key)),
})
}
/// Pre-commit verification: ensure change has human approval
pub async fn verify_approval_required(&self, change_id: &str) -> Result<()> {
info!("π Verifying approval requirement for change: {}", change_id);
// This would check the audit log to ensure approval exists
// For now: placeholder verification
if change_id.is_empty() {
return Err(anyhow::anyhow!("Change ID cannot be empty"));
}
debug!("β
Approval verification passed for: {}", change_id);
Ok(())
}
/// Stage files for commit
pub fn stage_files(&self, files: &[String]) -> Result<()> {
let repo = Repository::open(&self.repo_path)?;
let mut index = repo.index()?;
for file in files {
index.add_path(&std::path::Path::new(file))?;
}
info!("π¦ Staged {} files for commit", files.len());
index.write()?;
Ok(())
}
/// Create an approval certificate
pub fn create_approval_certificate(
&self,
change_id: &str,
reviewer: &str,
evidence_url: &str,
) -> Result<ApprovalCertificate> {
let now = Utc::now();
// Hash the evidence URL
let evidence_hash = blake3::hash(evidence_url.as_bytes());
let evidence_hash_hex = hex::encode(evidence_hash.as_bytes());
// Create signing material: change_id || reviewer || timestamp
let signing_material = format!(
"{}||{}||{}",
change_id,
reviewer,
now.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
);
// Sign (in production: use actual signing key, not placeholder)
let signature_bytes = blake3::hash(signing_material.as_bytes());
let signature_hex = hex::encode(signature_bytes.as_bytes());
let cert = ApprovalCertificate {
change_id: change_id.to_string(),
reviewer: reviewer.to_string(),
approval_time: now.to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
evidence_hash: evidence_hash_hex,
signature: signature_hex,
};
info!("ποΈ Approval certificate created: {:?}", cert);
Ok(cert)
}
/// Commit changes with human approval metadata
pub fn commit_with_approval(
&self,
change_id: &str,
reviewer: &str,
message: &str,
evidence_url: &str,
) -> Result<String> {
let repo = Repository::open(&self.repo_path)?;
// Get the index (staged files)
let mut index = repo.index()?;
let tree_id = index.write_tree()?;
let tree = repo.find_tree(tree_id)?;
// Create git signature for the commit
let git_sig = GitSignature::now(reviewer, &format!("{}-review@snapkitty.ai", reviewer))?;
// Get HEAD commit (parent)
let head = repo.head()?;
let parent_commit = repo.find_commit(head.target().ok_or(anyhow::anyhow!(
"No HEAD commit found"
))?)?;
// Format commit message with approval metadata
let commit_body = format!(
"{}\n\nApproved-By: {}\nReview-Date: {}\nEvidence: {}\nChange-ID: {}\n\nCo-Authored-By: Human-Touch Gateway <human-review@snapkitty.ai>",
message,
reviewer,
Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
evidence_url,
change_id
);
// Create the commit
let commit_oid = repo.commit(
Some("HEAD"),
&git_sig,
&git_sig,
&commit_body,
&tree,
&[&parent_commit],
)?;
let commit_hash = commit_oid.to_string();
info!(
"β
Commit created: {} (change: {}, reviewer: {})",
commit_hash, change_id, reviewer
);
Ok(commit_hash)
}
/// Reject a commit (prevent it from landing)
pub fn reject_commit(&self, change_id: &str, reason: &str) -> Result<()> {
warn!(
"β Commit rejected for change: {} β Reason: {}",
change_id, reason
);
// Would write rejection to audit log
// Prevent any git operations for this change
Ok(())
}
/// Verify commit signature and metadata
pub fn verify_commit_approval(&self, commit_hash: &str) -> Result<HumanApprovedCommit> {
let repo = Repository::open(&self.repo_path)?;
let oid = git2::Oid::from_str(commit_hash)?;
let commit = repo.find_commit(oid)?;
let message = commit.message().unwrap_or("(no message)");
// Parse approval metadata from commit message
let mut approved_by = "unknown";
let mut approval_date = "unknown";
let mut evidence_url = "unknown";
let mut change_id = "unknown";
for line in message.lines() {
if line.starts_with("Approved-By:") {
approved_by = line.trim_start_matches("Approved-By:").trim();
} else if line.starts_with("Review-Date:") {
approval_date = line.trim_start_matches("Review-Date:").trim();
} else if line.starts_with("Evidence:") {
evidence_url = line.trim_start_matches("Evidence:").trim();
} else if line.starts_with("Change-ID:") {
change_id = line.trim_start_matches("Change-ID:").trim();
}
}
// Verify all required fields are present
if approved_by == "unknown" {
return Err(anyhow::anyhow!("Commit missing Approved-By field"));
}
debug!(
"β
Commit verified: {} approved by {} on {}",
commit_hash, approved_by, approval_date
);
Ok(HumanApprovedCommit {
change_id: change_id.to_string(),
approved_by: approved_by.to_string(),
approval_date: approval_date.to_string(),
evidence_url: evidence_url.to_string(),
review_link: None,
})
}
/// Enforce pre-commit hook: no auto-commits allowed
pub fn check_no_auto_commit(&self, message: &str) -> Result<()> {
// Reject auto-generated commits
if message.contains("[auto]") || message.contains("auto-commit") {
return Err(anyhow::anyhow!(
"β Auto-commits rejected. All changes require human approval."
));
}
// Reject empty messages
if message.trim().is_empty() {
return Err(anyhow::anyhow!("β Commit message cannot be empty"));
}
// Require Approved-By field
if !message.contains("Approved-By:") {
return Err(anyhow::anyhow!(
"β Commit missing Approved-By field. All commits require human approval."
));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_verify_approval_required() {
let gateway = CommitGateway::new(PathBuf::from("."), 3600).unwrap();
assert!(gateway.verify_approval_required("test-123").await.is_ok());
assert!(gateway.verify_approval_required("").await.is_err());
}
#[test]
fn test_create_approval_certificate() {
let gateway = CommitGateway::new(PathBuf::from("."), 3600).unwrap();
let cert = gateway
.create_approval_certificate(
"change-123",
"reviewer@example.com",
"https://example.com/evidence",
)
.unwrap();
assert_eq!(cert.change_id, "change-123");
assert_eq!(cert.reviewer, "reviewer@example.com");
assert!(!cert.signature.is_empty());
}
#[test]
fn test_check_no_auto_commit() {
let gateway = CommitGateway::new(PathBuf::from("."), 3600).unwrap();
// Should reject auto-commits
assert!(gateway
.check_no_auto_commit("feat: [auto] add feature")
.is_err());
// Should reject empty
assert!(gateway.check_no_auto_commit("").is_err());
// Should require Approved-By
assert!(gateway.check_no_auto_commit("feat: add feature").is_err());
// Should accept valid message with approval
assert!(gateway
.check_no_auto_commit("feat: add feature\n\nApproved-By: Human")
.is_ok());
}
}
|