a1-ai 2.8.0

A1 — The cryptographic identity and authorization layer that turns anonymous AI agents into accountable, verifiable entities. One Identity. Full Provenance.
Documentation
use crate::error::A1Error;
use crate::identity::Signer;
use crate::registry::fresh_nonce;
use blake3::Hasher;
use serde::{Deserialize, Serialize};

const DOMAIN_GOV_POLICY: &str = "a1::dyolo::governance::policy::v2.8.0";
const DOMAIN_GOV_APPROVAL: &str = "a1::dyolo::governance::approval::v2.8.0";
const DOMAIN_GOV_AUDIT: &str = "a1::dyolo::governance::audit::v2.8.0";

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApprovalGate {
    pub capability: String,
    pub approver_did: String,
    pub approval_ttl_secs: u64,
    #[serde(default)]
    pub allow_retroactive: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApprovalToken {
    pub capability: String,
    pub approver_did: String,
    pub agent_did: String,
    pub action_id: String,
    pub granted_at_unix: u64,
    pub expires_at_unix: u64,
    pub nonce: String,
    pub signature: String,
}

impl ApprovalToken {
    pub fn issue(
        approver: &dyn Signer,
        capability: impl Into<String>,
        agent_did: impl Into<String>,
        action_id: impl Into<String>,
        granted_at: u64,
        ttl_secs: u64,
    ) -> Self {
        let vk = approver.verifying_key();
        let approver_did = format!("did:a1:{}", hex::encode(vk.as_bytes()));
        let capability = capability.into();
        let agent_did = agent_did.into();
        let action_id = action_id.into();
        let nonce = fresh_nonce();
        let expires = granted_at + ttl_secs;
        let msg = approval_signable(
            &capability,
            &approver_did,
            &agent_did,
            &action_id,
            granted_at,
            expires,
            &nonce,
        );
        let sig = approver.sign_message(&msg);
        Self {
            capability,
            approver_did,
            agent_did,
            action_id,
            granted_at_unix: granted_at,
            expires_at_unix: expires,
            nonce: hex::encode(nonce),
            signature: hex::encode(sig.to_bytes()),
        }
    }

    pub fn verify(&self, now_unix: u64) -> Result<(), A1Error> {
        if now_unix > self.expires_at_unix {
            return Err(A1Error::Expired(0, self.expires_at_unix, now_unix));
        }
        let pk_hex = self
            .approver_did
            .strip_prefix("did:a1:")
            .ok_or_else(|| A1Error::WireFormatError("invalid approver DID".into()))?;
        let pk_bytes =
            hex::decode(pk_hex).map_err(|_| A1Error::WireFormatError("invalid DID hex".into()))?;
        let pk_arr: [u8; 32] = pk_bytes
            .try_into()
            .map_err(|_| A1Error::WireFormatError("key must be 32 bytes".into()))?;
        let vk = ed25519_dalek::VerifyingKey::from_bytes(&pk_arr)
            .map_err(|_| A1Error::WireFormatError("invalid Ed25519 key".into()))?;
        let nonce_bytes = hex::decode(&self.nonce)
            .map_err(|_| A1Error::WireFormatError("invalid nonce hex".into()))?;
        let nonce: [u8; 16] = nonce_bytes
            .try_into()
            .map_err(|_| A1Error::WireFormatError("nonce must be 16 bytes".into()))?;
        let msg = approval_signable(
            &self.capability,
            &self.approver_did,
            &self.agent_did,
            &self.action_id,
            self.granted_at_unix,
            self.expires_at_unix,
            &nonce,
        );
        let sig_bytes = hex::decode(&self.signature)
            .map_err(|_| A1Error::WireFormatError("invalid sig hex".into()))?;
        let sig_arr: [u8; 64] = sig_bytes
            .try_into()
            .map_err(|_| A1Error::WireFormatError("sig must be 64 bytes".into()))?;
        let sig = ed25519_dalek::Signature::from_bytes(&sig_arr);
        use ed25519_dalek::Verifier;
        vk.verify(&msg, &sig)
            .map_err(|_| A1Error::HybridSignatureInvalid {
                component: "approval-token",
            })
    }
}

fn approval_signable(
    capability: &str,
    approver_did: &str,
    agent_did: &str,
    action_id: &str,
    granted_at: u64,
    expires_at: u64,
    nonce: &[u8; 16],
) -> Vec<u8> {
    let mut h = Hasher::new_derive_key(DOMAIN_GOV_APPROVAL);
    for s in [capability, approver_did, agent_did, action_id] {
        h.update(&(s.len() as u64).to_le_bytes());
        h.update(s.as_bytes());
    }
    h.update(&granted_at.to_le_bytes());
    h.update(&expires_at.to_le_bytes());
    h.update(nonce);
    h.finalize().as_bytes().to_vec()
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeyRotationPolicy {
    pub max_age_days: u32,
    pub mandatory: bool,
    pub rotation_authority_did: Option<String>,
}

impl Default for KeyRotationPolicy {
    fn default() -> Self {
        Self {
            max_age_days: 90,
            mandatory: true,
            rotation_authority_did: None,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum RotationStatus {
    Valid { days_remaining: u64 },
    Recommended { age_days: u64 },
    Required { age_days: u64 },
}

impl KeyRotationPolicy {
    pub fn check(&self, issued_at_unix: u64, now_unix: u64) -> RotationStatus {
        let age_days = now_unix.saturating_sub(issued_at_unix) / 86400;
        if age_days >= self.max_age_days as u64 {
            if self.mandatory {
                RotationStatus::Required { age_days }
            } else {
                RotationStatus::Recommended { age_days }
            }
        } else {
            RotationStatus::Valid {
                days_remaining: self.max_age_days as u64 - age_days,
            }
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GovernancePolicy {
    #[serde(default)]
    pub approval_gates: Vec<ApprovalGate>,
    #[serde(default)]
    pub key_rotation: KeyRotationPolicy,
    #[serde(default = "default_max_depth")]
    pub max_chain_depth: u8,
    #[serde(default)]
    pub allowed_namespaces: Vec<String>,
    #[serde(default)]
    pub blocked_capabilities: Vec<String>,
    #[serde(default)]
    pub require_human_approval_for: Vec<String>,
    #[serde(default = "default_true")]
    pub audit_all_authorizations: bool,
}

fn default_max_depth() -> u8 {
    16
}
fn default_true() -> bool {
    true
}

impl Default for GovernancePolicy {
    fn default() -> Self {
        Self {
            approval_gates: vec![],
            key_rotation: KeyRotationPolicy::default(),
            max_chain_depth: 16,
            allowed_namespaces: vec![],
            blocked_capabilities: vec![],
            require_human_approval_for: vec![],
            audit_all_authorizations: true,
        }
    }
}

impl GovernancePolicy {
    pub fn from_json(json: &str) -> Result<Self, A1Error> {
        serde_json::from_str(json).map_err(|e| A1Error::WireFormatError(e.to_string()))
    }
    pub fn from_env() -> Result<Option<Self>, A1Error> {
        match std::env::var("A1_GOVERNANCE_POLICY_FILE") {
            Ok(path) => {
                let json = std::fs::read_to_string(&path)
                    .map_err(|e| A1Error::WireFormatError(format!("cannot read {path}: {e}")))?;
                Ok(Some(Self::from_json(&json)?))
            }
            Err(_) => Ok(None),
        }
    }
    pub fn check_namespace(&self, ns: &str) -> Result<(), A1Error> {
        if self.allowed_namespaces.is_empty() || self.allowed_namespaces.iter().any(|n| n == ns) {
            Ok(())
        } else {
            Err(A1Error::PolicyViolation(format!(
                "namespace '{ns}' not in governance allowlist"
            )))
        }
    }
    pub fn check_capability_not_blocked(&self, cap: &str) -> Result<(), A1Error> {
        if self.blocked_capabilities.iter().any(|c| c == cap) {
            Err(A1Error::PolicyViolation(format!(
                "capability '{cap}' is blocked by governance policy"
            )))
        } else {
            Ok(())
        }
    }
    pub fn check_chain_depth(&self, depth: usize) -> Result<(), A1Error> {
        if depth > self.max_chain_depth as usize {
            Err(A1Error::MaxDepthExceeded(depth, self.max_chain_depth))
        } else {
            Ok(())
        }
    }
    pub fn requires_human_approval(&self, cap: &str) -> bool {
        self.require_human_approval_for.iter().any(|c| c == cap)
    }
    pub fn commitment(&self) -> Result<[u8; 32], A1Error> {
        let json =
            serde_json::to_string(self).map_err(|e| A1Error::WireFormatError(e.to_string()))?;
        let mut h = Hasher::new_derive_key(DOMAIN_GOV_POLICY);
        h.update(json.as_bytes());
        Ok(h.finalize().into())
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditReport {
    pub title: String,
    pub scope: String,
    pub period_start_unix: u64,
    pub period_end_unix: u64,
    pub total_authorizations: u64,
    pub denied_authorizations: u64,
    pub revocations_issued: u64,
    pub passports_due_rotation: u64,
    pub policy_commitment_hex: String,
    pub generated_at: String,
    pub compliance_standards: Vec<String>,
    pub findings: Vec<AuditFinding>,
    pub report_hash: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditFinding {
    pub severity: FindingSeverity,
    pub code: String,
    pub description: String,
    pub count: u64,
    pub recommendation: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum FindingSeverity {
    Info,
    Warning,
    Critical,
}

impl AuditReport {
    pub fn new(
        scope: impl Into<String>,
        period_start: u64,
        period_end: u64,
        policy: &GovernancePolicy,
    ) -> Result<Self, A1Error> {
        let scope = scope.into();
        let policy_hex = hex::encode(policy.commitment()?);
        Ok(Self {
            title: format!("A1 Compliance Audit — {scope}"),
            scope,
            period_start_unix: period_start,
            period_end_unix: period_end,
            total_authorizations: 0,
            denied_authorizations: 0,
            revocations_issued: 0,
            passports_due_rotation: 0,
            policy_commitment_hex: policy_hex,
            generated_at: unix_to_iso(period_end),
            compliance_standards: vec![
                "EU AI Act (Art. 13, 14, 17)".into(),
                "NIST AI RMF Govern 1.7, 6.2".into(),
                "SOC 2 Type II (CC6.1, CC7.2)".into(),
                "ISO/IEC 27001:2022 A.9".into(),
            ],
            findings: vec![],
            report_hash: String::new(),
        })
    }
    pub fn add_finding(&mut self, f: AuditFinding) {
        self.findings.push(f);
    }
    pub fn finalize(&mut self) -> Result<(), A1Error> {
        let json =
            serde_json::to_string(self).map_err(|e| A1Error::WireFormatError(e.to_string()))?;
        let mut h = Hasher::new_derive_key(DOMAIN_GOV_AUDIT);
        h.update(json.as_bytes());
        self.report_hash = hex::encode(h.finalize().as_bytes());
        Ok(())
    }
}

fn unix_to_iso(unix: u64) -> String {
    let (s, m, h) = (unix % 60, (unix / 60) % 60, (unix / 3600) % 24);
    let mut d = unix / 86400;
    let mut y = 1970u64;
    loop {
        let yl = if (y.is_multiple_of(4) && !y.is_multiple_of(100)) || y.is_multiple_of(400) {
            366
        } else {
            365
        };
        if d < yl {
            break;
        }
        d -= yl;
        y += 1;
    }
    let ml: [u64; 12] = if (y.is_multiple_of(4) && !y.is_multiple_of(100)) || y.is_multiple_of(400)
    {
        [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
    } else {
        [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
    };
    let mut mo = 1u64;
    for mlen in ml {
        if d < mlen {
            break;
        }
        d -= mlen;
        mo += 1;
    }
    format!("{y:04}-{mo:02}-{d:02}T{h:02}:{m:02}:{s:02}Z")
}