dyolo-kya 2.0.0

Know Your Agent (KYA): cryptographic chain-of-custody for recursive AI delegation with provable scope narrowing, namespace isolation, and enterprise-grade storage health
Documentation
use thiserror::Error;

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StorageErrorKind {
    Transient,
    Permanent,
}

#[derive(Debug, Clone)]
pub struct KyaStorageError {
    pub kind: StorageErrorKind,
    pub message: String,
}

impl PartialEq for KyaStorageError {
    fn eq(&self, other: &Self) -> bool {
        self.kind == other.kind
    }
}

impl Eq for KyaStorageError {}

impl KyaStorageError {
    pub fn transient(msg: impl Into<String>) -> Self {
        Self {
            kind: StorageErrorKind::Transient,
            message: msg.into(),
        }
    }

    pub fn permanent(msg: impl Into<String>) -> Self {
        Self {
            kind: StorageErrorKind::Permanent,
            message: msg.into(),
        }
    }

    pub fn is_transient(&self) -> bool {
        self.kind == StorageErrorKind::Transient
    }
}

impl std::fmt::Display for KyaStorageError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let label = match self.kind {
            StorageErrorKind::Transient => "transient",
            StorageErrorKind::Permanent => "permanent",
        };
        write!(f, "{label} storage error: {}", self.message)
    }
}

impl std::error::Error for KyaStorageError {}

#[derive(Debug, Error, PartialEq, Eq)]
#[non_exhaustive]
pub enum KyaError {
    #[error("delegation chain is empty")]
    EmptyChain,

    #[error("storage backend failure: {0}")]
    StorageFailure(KyaStorageError),

    #[error("chain does not anchor to the declared principal")]
    RootMismatch,

    #[error("delegation linkage broken at hop {0}")]
    BrokenLinkage(usize),

    #[error("invalid signature at hop {0}")]
    InvalidSignature(usize),

    #[error("delegation at hop {0} not yet valid (issued_at={1}, now={2})")]
    NotYetValid(usize, u64, u64),

    #[error("delegation at hop {0} has expired (expiry={1}, now={2})")]
    Expired(usize, u64, u64),

    #[error("temporal violation at hop {0}: child expiry {1} exceeds parent expiry {2}")]
    TemporalViolation(usize, u64, u64),

    #[error("depth limit exceeded at hop {0} (limit={1})")]
    MaxDepthExceeded(usize, u8),

    #[error("sub-scope proof is structurally invalid")]
    InvalidSubScopeProof,

    #[error(
        "scope escalation at hop {0}: delegated scope is not within the delegator's authorization"
    )]
    ScopeEscalation(usize),

    #[error("executing agent is not the terminal delegate")]
    UnauthorizedLeaf,

    #[error("execution intent is not within the terminal scope")]
    ScopeViolation,

    #[error("nonce has already been consumed")]
    NonceReplay,

    #[error("delegation certificate has been revoked")]
    Revoked,

    #[error("intent is not present in this tree")]
    IntentNotFound,

    #[error("intent tree requires at least one intent")]
    EmptyTree,

    #[error("wire format error: {0}")]
    WireFormatError(String),

    #[error("unsupported certificate version: expected {expected}, got {got}")]
    UnsupportedVersion { expected: u8, got: u8 },

    #[error("policy violation: {0}")]
    PolicyViolation(String),

    #[error("batch authorization failed at index {index}: {reason}")]
    BatchItemFailed { index: usize, reason: String },

    #[error("MAC verification failed")]
    MacVerificationFailed,

    #[error("namespace mismatch: chain namespace is '{chain}', authorization requested for '{requested}'")]
    NamespaceMismatch { chain: String, requested: String },

    #[error("rate limit exceeded for key")]
    RateLimitExceeded,

    #[error("storage health check failed: {0}")]
    StorageUnhealthy(String),
}

impl KyaError {
    pub fn as_storage_error(&self) -> Option<&KyaStorageError> {
        if let Self::StorageFailure(e) = self {
            Some(e)
        } else {
            None
        }
    }

    pub fn is_transient_storage_failure(&self) -> bool {
        self.as_storage_error().is_some_and(|e| e.is_transient())
    }

    pub fn error_code(&self) -> &'static str {
        match self {
            Self::Expired(..) => "CERT_EXPIRED",
            Self::Revoked => "CERT_REVOKED",
            Self::NonceReplay => "NONCE_REPLAY",
            Self::ScopeViolation => "SCOPE_VIOLATION",
            Self::ScopeEscalation(_) => "SCOPE_ESCALATION",
            Self::InvalidSignature(_) => "INVALID_SIGNATURE",
            Self::BrokenLinkage(_) => "CHAIN_BROKEN_LINKAGE",
            Self::MaxDepthExceeded(..) => "CHAIN_DEPTH_EXCEEDED",
            Self::PolicyViolation(_) => "POLICY_VIOLATION",
            Self::StorageFailure(_) => "STORAGE_ERROR",
            Self::BatchItemFailed { .. } => "BATCH_ITEM_FAILED",
            Self::MacVerificationFailed => "MAC_VERIFICATION_FAILED",
            Self::NamespaceMismatch { .. } => "NAMESPACE_MISMATCH",
            Self::RateLimitExceeded => "RATE_LIMIT_EXCEEDED",
            Self::StorageUnhealthy(_) => "STORAGE_UNHEALTHY",
            _ => "AUTHORIZATION_FAILED",
        }
    }

    pub fn http_status(&self) -> u16 {
        match self {
            Self::StorageFailure(e) if e.is_transient() => 503,
            Self::StorageFailure(_) => 500,
            Self::StorageUnhealthy(_) => 503,
            Self::RateLimitExceeded => 429,
            Self::EmptyChain | Self::WireFormatError(_) | Self::UnsupportedVersion { .. } => 400,
            Self::Revoked | Self::Expired(..) | Self::NotYetValid(..) | Self::NonceReplay => 401,
            Self::ScopeViolation | Self::ScopeEscalation(_) | Self::UnauthorizedLeaf => 403,
            Self::InvalidSignature(_) | Self::RootMismatch | Self::BrokenLinkage(_) => 403,
            Self::PolicyViolation(_) | Self::NamespaceMismatch { .. } => 403,
            Self::MacVerificationFailed => 401,
            _ => 403,
        }
    }
}