delegated 0.1.1

Fail-closed trust evaluation for agentic AI systems — delegation tokens, policy enforcement, and audit for agent-to-agent and human-to-agent workflows.
Documentation
use crate::audit::{AuditQuery, AuditReader, read_audit_events};
use crate::delegation_ux::{
    ApprovalCallbackPayload, ApprovalDecision, ConsentReceipt, DelegationGrantProposal,
    issue_consent_receipt, issue_revocation_receipt, to_approval_callback,
};
use crate::engine::simulate_request_policy;
use crate::models::{AuditEvent, PolicyCheck, Violation};
use crate::revocation::TrustStateAdmin;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ApprovalOperation {
    pub receipt: ConsentReceipt,
    pub callback: ApprovalCallbackPayload,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RevocationOperation {
    pub receipt: ConsentReceipt,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PolicySimulationResult {
    pub checks: Vec<PolicyCheck>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OperationalReport {
    pub total_events: usize,
    pub allowed_events: usize,
    pub denied_events: usize,
    pub stage_counts: HashMap<String, usize>,
}

pub fn record_approval_decision(
    proposal: &DelegationGrantProposal,
    decision: ApprovalDecision,
    actor_id: impl Into<String>,
    reason: Option<String>,
    issued_at: DateTime<Utc>,
    token_id: Option<String>,
) -> ApprovalOperation {
    let receipt = issue_consent_receipt(proposal, decision, actor_id, reason, issued_at, token_id);
    let callback = to_approval_callback(&receipt);
    ApprovalOperation { receipt, callback }
}

pub fn revoke_token_with_receipt(
    trust_state: &dyn TrustStateAdmin,
    request_id: impl Into<String>,
    token_id: String,
    actor_id: impl Into<String>,
    reason: Option<String>,
    issued_at: DateTime<Utc>,
) -> Result<RevocationOperation, Violation> {
    trust_state
        .revoke_token(&token_id)
        .map_err(|error| Violation::new("control_plane_revoke", error.to_string()))?;
    let receipt = issue_revocation_receipt(request_id, actor_id, reason, issued_at, token_id);
    Ok(RevocationOperation { receipt })
}

pub fn emergency_deny_agent(
    trust_state: &dyn TrustStateAdmin,
    agent_id: impl Into<String>,
) -> Result<(), Violation> {
    let agent_id = agent_id.into();
    trust_state
        .emergency_deny_agent(&agent_id)
        .map_err(|error| Violation::new("control_plane_emergency_deny", error.to_string()))
}

pub fn simulate_policy(raw_request: &Value) -> Result<PolicySimulationResult, Violation> {
    let checks = simulate_request_policy(raw_request, &crate::models::HostContext::default())?;
    Ok(PolicySimulationResult { checks })
}

pub fn export_audit_events(
    reader: &dyn AuditReader,
    query: AuditQuery,
) -> Result<Vec<AuditEvent>, Violation> {
    read_audit_events(reader, query)
        .map_err(|error| Violation::new("control_plane_audit_export", error.to_string()))
}

pub fn build_operational_report(
    reader: &dyn AuditReader,
    query: AuditQuery,
) -> Result<OperationalReport, Violation> {
    let events = export_audit_events(reader, query)?;
    let mut stage_counts: HashMap<String, usize> = HashMap::new();
    let mut allowed_events = 0usize;
    let mut denied_events = 0usize;
    for event in &events {
        if event.allowed {
            allowed_events += 1;
        } else {
            denied_events += 1;
        }
        *stage_counts.entry(event.stage.clone()).or_insert(0) += 1;
    }
    Ok(OperationalReport {
        total_events: events.len(),
        allowed_events,
        denied_events,
        stage_counts,
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::audit::{AuditSink, JsonlFileAuditSink};
    use crate::models::MaxSpend;
    use crate::revocation::{InMemoryTrustState, TrustStateStore};
    use chrono::TimeZone;

    fn sample_proposal() -> DelegationGrantProposal {
        DelegationGrantProposal {
            request_id: "req_cp_123".to_string(),
            delegator_id: "user:jake-abendroth".to_string(),
            agent_id: "agent:example:scheduler:v1".to_string(),
            owner_id: "org:example".to_string(),
            intent: "schedule_demo".to_string(),
            audience: vec!["tool:google-calendar".to_string()],
            allowed_actions: vec!["calendar.create_event".to_string()],
            max_spend: Some(MaxSpend {
                amount: 0,
                currency: "USD".to_string(),
            }),
            expires_at: Utc
                .with_ymd_and_hms(2026, 6, 1, 20, 40, 0)
                .single()
                .expect("valid timestamp"),
        }
    }

    #[test]
    fn records_approval_and_callback() {
        let now = Utc
            .with_ymd_and_hms(2026, 6, 1, 20, 11, 0)
            .single()
            .expect("valid timestamp");
        let operation = record_approval_decision(
            &sample_proposal(),
            ApprovalDecision::Approve,
            "user:jake-abendroth",
            Some("approved".to_string()),
            now,
            Some("dlg_1".to_string()),
        );
        assert_eq!(operation.receipt.request_id, "req_cp_123");
        assert_eq!(operation.callback.request_id, "req_cp_123");
    }

    #[test]
    fn revokes_token_with_receipt() {
        let state = InMemoryTrustState::new();
        let now = Utc
            .with_ymd_and_hms(2026, 6, 1, 20, 25, 0)
            .single()
            .expect("valid timestamp");
        let operation = revoke_token_with_receipt(
            &state,
            "req_cp_123",
            "dlg_1".to_string(),
            "user:jake-abendroth",
            Some("manual revoke".to_string()),
            now,
        )
        .expect("revocation should succeed");
        assert_eq!(operation.receipt.token_id.as_deref(), Some("dlg_1"));
        assert!(
            state
                .is_token_revoked("dlg_1")
                .expect("state query should succeed")
        );
    }

    #[test]
    fn builds_report_from_exported_audit_events() {
        let path = std::env::temp_dir().join(format!(
            "delegated_cp_audit_{}.jsonl",
            std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .expect("time should be after epoch")
                .as_nanos()
        ));
        let sink = JsonlFileAuditSink::new(path.clone());
        sink.write_event(&AuditEvent {
            occurred_at: Utc
                .with_ymd_and_hms(2026, 6, 1, 20, 20, 0)
                .single()
                .expect("valid timestamp"),
            allowed: true,
            stage: "evaluate_policy".to_string(),
            reason: "ok".to_string(),
            request_id: Some("1".to_string()),
            agent_id: Some("a".to_string()),
            delegator_id: Some("d".to_string()),
            audience: Some("tool".to_string()),
            action: Some("act".to_string()),
            token_id: Some("t".to_string()),
        })
        .expect("write should succeed");
        sink.write_event(&AuditEvent {
            occurred_at: Utc
                .with_ymd_and_hms(2026, 6, 1, 20, 21, 0)
                .single()
                .expect("valid timestamp"),
            allowed: false,
            stage: "verify_signatures".to_string(),
            reason: "denied".to_string(),
            request_id: Some("2".to_string()),
            agent_id: Some("a".to_string()),
            delegator_id: Some("d".to_string()),
            audience: Some("tool".to_string()),
            action: Some("act".to_string()),
            token_id: Some("t".to_string()),
        })
        .expect("write should succeed");

        let report = build_operational_report(
            &sink,
            AuditQuery {
                since: None,
                limit: 10,
            },
        )
        .expect("report should build");
        assert_eq!(report.total_events, 2);
        assert_eq!(report.allowed_events, 1);
        assert_eq!(report.denied_events, 1);
        std::fs::remove_file(path).expect("temporary audit file should be removable");
    }
}