From 018b2f29248600b333b1efd9ad1aa04f75dc7074 Mon Sep 17 00:00:00 2001 From: Drake Date: Mon, 21 Sep 2026 10:07:47 -0500 Subject: [PATCH] feat(inquisitor): implement unified evaluator protocol adapters for constitutional and diff auditing with canary rollback support --- Cargo.toml | 6 + src/lib.rs | 3 + src/protocol_evaluator.rs | 199 ++++++++++++++++++++++++++++++++++ tests/protocol_integration.rs | 157 +++++++++++++++++++++++++++ 4 files changed, 365 insertions(+) create mode 100644 src/protocol_evaluator.rs create mode 100644 tests/protocol_integration.rs diff --git a/Cargo.toml b/Cargo.toml index 9fc4a17..f977274 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,12 @@ serde_json = "1.0" regex = "1.13" chrono = { version = "0.4", features = ["serde"] } colored = "2.2" +uuid = { version = "1.8", features = ["v4"] } +async-trait = "0.1" +tokio = { version = "1.40", features = ["full"] } +p256 = { version = "0.14.0", features = ["ecdsa"] } +aien-protocol-types = { path = "../aien-protocols/crates/aien-protocol-types" } +aien-evaluation-protocol = { path = "../aien-protocols/crates/aien-evaluation-protocol" } [profile.release] opt-level = 3 diff --git a/src/lib.rs b/src/lib.rs index c0720f7..7d72773 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,3 +14,6 @@ pub use evaluator::TestimonyEvaluator as Evaluator; pub use interview::generate_inquisitor_interview; pub use review::{generate_pr_review, PrReview}; pub use triage::{triage_issue, IssueCategory, TriageResult}; + +pub mod protocol_evaluator; +pub use protocol_evaluator::{InquisitorConstitutionalEvaluator, InquisitorDiffEvaluator}; diff --git a/src/protocol_evaluator.rs b/src/protocol_evaluator.rs new file mode 100644 index 0000000..46a115f --- /dev/null +++ b/src/protocol_evaluator.rs @@ -0,0 +1,199 @@ +use crate::audit::DiffAuditor; +use crate::eval::TestimonyEvaluator; +use aien_evaluation_protocol::{ + EvaluationError, EvaluationPlan, Evaluator, EvaluatorDescriptor, EvaluatorIdentity, + EvaluatorOutcome, EvaluatorStatus, Finding, Measurement, +}; +use aien_protocol_types::{ArtifactRef, Digest32}; + +pub struct InquisitorDiffEvaluator { + diff_payload: Option, +} + +impl InquisitorDiffEvaluator { + pub fn new() -> Self { + Self { diff_payload: None } + } + + pub fn with_diff(diff: String) -> Self { + Self { + diff_payload: Some(diff), + } + } +} + +impl Default for InquisitorDiffEvaluator { + fn default() -> Self { + Self::new() + } +} + +#[async_trait::async_trait] +impl Evaluator for InquisitorDiffEvaluator { + fn descriptor(&self) -> EvaluatorDescriptor { + EvaluatorDescriptor { + evaluator_id: "spark-inquisitor-diff-auditor".to_string(), + version: "0.2.0".to_string(), + required: true, + } + } + + async fn evaluate( + &self, + _plan: &EvaluationPlan, + _subject: &ArtifactRef, + ) -> Result { + let diff_text = self.diff_payload.as_deref().unwrap_or(""); + let report = DiffAuditor::audit_diff(diff_text); + + let mut findings = Vec::new(); + for v in &report.violations { + findings.push(Finding { + severity: "Fatal".to_string(), + rule_id: "CONSTITUTIONAL_TELEMETRY".to_string(), + message: v.clone(), + location: None, + }); + } + for s in &report.secret_violations { + findings.push(Finding { + severity: "Fatal".to_string(), + rule_id: "ZERO_DISK_SECRETS".to_string(), + message: s.clone(), + location: None, + }); + } + for u in &report.unslop_violations { + findings.push(Finding { + severity: "InvariantViolation".to_string(), + rule_id: "SOVEREIGN_UNSLOP".to_string(), + message: u.clone(), + location: None, + }); + } + + let status = if report.clean { + EvaluatorStatus::Passed + } else { + EvaluatorStatus::Failed + }; + + let measurements = vec![ + Measurement { + metric: "telemetry_violations".to_string(), + value: report.violations.len() as f64, + unit: "count".to_string(), + }, + Measurement { + metric: "secret_violations".to_string(), + value: report.secret_violations.len() as f64, + unit: "count".to_string(), + }, + Measurement { + metric: "unslop_violations".to_string(), + value: report.unslop_violations.len() as f64, + unit: "count".to_string(), + }, + ]; + + let mut outcome = EvaluatorOutcome { + evaluator: EvaluatorIdentity { + evaluator_id: "spark-inquisitor-diff-auditor".to_string(), + version: "0.2.0".to_string(), + binary_digest: Digest32([0xaa; 32]), + config_digest: Digest32([0xbb; 32]), + }, + status, + findings, + measurements, + evidence: vec![], + execution_digest: Digest32::ZERO, + }; + + outcome.execution_digest = outcome.compute_execution_digest(); + Ok(outcome) + } +} + +pub struct InquisitorConstitutionalEvaluator { + testimony_payload: Option, +} + +impl InquisitorConstitutionalEvaluator { + pub fn new() -> Self { + Self { + testimony_payload: None, + } + } + + pub fn with_testimony(testimony: String) -> Self { + Self { + testimony_payload: Some(testimony), + } + } +} + +impl Default for InquisitorConstitutionalEvaluator { + fn default() -> Self { + Self::new() + } +} + +#[async_trait::async_trait] +impl Evaluator for InquisitorConstitutionalEvaluator { + fn descriptor(&self) -> EvaluatorDescriptor { + EvaluatorDescriptor { + evaluator_id: "spark-inquisitor-constitution".to_string(), + version: "0.2.0".to_string(), + required: true, + } + } + + async fn evaluate( + &self, + _plan: &EvaluationPlan, + _subject: &ArtifactRef, + ) -> Result { + let testimony_text = self.testimony_payload.as_deref().unwrap_or(""); + let verdict = TestimonyEvaluator::evaluate(testimony_text); + + let mut findings = Vec::new(); + if !verdict.approved { + findings.push(Finding { + severity: "Fatal".to_string(), + rule_id: "CONSTITUTIONAL_OATH_INCOMPLETE".to_string(), + message: verdict.reason, + location: None, + }); + } + + let status = if verdict.approved { + EvaluatorStatus::Passed + } else { + EvaluatorStatus::Failed + }; + + let measurements = vec![Measurement { + metric: "alignment_score".to_string(), + value: verdict.score as f64, + unit: "score".to_string(), + }]; + + let mut outcome = EvaluatorOutcome { + evaluator: EvaluatorIdentity { + evaluator_id: "spark-inquisitor-constitution".to_string(), + version: "0.2.0".to_string(), + binary_digest: Digest32([0xcc; 32]), + config_digest: Digest32([0xdd; 32]), + }, + status, + findings, + measurements, + evidence: vec![], + execution_digest: Digest32::ZERO, + }; + + outcome.execution_digest = outcome.compute_execution_digest(); + Ok(outcome) + } +} diff --git a/tests/protocol_integration.rs b/tests/protocol_integration.rs new file mode 100644 index 0000000..11af471 --- /dev/null +++ b/tests/protocol_integration.rs @@ -0,0 +1,157 @@ +use aien_evaluation_protocol::{ + CanaryRollbackHarness, CryptographicTier, EvaluationPlan, Evaluator, EvaluatorDescriptor, + EvaluatorStatus, Verdict, VerifierIdentity, +}; +use aien_protocol_types::{ArtifactRef, Digest32, EvaluationId, Timestamp}; +use p256::ecdsa::{SigningKey, VerifyingKey}; +use spark_inquisitor::{InquisitorConstitutionalEvaluator, InquisitorDiffEvaluator}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; + +fn create_test_plan() -> EvaluationPlan { + let mut plan = EvaluationPlan { + evaluation_id: EvaluationId::new_v4(), + subject: ArtifactRef { + artifact_id: uuid::Uuid::new_v4(), + digest: Digest32([0x11; 32]), + media_type: "text/x-diff".to_string(), + byte_size: 512, + }, + profile: "sovereign-inquisitor-strict".to_string(), + evaluators: vec![ + EvaluatorDescriptor { + evaluator_id: "spark-inquisitor-diff-auditor".to_string(), + version: "0.2.0".to_string(), + required: true, + }, + EvaluatorDescriptor { + evaluator_id: "spark-inquisitor-constitution".to_string(), + version: "0.2.0".to_string(), + required: true, + }, + ], + baseline: None, + sandbox_profile: "airgap".to_string(), + policy_digest: Digest32([0x22; 32]), + evaluator_manifest_digest: Digest32([0x33; 32]), + plan_digest: Digest32::ZERO, + }; + plan.plan_digest = plan.compute_plan_digest(); + plan +} + +#[tokio::test] +async fn test_inquisitor_diff_evaluator_clean_pass() { + let clean_diff = r#" +diff --git a/src/lib.rs b/src/lib.rs +--- a/src/lib.rs ++++ b/src/lib.rs +@@ -1,3 +1,4 @@ + pub fn compute(x: u32) -> u32 { ++ x.saturating_add(42) + } +"#; + + let eval = InquisitorDiffEvaluator::with_diff(clean_diff.to_string()); + let plan = create_test_plan(); + let subject = plan.subject.clone(); + + let outcome = eval.evaluate(&plan, &subject).await.expect("evaluation error"); + assert_eq!(outcome.status, EvaluatorStatus::Passed); + assert!(outcome.findings.is_empty()); + assert_ne!(outcome.execution_digest, Digest32::ZERO); +} + +#[tokio::test] +async fn test_inquisitor_diff_evaluator_telemetry_and_unslop_rejection() { + let dirty_diff = r#" +diff --git a/src/lib.rs b/src/lib.rs +--- a/src/lib.rs ++++ b/src/lib.rs +@@ -1,3 +1,5 @@ ++// This is a crucial game-changer -- unleashing seamless telemetry ++use posthog_rs::Client; + pub fn compute(x: u32) -> u32 { + x + 1 + } +"#; + + let eval = InquisitorDiffEvaluator::with_diff(dirty_diff.to_string()); + let plan = create_test_plan(); + let subject = plan.subject.clone(); + + let outcome = eval.evaluate(&plan, &subject).await.expect("evaluation error"); + assert_eq!(outcome.status, EvaluatorStatus::Failed); + assert!(!outcome.findings.is_empty()); + + let rule_ids: Vec<&str> = outcome.findings.iter().map(|f| f.rule_id.as_str()).collect(); + assert!(rule_ids.contains(&"CONSTITUTIONAL_TELEMETRY"), "Must catch posthog telemetry"); + assert!(rule_ids.contains(&"SOVEREIGN_UNSLOP"), "Must catch em dash and buzzwords"); +} + +#[tokio::test] +async fn test_inquisitor_constitutional_evaluator() { + let good_testimony = "I affirm and swear my commitment to the Sovereign Contributor Oath. I certify zero telemetry, zero surveillance, and complete alignment with the constitution."; + let eval_good = InquisitorConstitutionalEvaluator::with_testimony(good_testimony.to_string()); + + let plan = create_test_plan(); + let subject = plan.subject.clone(); + + let outcome_good = eval_good.evaluate(&plan, &subject).await.expect("evaluation error"); + assert_eq!(outcome_good.status, EvaluatorStatus::Passed); + + let bad_testimony = "I decline to affirm the oath and intend to add tracking."; + let eval_bad = InquisitorConstitutionalEvaluator::with_testimony(bad_testimony.to_string()); + let outcome_bad = eval_bad.evaluate(&plan, &subject).await.expect("evaluation error"); + assert_eq!(outcome_bad.status, EvaluatorStatus::Failed); +} + +#[tokio::test] +async fn test_inquisitor_canary_rollback_integration() { + let signing_key = SigningKey::from_slice(&[88u8; 32]).expect("valid p256 signing key"); + let harness = CanaryRollbackHarness::new( + VerifierIdentity { + principal_id: "spark-inquisitor-sentinel".to_string(), + key_id: "inquisitor-tpm-key".to_string(), + trust_epoch: 1, + trusted_build_digest: Digest32([0x44; 32]), + policy_bundle_digest: Digest32([0x55; 32]), + }, + signing_key, + CryptographicTier::Tier2SoftwareKey, + ); + + let plan = create_test_plan(); + let subject = plan.subject.clone(); + + // 1. Violation triggers rollback + let dirty_diff = "+ let _bad = \"\u{2014} em dash in diff\";"; + let diff_eval = Box::new(InquisitorDiffEvaluator::with_diff(dirty_diff.to_string())); + let const_eval = Box::new(InquisitorConstitutionalEvaluator::with_testimony( + "I certify and affirm the Sovereign Oath.".to_string(), + )); + let evaluators: Vec> = vec![diff_eval, const_eval]; + + let rollback_triggered = Arc::new(AtomicBool::new(false)); + let rollback_clone = rollback_triggered.clone(); + + let (receipt, _) = harness + .evaluate_and_enforce( + &plan, + &subject, + &evaluators, + Timestamp(100), + || async move { + rollback_clone.store(true, Ordering::SeqCst); + Ok(()) + }, + ) + .await + .expect("harness evaluation failed"); + + assert!(rollback_triggered.load(Ordering::SeqCst), "Rollback must execute on unslop violation"); + assert_eq!(receipt.receipt.verdict, Verdict::Fail); + + let verifying_key = VerifyingKey::from(&harness.signing_key); + assert!(receipt.verify(&verifying_key).unwrap()); +}