zeroclaw/src/verifiable_intent/error.rs
Will Sarg 4a2be7c2e5
feat(verifiable_intent): add native verifiable intent lifecycle module (#2938)
* 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>
2026-03-20 17:52:55 -04:00

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);
}
}