* feat(verifiable_intent): add native verifiable intent lifecycle module Implements a Rust-native Verifiable Intent (VI) subsystem for ZeroClaw, providing full credential lifecycle support for commerce agent authorization using SD-JWT layered credentials. New module: src/verifiable_intent/ - error.rs: ViError/ViErrorKind (25+ variants), implements std::error::Error - types.rs: JWK, Cnf, Entity, Constraint (8 variants), Immediate/Autonomous mandate structs, Fulfillment, Layer1/Layer2/CredentialChain - crypto.rs: base64url helpers, SD hash, JWS sign/verify, EC P-256 key generation/loading, disclosure creation, SD-JWT serialize/parse - verification.rs: StrictnessMode, ChainVerificationResult, ConstraintCheckResult, verify_timestamps, verify_sd_hash_binding, verify_l3_cross_reference, verify_checkout_hash_binding, check_constraints - issuance.rs: create_layer2_immediate, create_layer2_autonomous, create_layer3_payment, create_layer3_checkout New tool: src/tools/verifiable_intent.rs - VerifiableIntentTool implementing Tool trait (name: vi_verify) - Operations: verify_binding, evaluate_constraints, verify_timestamps - Gated behind verifiable_intent.enabled config flag Wiring: - src/lib.rs: pub mod verifiable_intent - src/main.rs: mod verifiable_intent (binary re-declaration) - src/config/schema.rs: VerifiableIntentConfig struct, field on Config - src/config/mod.rs: re-exports VerifiableIntentConfig - src/onboard/wizard.rs: default field in Config literals - src/tools/mod.rs: conditional tool registration Uses only existing deps: ring (ECDSA P-256), sha2, base64, serde_json, chrono, anyhow. No new dependencies added. Validation: cargo fmt clean, cargo clippy -D warnings clean, cargo test --lib -- verifiable_intent passes (44 tests) * chore(verifiable_intent): add Apache 2.0 attribution for VI spec reference The src/verifiable_intent/ module is a Rust-native reimplementation based on the Verifiable Intent open specification and reference implementation by genereda (https://github.com/genereda/verifiable-intent), Apache 2.0. - Add attribution section to src/verifiable_intent/mod.rs doc comment - Add third-party attribution entry to NOTICE per Apache 2.0 section 4(d) * fix(verifiable_intent): correct VI attribution URL and author Replace hallucinated github.com/genereda/verifiable-intent with the actual remote: github.com/agent-intent/verifiable-intent * fix(verifiable_intent): remove unused pub re-exports to fix clippy Remove unused re-exports of ViError, ViErrorKind, types::*, ChainVerificationResult, and ConstraintCheckResult from the module root. Only StrictnessMode is used externally. --------- Co-authored-by: argenis de la rosa <theonlyhennygod@gmail.com>
114 lines
4.4 KiB
Rust
114 lines
4.4 KiB
Rust
//! Machine-readable error taxonomy for Verifiable Intent operations.
|
|
//!
|
|
//! Every VI error carries a [`ViErrorKind`] discriminant so policy engines and
|
|
//! tool gates can branch deterministically on failure reason without parsing
|
|
//! human-readable messages.
|
|
|
|
use std::fmt;
|
|
|
|
/// Discriminant for VI error classification — used by policy engines to decide
|
|
/// whether a transaction should be blocked, retried, or escalated.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
pub enum ViErrorKind {
|
|
// ── Credential structure errors ───────────────────────────────────
|
|
/// JWT header is malformed or missing required fields.
|
|
InvalidHeader,
|
|
/// JWT payload cannot be decoded or is missing required claims.
|
|
InvalidPayload,
|
|
/// SD-JWT disclosure is malformed or cannot be resolved.
|
|
InvalidDisclosure,
|
|
/// Credential has expired (`exp` < now).
|
|
Expired,
|
|
/// Credential is not yet valid (`iat` > now).
|
|
NotYetValid,
|
|
|
|
// ── Signature / key errors ────────────────────────────────────────
|
|
/// Cryptographic signature verification failed.
|
|
SignatureInvalid,
|
|
/// The signing key does not match the expected `cnf.jwk` binding.
|
|
KeyMismatch,
|
|
/// Key material is missing or in an unsupported format.
|
|
KeyUnsupported,
|
|
|
|
// ── Chain binding errors ──────────────────────────────────────────
|
|
/// `sd_hash` in L2/L3 does not match the hash of the parent layer.
|
|
SdHashMismatch,
|
|
/// `checkout_hash` / `transaction_id` cross-reference between L3a and L3b failed.
|
|
CrossReferenceMismatch,
|
|
/// `conditional_transaction_id` binding between payment and checkout mandates failed.
|
|
ReferenceBindingMismatch,
|
|
|
|
// ── Constraint violations ─────────────────────────────────────────
|
|
/// Transaction amount is outside the permitted range.
|
|
AmountOutOfRange,
|
|
/// Cumulative budget cap exceeded.
|
|
BudgetExceeded,
|
|
/// Currency in L3 does not match the constraint currency.
|
|
CurrencyMismatch,
|
|
/// Merchant is not in the allowed merchant list.
|
|
MerchantNotAllowed,
|
|
/// Payee is not in the allowed payee list.
|
|
PayeeNotAllowed,
|
|
/// Line items violate product selection or quantity constraints.
|
|
LineItemViolation,
|
|
/// Recurrence constraint violated.
|
|
RecurrenceViolation,
|
|
/// An unknown constraint type was encountered in strict mode.
|
|
UnknownConstraintType,
|
|
|
|
// ── Mode / structural mismatch ────────────────────────────────────
|
|
/// L2 contains `cnf` in Immediate mode (forbidden) or lacks it in Autonomous mode.
|
|
ModeMismatch,
|
|
/// Mandate VCT value is not recognized.
|
|
UnknownMandateType,
|
|
/// Mandate pair is incomplete (missing checkout or payment mandate).
|
|
IncompleteMandatePair,
|
|
|
|
// ── Issuance errors ───────────────────────────────────────────────
|
|
/// Issuance failed due to missing or invalid input parameters.
|
|
IssuanceInputInvalid,
|
|
}
|
|
|
|
/// A Verifiable Intent error with a machine-readable kind and human-readable context.
|
|
#[derive(Debug, Clone)]
|
|
pub struct ViError {
|
|
pub kind: ViErrorKind,
|
|
pub message: String,
|
|
}
|
|
|
|
impl ViError {
|
|
pub fn new(kind: ViErrorKind, message: impl Into<String>) -> Self {
|
|
Self {
|
|
kind,
|
|
message: message.into(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for ViError {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(f, "VI/{:?}: {}", self.kind, self.message)
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for ViError {}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn error_display_includes_kind_and_message() {
|
|
let err = ViError::new(ViErrorKind::AmountOutOfRange, "50000 > 40000 USD");
|
|
let s = format!("{err}");
|
|
assert!(s.contains("AmountOutOfRange"));
|
|
assert!(s.contains("50000 > 40000 USD"));
|
|
}
|
|
|
|
#[test]
|
|
fn error_kind_equality() {
|
|
assert_eq!(ViErrorKind::Expired, ViErrorKind::Expired);
|
|
assert_ne!(ViErrorKind::Expired, ViErrorKind::SignatureInvalid);
|
|
}
|
|
}
|