use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::crypto::{hash, Hash, PublicKey, SecretKey, Sig};
use crate::error::{Error, Result};
use crate::event::{EventId, ResourceId};
use super::hitl::Severity;
use super::principal::PrincipalId;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutcomeAttestation {
action_event_id: EventId,
outcome: ActionOutcome,
evidence: Vec<Evidence>,
attestor: Attestor,
observed_at: i64,
signature: Sig,
}
impl OutcomeAttestation {
pub fn builder() -> OutcomeAttestationBuilder {
OutcomeAttestationBuilder::new()
}
pub fn action_event_id(&self) -> EventId {
self.action_event_id
}
pub fn outcome(&self) -> &ActionOutcome {
&self.outcome
}
pub fn evidence(&self) -> &[Evidence] {
&self.evidence
}
pub fn attestor(&self) -> &Attestor {
&self.attestor
}
pub fn observed_at(&self) -> i64 {
self.observed_at
}
pub fn signature(&self) -> &Sig {
&self.signature
}
pub fn canonical_bytes(&self) -> Vec<u8> {
let mut data = Vec::new();
data.extend_from_slice(self.action_event_id.0.as_bytes());
let outcome_json = serde_json::to_vec(&self.outcome).unwrap_or_default();
data.extend_from_slice(&outcome_json);
for evidence in &self.evidence {
let evidence_json = serde_json::to_vec(evidence).unwrap_or_default();
data.extend_from_slice(&evidence_json);
}
let attestor_json = serde_json::to_vec(&self.attestor).unwrap_or_default();
data.extend_from_slice(&attestor_json);
data.extend_from_slice(&self.observed_at.to_le_bytes());
data
}
pub fn verify_signature(&self, public_key: &PublicKey) -> Result<()> {
let message = self.canonical_bytes();
public_key.verify(&message, &self.signature)
}
pub fn verify_against_attestor(&self) -> Result<()> {
let public_key = self.attestor.public_key().ok_or_else(|| {
Error::invalid_input(
"attestor type does not carry a public key; \
use external verification for HumanObserver/CryptographicProof",
)
})?;
self.verify_signature(public_key)
}
pub fn is_evidence_sufficient(&self, severity: Severity) -> bool {
match severity {
Severity::Low => {
true
}
Severity::Medium => {
self.evidence.iter().any(|e| e.is_external())
}
Severity::High => {
let external_count = self.evidence.iter().filter(|e| e.is_external()).count();
external_count >= 2
}
Severity::Critical => {
let has_third_party = self
.evidence
.iter()
.any(|e| matches!(e, Evidence::ThirdPartyAttestation { .. }));
let has_human = matches!(self.attestor, Attestor::HumanObserver { .. });
let has_receipt = self
.evidence
.iter()
.any(|e| matches!(e, Evidence::Receipt { .. }));
let external_count = self.evidence.iter().filter(|e| e.is_external()).count();
let has_corroborated_receipt = has_receipt && external_count >= 2;
has_third_party || has_human || has_corroborated_receipt
}
}
}
pub fn is_self_attestation(&self) -> bool {
matches!(self.attestor, Attestor::SelfAttestation { .. })
}
}
#[derive(Debug, Default)]
pub struct OutcomeAttestationBuilder {
action_event_id: Option<EventId>,
outcome: Option<ActionOutcome>,
evidence: Vec<Evidence>,
attestor: Option<Attestor>,
observed_at: Option<i64>,
}
impl OutcomeAttestationBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn action_event_id(mut self, id: EventId) -> Self {
self.action_event_id = Some(id);
self
}
pub fn outcome(mut self, outcome: ActionOutcome) -> Self {
self.outcome = Some(outcome);
self
}
pub fn evidence(mut self, evidence: Evidence) -> Self {
self.evidence.push(evidence);
self
}
pub fn evidence_list(mut self, evidence: Vec<Evidence>) -> Self {
self.evidence = evidence;
self
}
pub fn attestor(mut self, attestor: Attestor) -> Self {
self.attestor = Some(attestor);
self
}
pub fn observed_at(mut self, timestamp: i64) -> Self {
self.observed_at = Some(timestamp);
self
}
pub fn observed_now(mut self) -> Self {
self.observed_at = Some(chrono::Utc::now().timestamp_millis());
self
}
pub fn sign(self, key: &SecretKey) -> Result<OutcomeAttestation> {
let action_event_id = self
.action_event_id
.ok_or_else(|| Error::invalid_input("action_event_id is required"))?;
let outcome = self
.outcome
.ok_or_else(|| Error::invalid_input("outcome is required"))?;
let attestor = self
.attestor
.ok_or_else(|| Error::invalid_input("attestor is required"))?;
let observed_at = self
.observed_at
.unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
let mut attestation = OutcomeAttestation {
action_event_id,
outcome,
evidence: self.evidence,
attestor,
observed_at,
signature: Sig::empty(), };
let message = attestation.canonical_bytes();
attestation.signature = key.sign(&message);
Ok(attestation)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "status", rename_all = "snake_case")]
pub enum ActionOutcome {
Success {
result: serde_json::Value,
result_hash: Hash,
},
PartialSuccess {
completed: Vec<String>,
failed: Vec<String>,
result: serde_json::Value,
},
Failure {
error: String,
error_code: Option<String>,
recoverable: bool,
},
Pending {
expected_completion: Option<i64>,
},
RolledBack {
rollback_reason: String,
rollback_event_id: EventId,
},
}
impl ActionOutcome {
pub fn success(result: serde_json::Value) -> Self {
let result_hash = hash(result.to_string().as_bytes());
Self::Success {
result,
result_hash,
}
}
pub fn success_with_hash(result: serde_json::Value, result_hash: Hash) -> Self {
Self::Success {
result,
result_hash,
}
}
pub fn partial_success(
completed: Vec<String>,
failed: Vec<String>,
result: serde_json::Value,
) -> Self {
Self::PartialSuccess {
completed,
failed,
result,
}
}
pub fn failure(error: impl Into<String>, recoverable: bool) -> Self {
Self::Failure {
error: error.into(),
error_code: None,
recoverable,
}
}
pub fn failure_with_code(
error: impl Into<String>,
error_code: impl Into<String>,
recoverable: bool,
) -> Self {
Self::Failure {
error: error.into(),
error_code: Some(error_code.into()),
recoverable,
}
}
pub fn pending(expected_completion: Option<i64>) -> Self {
Self::Pending {
expected_completion,
}
}
pub fn rolled_back(reason: impl Into<String>, rollback_event_id: EventId) -> Self {
Self::RolledBack {
rollback_reason: reason.into(),
rollback_event_id,
}
}
pub fn is_success(&self) -> bool {
matches!(self, ActionOutcome::Success { .. })
}
pub fn is_failure(&self) -> bool {
matches!(self, ActionOutcome::Failure { .. })
}
pub fn is_pending(&self) -> bool {
matches!(self, ActionOutcome::Pending { .. })
}
pub fn is_final(&self) -> bool {
!matches!(self, ActionOutcome::Pending { .. })
}
pub fn is_recoverable(&self) -> bool {
match self {
ActionOutcome::Failure { recoverable, .. } => *recoverable,
_ => false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Evidence {
DataHash {
resource: ResourceId,
hash: Hash,
size: u64,
},
ExternalConfirmation {
system: String,
confirmation_id: String,
timestamp: i64,
},
Receipt {
issuer: String,
receipt: Vec<u8>,
},
Visual {
hash: Hash,
description: String,
},
LogEntries {
source: String,
entries: Vec<String>,
hash: Hash,
},
ThirdPartyAttestation {
attestor: PublicKey,
attestation: Vec<u8>,
},
}
impl Evidence {
pub fn data_hash(resource: ResourceId, hash: Hash, size: u64) -> Self {
Self::DataHash {
resource,
hash,
size,
}
}
pub fn external_confirmation(
system: impl Into<String>,
confirmation_id: impl Into<String>,
timestamp: i64,
) -> Self {
Self::ExternalConfirmation {
system: system.into(),
confirmation_id: confirmation_id.into(),
timestamp,
}
}
pub fn receipt(issuer: impl Into<String>, receipt: Vec<u8>) -> Self {
Self::Receipt {
issuer: issuer.into(),
receipt,
}
}
pub fn visual(hash: Hash, description: impl Into<String>) -> Self {
Self::Visual {
hash,
description: description.into(),
}
}
pub fn log_entries(source: impl Into<String>, entries: Vec<String>) -> Self {
let entries_json = serde_json::to_string(&entries).unwrap_or_default();
let hash = hash(entries_json.as_bytes());
Self::LogEntries {
source: source.into(),
entries,
hash,
}
}
pub fn third_party_attestation(attestor: PublicKey, attestation: Vec<u8>) -> Self {
Self::ThirdPartyAttestation {
attestor,
attestation,
}
}
pub fn is_external(&self) -> bool {
matches!(
self,
Evidence::ExternalConfirmation { .. }
| Evidence::Receipt { .. }
| Evidence::ThirdPartyAttestation { .. }
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Attestor {
SelfAttestation {
agent: PublicKey,
},
ExecutionSystem {
system_id: String,
system_key: PublicKey,
},
Monitor {
monitor_id: String,
monitor_key: PublicKey,
},
HumanObserver {
principal: PrincipalId,
},
CryptographicProof {
proof_type: String,
},
}
impl Attestor {
pub fn self_attestation(agent: PublicKey) -> Self {
Self::SelfAttestation { agent }
}
pub fn execution_system(system_id: impl Into<String>, system_key: PublicKey) -> Self {
Self::ExecutionSystem {
system_id: system_id.into(),
system_key,
}
}
pub fn monitor(monitor_id: impl Into<String>, monitor_key: PublicKey) -> Self {
Self::Monitor {
monitor_id: monitor_id.into(),
monitor_key,
}
}
pub fn human_observer(principal: PrincipalId) -> Self {
Self::HumanObserver { principal }
}
pub fn cryptographic_proof(proof_type: impl Into<String>) -> Self {
Self::CryptographicProof {
proof_type: proof_type.into(),
}
}
pub fn public_key(&self) -> Option<&PublicKey> {
match self {
Attestor::SelfAttestation { agent } => Some(agent),
Attestor::ExecutionSystem { system_key, .. } => Some(system_key),
Attestor::Monitor { monitor_key, .. } => Some(monitor_key),
Attestor::HumanObserver { .. } => None,
Attestor::CryptographicProof { .. } => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct IdempotencyKey {
agent: PublicKey,
action_type: String,
client_key: String,
}
impl IdempotencyKey {
pub fn new(
agent: PublicKey,
action_type: impl Into<String>,
client_key: impl Into<String>,
) -> Self {
Self {
agent,
action_type: action_type.into(),
client_key: client_key.into(),
}
}
pub fn agent(&self) -> &PublicKey {
&self.agent
}
pub fn action_type(&self) -> &str {
&self.action_type
}
pub fn client_key(&self) -> &str {
&self.client_key
}
pub fn hash(&self) -> Hash {
let mut data = Vec::new();
data.extend_from_slice(&self.agent.as_bytes());
data.extend_from_slice(self.action_type.as_bytes());
data.extend_from_slice(self.client_key.as_bytes());
hash(&data)
}
}
impl std::fmt::Display for IdempotencyKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}:{}:{}",
hex::encode(self.agent.as_bytes()),
self.action_type,
self.client_key
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IdempotencyRecord {
key: IdempotencyKey,
original_event_id: EventId,
outcome: ActionOutcome,
created_at: i64,
expires_at: i64,
}
impl IdempotencyRecord {
pub fn new(
key: IdempotencyKey,
original_event_id: EventId,
outcome: ActionOutcome,
ttl_ms: i64,
) -> Self {
let now = chrono::Utc::now().timestamp_millis();
Self {
key,
original_event_id,
outcome,
created_at: now,
expires_at: now + ttl_ms,
}
}
pub fn key(&self) -> &IdempotencyKey {
&self.key
}
pub fn original_event_id(&self) -> EventId {
self.original_event_id
}
pub fn outcome(&self) -> &ActionOutcome {
&self.outcome
}
pub fn created_at(&self) -> i64 {
self.created_at
}
pub fn expires_at(&self) -> i64 {
self.expires_at
}
pub fn is_expired(&self) -> bool {
chrono::Utc::now().timestamp_millis() > self.expires_at
}
pub fn is_valid(&self) -> bool {
!self.is_expired()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutcomeDispute {
disputed_attestation_event_id: EventId,
disputant: Attestor,
reason: String,
counter_evidence: Vec<Evidence>,
filed_at: i64,
status: DisputeStatus,
}
impl OutcomeDispute {
pub fn new(
disputed_attestation_event_id: EventId,
disputant: Attestor,
reason: impl Into<String>,
) -> Self {
Self {
disputed_attestation_event_id,
disputant,
reason: reason.into(),
counter_evidence: Vec::new(),
filed_at: chrono::Utc::now().timestamp_millis(),
status: DisputeStatus::Pending,
}
}
pub fn with_evidence(mut self, evidence: Evidence) -> Self {
self.counter_evidence.push(evidence);
self
}
pub fn disputed_attestation_event_id(&self) -> EventId {
self.disputed_attestation_event_id
}
pub fn disputant(&self) -> &Attestor {
&self.disputant
}
pub fn reason(&self) -> &str {
&self.reason
}
pub fn counter_evidence(&self) -> &[Evidence] {
&self.counter_evidence
}
pub fn filed_at(&self) -> i64 {
self.filed_at
}
pub fn status(&self) -> &DisputeStatus {
&self.status
}
pub fn set_status(&mut self, status: DisputeStatus) {
self.status = status;
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "status", rename_all = "snake_case")]
pub enum DisputeStatus {
Pending,
UnderReview {
reviewer: PrincipalId,
started_at: i64,
},
RejectedOriginalStands {
reason: String,
resolution_event_id: EventId,
},
UpheldOriginalInvalidated {
reason: String,
corrected_outcome: Option<Box<ActionOutcome>>,
resolution_event_id: EventId,
},
}
impl DisputeStatus {
pub fn is_pending(&self) -> bool {
matches!(
self,
DisputeStatus::Pending | DisputeStatus::UnderReview { .. }
)
}
pub fn is_resolved(&self) -> bool {
!self.is_pending()
}
}
#[derive(Debug, Default)]
pub struct IdempotencyStore {
records: HashMap<Hash, IdempotencyRecord>,
}
impl IdempotencyStore {
pub fn new() -> Self {
Self::default()
}
pub fn insert(&mut self, record: IdempotencyRecord) {
let key_hash = record.key().hash();
self.records.insert(key_hash, record);
}
pub fn lookup(&self, key: &IdempotencyKey) -> Option<&IdempotencyRecord> {
self.records.get(&key.hash()).filter(|r| !r.is_expired())
}
pub fn cleanup(&mut self) -> usize {
let before = self.records.len();
self.records.retain(|_, r| !r.is_expired());
before - self.records.len()
}
pub fn len(&self) -> usize {
self.records.len()
}
pub fn is_empty(&self) -> bool {
self.records.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::ResourceKind;
fn test_event_id() -> EventId {
EventId(hash(b"test-event"))
}
fn test_key() -> SecretKey {
SecretKey::generate()
}
fn test_resource_id() -> ResourceId {
ResourceId::new(ResourceKind::File, "/tmp/test.txt")
}
#[test]
fn outcome_success() {
let outcome = ActionOutcome::success(serde_json::json!({"result": "ok"}));
assert!(outcome.is_success());
assert!(!outcome.is_failure());
assert!(outcome.is_final());
}
#[test]
fn outcome_failure() {
let outcome = ActionOutcome::failure("Something went wrong", true);
assert!(outcome.is_failure());
assert!(outcome.is_recoverable());
let non_recoverable = ActionOutcome::failure("Fatal error", false);
assert!(!non_recoverable.is_recoverable());
}
#[test]
fn outcome_partial_success() {
let outcome = ActionOutcome::partial_success(
vec!["step1".to_string(), "step2".to_string()],
vec!["step3".to_string()],
serde_json::json!({}),
);
assert!(!outcome.is_success());
assert!(!outcome.is_failure());
}
#[test]
fn outcome_pending() {
let outcome = ActionOutcome::pending(Some(chrono::Utc::now().timestamp_millis() + 60000));
assert!(outcome.is_pending());
assert!(!outcome.is_final());
}
#[test]
fn outcome_rolled_back() {
let outcome = ActionOutcome::rolled_back("User cancelled", test_event_id());
assert!(outcome.is_final());
}
#[test]
fn evidence_data_hash() {
let evidence = Evidence::data_hash(test_resource_id(), hash(b"data"), 1024);
assert!(!evidence.is_external());
}
#[test]
fn evidence_external_confirmation() {
let evidence = Evidence::external_confirmation("github", "pr-123", 1000);
assert!(evidence.is_external());
}
#[test]
fn evidence_receipt() {
let evidence = Evidence::receipt("blockchain", vec![1, 2, 3, 4]);
assert!(evidence.is_external());
}
#[test]
fn evidence_visual() {
let evidence = Evidence::visual(hash(b"screenshot"), "Shows successful deployment");
assert!(!evidence.is_external());
}
#[test]
fn evidence_log_entries() {
let evidence = Evidence::log_entries("server.log", vec!["INFO: Started".to_string()]);
assert!(!evidence.is_external());
}
#[test]
fn evidence_third_party() {
let key = test_key();
let evidence = Evidence::third_party_attestation(key.public_key(), vec![1, 2, 3]);
assert!(evidence.is_external());
}
#[test]
fn attestor_self() {
let key = test_key();
let attestor = Attestor::self_attestation(key.public_key());
assert!(attestor.public_key().is_some());
}
#[test]
fn attestor_execution_system() {
let key = test_key();
let attestor = Attestor::execution_system("docker-runtime", key.public_key());
assert!(attestor.public_key().is_some());
}
#[test]
fn attestor_human() {
let principal = PrincipalId::user("user@example.com").unwrap();
let attestor = Attestor::human_observer(principal);
assert!(attestor.public_key().is_none());
}
#[test]
fn attestation_build_and_sign() {
let key = test_key();
let attestation = OutcomeAttestation::builder()
.action_event_id(test_event_id())
.outcome(ActionOutcome::success(serde_json::json!({})))
.attestor(Attestor::self_attestation(key.public_key()))
.observed_now()
.sign(&key)
.unwrap();
assert!(attestation.verify_signature(&key.public_key()).is_ok());
}
#[test]
fn attestation_requires_action_event_id() {
let key = test_key();
let result = OutcomeAttestation::builder()
.outcome(ActionOutcome::success(serde_json::json!({})))
.attestor(Attestor::self_attestation(key.public_key()))
.sign(&key);
assert!(result.is_err());
}
#[test]
fn attestation_requires_outcome() {
let key = test_key();
let result = OutcomeAttestation::builder()
.action_event_id(test_event_id())
.attestor(Attestor::self_attestation(key.public_key()))
.sign(&key);
assert!(result.is_err());
}
#[test]
fn attestation_requires_attestor() {
let key = test_key();
let result = OutcomeAttestation::builder()
.action_event_id(test_event_id())
.outcome(ActionOutcome::success(serde_json::json!({})))
.sign(&key);
assert!(result.is_err());
}
#[test]
fn evidence_sufficiency_low() {
let key = test_key();
let attestation = OutcomeAttestation::builder()
.action_event_id(test_event_id())
.outcome(ActionOutcome::success(serde_json::json!({})))
.attestor(Attestor::self_attestation(key.public_key()))
.sign(&key)
.unwrap();
assert!(attestation.is_evidence_sufficient(Severity::Low));
}
#[test]
fn evidence_sufficiency_medium() {
let key = test_key();
let attestation = OutcomeAttestation::builder()
.action_event_id(test_event_id())
.outcome(ActionOutcome::success(serde_json::json!({})))
.attestor(Attestor::self_attestation(key.public_key()))
.sign(&key)
.unwrap();
assert!(!attestation.is_evidence_sufficient(Severity::Medium));
let attestation = OutcomeAttestation::builder()
.action_event_id(test_event_id())
.outcome(ActionOutcome::success(serde_json::json!({})))
.attestor(Attestor::self_attestation(key.public_key()))
.evidence(Evidence::external_confirmation("ci", "build-123", 1000))
.sign(&key)
.unwrap();
assert!(attestation.is_evidence_sufficient(Severity::Medium));
}
#[test]
fn evidence_sufficiency_high() {
let key = test_key();
let attestation = OutcomeAttestation::builder()
.action_event_id(test_event_id())
.outcome(ActionOutcome::success(serde_json::json!({})))
.attestor(Attestor::self_attestation(key.public_key()))
.evidence(Evidence::external_confirmation("ci", "build-123", 1000))
.sign(&key)
.unwrap();
assert!(!attestation.is_evidence_sufficient(Severity::High));
let attestation = OutcomeAttestation::builder()
.action_event_id(test_event_id())
.outcome(ActionOutcome::success(serde_json::json!({})))
.attestor(Attestor::self_attestation(key.public_key()))
.evidence(Evidence::external_confirmation("ci", "build-123", 1000))
.evidence(Evidence::receipt("notary", vec![1, 2, 3]))
.sign(&key)
.unwrap();
assert!(attestation.is_evidence_sufficient(Severity::High));
}
#[test]
fn evidence_sufficiency_critical() {
let key = test_key();
let attestation = OutcomeAttestation::builder()
.action_event_id(test_event_id())
.outcome(ActionOutcome::success(serde_json::json!({})))
.attestor(Attestor::self_attestation(key.public_key()))
.evidence(Evidence::external_confirmation("ci", "build-123", 1000))
.sign(&key)
.unwrap();
assert!(!attestation.is_evidence_sufficient(Severity::Critical));
let attestation = OutcomeAttestation::builder()
.action_event_id(test_event_id())
.outcome(ActionOutcome::success(serde_json::json!({})))
.attestor(Attestor::self_attestation(key.public_key()))
.evidence(Evidence::third_party_attestation(
key.public_key(),
vec![1, 2, 3],
))
.sign(&key)
.unwrap();
assert!(attestation.is_evidence_sufficient(Severity::Critical));
let principal = PrincipalId::user("admin@example.com").unwrap();
let attestation = OutcomeAttestation::builder()
.action_event_id(test_event_id())
.outcome(ActionOutcome::success(serde_json::json!({})))
.attestor(Attestor::human_observer(principal))
.sign(&key)
.unwrap();
assert!(attestation.is_evidence_sufficient(Severity::Critical));
}
#[test]
fn verify_against_attestor_rejects_key_mismatch() {
let real_key = test_key();
let fake_key = test_key();
let attestation = OutcomeAttestation::builder()
.action_event_id(test_event_id())
.outcome(ActionOutcome::success(serde_json::json!({})))
.attestor(Attestor::self_attestation(fake_key.public_key())) .observed_now()
.sign(&real_key) .unwrap();
assert!(attestation.verify_signature(&real_key.public_key()).is_ok());
assert!(attestation.verify_against_attestor().is_err());
}
#[test]
fn verify_against_attestor_accepts_matching_key() {
let key = test_key();
let attestation = OutcomeAttestation::builder()
.action_event_id(test_event_id())
.outcome(ActionOutcome::success(serde_json::json!({})))
.attestor(Attestor::self_attestation(key.public_key()))
.observed_now()
.sign(&key)
.unwrap();
assert!(attestation.verify_against_attestor().is_ok());
}
#[test]
fn verify_against_attestor_for_human_observer_returns_error() {
let key = test_key();
let principal = PrincipalId::user("admin@example.com").unwrap();
let attestation = OutcomeAttestation::builder()
.action_event_id(test_event_id())
.outcome(ActionOutcome::success(serde_json::json!({})))
.attestor(Attestor::human_observer(principal))
.observed_now()
.sign(&key)
.unwrap();
let result = attestation.verify_against_attestor();
assert!(result.is_err());
}
#[test]
fn verify_against_attestor_for_execution_system() {
let system_key = test_key();
let attestation = OutcomeAttestation::builder()
.action_event_id(test_event_id())
.outcome(ActionOutcome::success(serde_json::json!({})))
.attestor(Attestor::execution_system(
"docker",
system_key.public_key(),
))
.observed_now()
.sign(&system_key)
.unwrap();
assert!(attestation.verify_against_attestor().is_ok());
}
#[test]
fn verify_against_attestor_for_monitor() {
let monitor_key = test_key();
let attestation = OutcomeAttestation::builder()
.action_event_id(test_event_id())
.outcome(ActionOutcome::success(serde_json::json!({})))
.attestor(Attestor::monitor("prometheus", monitor_key.public_key()))
.observed_now()
.sign(&monitor_key)
.unwrap();
assert!(attestation.verify_against_attestor().is_ok());
}
#[test]
fn verify_against_attestor_for_cryptographic_proof_returns_error() {
let key = test_key();
let attestation = OutcomeAttestation::builder()
.action_event_id(test_event_id())
.outcome(ActionOutcome::success(serde_json::json!({})))
.attestor(Attestor::cryptographic_proof("blockchain-anchor"))
.observed_now()
.sign(&key)
.unwrap();
assert!(attestation.verify_against_attestor().is_err());
}
#[test]
fn idempotency_key_hash() {
let key = test_key();
let idem_key1 = IdempotencyKey::new(key.public_key(), "file_write", "request-123");
let idem_key2 = IdempotencyKey::new(key.public_key(), "file_write", "request-123");
assert_eq!(idem_key1.hash(), idem_key2.hash());
let idem_key3 = IdempotencyKey::new(key.public_key(), "file_write", "request-456");
assert_ne!(idem_key1.hash(), idem_key3.hash());
}
#[test]
fn idempotency_key_display() {
let key = test_key();
let idem_key = IdempotencyKey::new(key.public_key(), "file_write", "request-123");
let display = format!("{}", idem_key);
assert!(display.contains("file_write"));
assert!(display.contains("request-123"));
}
#[test]
fn idempotency_record_valid() {
let key = test_key();
let idem_key = IdempotencyKey::new(key.public_key(), "file_write", "request-123");
let record = IdempotencyRecord::new(
idem_key,
test_event_id(),
ActionOutcome::success(serde_json::json!({})),
60000, );
assert!(record.is_valid());
assert!(!record.is_expired());
}
#[test]
fn idempotency_record_expired() {
let key = test_key();
let idem_key = IdempotencyKey::new(key.public_key(), "file_write", "request-123");
let record = IdempotencyRecord::new(
idem_key,
test_event_id(),
ActionOutcome::success(serde_json::json!({})),
-1, );
assert!(!record.is_valid());
assert!(record.is_expired());
}
#[test]
fn idempotency_store_insert_and_lookup() {
let mut store = IdempotencyStore::new();
let key = test_key();
let idem_key = IdempotencyKey::new(key.public_key(), "write", "req-1");
let record = IdempotencyRecord::new(
idem_key.clone(),
test_event_id(),
ActionOutcome::success(serde_json::json!({})),
60000,
);
let expected_event_id = record.original_event_id();
store.insert(record);
let found = store.lookup(&idem_key);
assert!(found.is_some());
assert_eq!(found.unwrap().original_event_id(), expected_event_id);
}
#[test]
fn idempotency_store_returns_none_for_unknown() {
let store = IdempotencyStore::new();
let key = test_key();
let idem_key = IdempotencyKey::new(key.public_key(), "write", "unknown");
assert!(store.lookup(&idem_key).is_none());
}
#[test]
fn idempotency_store_expired_records_not_returned() {
let mut store = IdempotencyStore::new();
let key = test_key();
let idem_key = IdempotencyKey::new(key.public_key(), "write", "req-1");
let record = IdempotencyRecord::new(
idem_key.clone(),
test_event_id(),
ActionOutcome::success(serde_json::json!({})),
-1, );
store.insert(record);
assert!(store.lookup(&idem_key).is_none()); }
#[test]
fn idempotency_store_cleanup_removes_expired() {
let mut store = IdempotencyStore::new();
let key = test_key();
for i in 0..3 {
let idem_key = IdempotencyKey::new(key.public_key(), "write", format!("expired-{}", i));
store.insert(IdempotencyRecord::new(
idem_key,
test_event_id(),
ActionOutcome::success(serde_json::json!({})),
-1, ));
}
for i in 0..2 {
let idem_key = IdempotencyKey::new(key.public_key(), "write", format!("valid-{}", i));
store.insert(IdempotencyRecord::new(
idem_key,
test_event_id(),
ActionOutcome::success(serde_json::json!({})),
60000, ));
}
assert_eq!(store.len(), 5);
let removed = store.cleanup();
assert_eq!(removed, 3);
assert_eq!(store.len(), 2);
}
#[test]
fn idempotency_store_overwrite_existing() {
let mut store = IdempotencyStore::new();
let key = test_key();
let idem_key = IdempotencyKey::new(key.public_key(), "write", "req-1");
let record1 = IdempotencyRecord::new(
idem_key.clone(),
test_event_id(),
ActionOutcome::success(serde_json::json!({"version": 1})),
60000,
);
store.insert(record1);
let event2 = EventId(hash(b"second-event"));
let record2 = IdempotencyRecord::new(
idem_key.clone(),
event2,
ActionOutcome::success(serde_json::json!({"version": 2})),
60000,
);
store.insert(record2);
assert_eq!(store.len(), 1);
let found = store.lookup(&idem_key).unwrap();
assert_eq!(found.original_event_id(), event2);
}
#[test]
fn idempotency_store_is_empty() {
let store = IdempotencyStore::new();
assert!(store.is_empty());
assert_eq!(store.len(), 0);
}
#[test]
fn dispute_creation() {
let key = test_key();
let dispute = OutcomeDispute::new(
test_event_id(),
Attestor::self_attestation(key.public_key()),
"Outcome was not as described",
);
assert!(dispute.status().is_pending());
assert!(!dispute.status().is_resolved());
}
#[test]
fn dispute_with_evidence() {
let key = test_key();
let dispute = OutcomeDispute::new(
test_event_id(),
Attestor::self_attestation(key.public_key()),
"Incorrect outcome",
)
.with_evidence(Evidence::log_entries(
"server.log",
vec!["ERROR: Failed".to_string()],
));
assert_eq!(dispute.counter_evidence().len(), 1);
}
#[test]
fn dispute_status_transitions() {
let principal = PrincipalId::user("reviewer@example.com").unwrap();
let pending = DisputeStatus::Pending;
assert!(pending.is_pending());
let under_review = DisputeStatus::UnderReview {
reviewer: principal.clone(),
started_at: chrono::Utc::now().timestamp_millis(),
};
assert!(under_review.is_pending());
let rejected = DisputeStatus::RejectedOriginalStands {
reason: "Evidence insufficient".to_string(),
resolution_event_id: test_event_id(),
};
assert!(rejected.is_resolved());
let upheld = DisputeStatus::UpheldOriginalInvalidated {
reason: "Clear evidence of error".to_string(),
corrected_outcome: Some(Box::new(ActionOutcome::failure("Actual failure", false))),
resolution_event_id: test_event_id(),
};
assert!(upheld.is_resolved());
}
#[test]
fn evidence_receipt_alone_insufficient_for_critical() {
let key = test_key();
let attestation = OutcomeAttestation::builder()
.action_event_id(test_event_id())
.outcome(ActionOutcome::success(serde_json::json!({})))
.attestor(Attestor::self_attestation(key.public_key()))
.evidence(Evidence::receipt("self-system", vec![1, 2, 3]))
.sign(&key)
.unwrap();
assert!(!attestation.is_evidence_sufficient(Severity::Critical));
}
#[test]
fn evidence_third_party_attestation_satisfies_critical() {
let key = test_key();
let third_party_key = SecretKey::generate();
let attestation = OutcomeAttestation::builder()
.action_event_id(test_event_id())
.outcome(ActionOutcome::success(serde_json::json!({})))
.attestor(Attestor::self_attestation(key.public_key()))
.evidence(Evidence::third_party_attestation(
third_party_key.public_key(),
vec![1, 2, 3],
))
.sign(&key)
.unwrap();
assert!(attestation.is_evidence_sufficient(Severity::Critical));
}
#[test]
fn evidence_human_observer_satisfies_critical() {
let key = test_key();
let principal = PrincipalId::user("admin@example.com").unwrap();
let attestation = OutcomeAttestation::builder()
.action_event_id(test_event_id())
.outcome(ActionOutcome::success(serde_json::json!({})))
.attestor(Attestor::human_observer(principal))
.sign(&key)
.unwrap();
assert!(attestation.is_evidence_sufficient(Severity::Critical));
}
#[test]
fn evidence_receipt_plus_external_confirmation_satisfies_critical() {
let key = test_key();
let attestation = OutcomeAttestation::builder()
.action_event_id(test_event_id())
.outcome(ActionOutcome::success(serde_json::json!({})))
.attestor(Attestor::self_attestation(key.public_key()))
.evidence(Evidence::receipt("notary-service", vec![1, 2, 3]))
.evidence(Evidence::external_confirmation(
"monitoring",
"check-456",
chrono::Utc::now().timestamp_millis(),
))
.sign(&key)
.unwrap();
assert!(attestation.is_evidence_sufficient(Severity::Critical));
}
#[test]
fn evidence_external_confirmation_alone_insufficient_for_critical() {
let key = test_key();
let attestation = OutcomeAttestation::builder()
.action_event_id(test_event_id())
.outcome(ActionOutcome::success(serde_json::json!({})))
.attestor(Attestor::self_attestation(key.public_key()))
.evidence(Evidence::external_confirmation(
"monitoring",
"check-789",
chrono::Utc::now().timestamp_millis(),
))
.sign(&key)
.unwrap();
assert!(!attestation.is_evidence_sufficient(Severity::Critical));
}
#[test]
fn evidence_no_evidence_insufficient_for_critical() {
let key = test_key();
let attestation = OutcomeAttestation::builder()
.action_event_id(test_event_id())
.outcome(ActionOutcome::success(serde_json::json!({})))
.attestor(Attestor::self_attestation(key.public_key()))
.sign(&key)
.unwrap();
assert!(!attestation.is_evidence_sufficient(Severity::Critical));
}
#[test]
fn evidence_low_severity_always_sufficient() {
let key = test_key();
let attestation = OutcomeAttestation::builder()
.action_event_id(test_event_id())
.outcome(ActionOutcome::success(serde_json::json!({})))
.attestor(Attestor::self_attestation(key.public_key()))
.sign(&key)
.unwrap();
assert!(attestation.is_evidence_sufficient(Severity::Low));
}
#[test]
fn evidence_medium_requires_external() {
let key = test_key();
let attestation = OutcomeAttestation::builder()
.action_event_id(test_event_id())
.outcome(ActionOutcome::success(serde_json::json!({})))
.attestor(Attestor::self_attestation(key.public_key()))
.sign(&key)
.unwrap();
assert!(!attestation.is_evidence_sufficient(Severity::Medium));
let attestation2 = OutcomeAttestation::builder()
.action_event_id(test_event_id())
.outcome(ActionOutcome::success(serde_json::json!({})))
.attestor(Attestor::self_attestation(key.public_key()))
.evidence(Evidence::external_confirmation(
"ci",
"run-123",
chrono::Utc::now().timestamp_millis(),
))
.sign(&key)
.unwrap();
assert!(attestation2.is_evidence_sufficient(Severity::Medium));
}
}