Skip to main content

ratify_protocol/
types.rs

1//! Ratify Protocol v1 types.
2//!
3//! Every public key and every signature is a hybrid pair: one Ed25519
4//! component and one ML-DSA-65 (FIPS 204) component. Both must verify.
5
6use serde::ser::{SerializeMap, Serializer};
7use serde::{Deserialize, Serialize};
8
9pub const PROTOCOL_VERSION: i32 = 1;
10pub const MAX_DELEGATION_CHAIN_DEPTH: usize = 3;
11pub const CHALLENGE_WINDOW_SECONDS: i64 = 300;
12
13pub const ED25519_PUBLIC_KEY_SIZE: usize = 32;
14pub const ED25519_SIGNATURE_SIZE: usize = 64;
15pub const MLDSA65_PUBLIC_KEY_SIZE: usize = 1952;
16pub const MLDSA65_SIGNATURE_SIZE: usize = 3309;
17
18/// Ed25519 + ML-DSA-65 public key pair.
19///
20/// Canonical JSON form (keys in lex order):
21/// `{"ed25519":"<base64-32-bytes>","ml_dsa_65":"<base64-1952-bytes>"}`
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23pub struct HybridPublicKey {
24    #[serde(with = "crate::canonical::base64_bytes")]
25    pub ed25519: Vec<u8>, // 32 bytes
26    #[serde(with = "crate::canonical::base64_bytes")]
27    pub ml_dsa_65: Vec<u8>, // 1952 bytes
28}
29
30/// Ed25519 + ML-DSA-65 signature pair over the same canonical bytes.
31///
32/// Both components MUST verify for the signature to be accepted.
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34pub struct HybridSignature {
35    #[serde(with = "crate::canonical::base64_bytes")]
36    pub ed25519: Vec<u8>, // 64 bytes
37    #[serde(with = "crate::canonical::base64_bytes")]
38    pub ml_dsa_65: Vec<u8>, // 3309 bytes
39}
40
41/// Both component private keys. Never serialized to the wire.
42#[derive(Debug, Clone)]
43pub struct HybridPrivateKey {
44    pub ed25519: Vec<u8>,   // 32-byte seed
45    pub ml_dsa_65: Vec<u8>, // ML-DSA-65 secret key bytes
46}
47
48/// Optional external binding for higher-assurance identity.
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct Anchor {
51    #[serde(rename = "type")]
52    pub anchor_type: String,
53    pub provider: String,
54    pub reference: String,
55    pub verified_at: i64,
56}
57
58/// Master identity for a human (or tenant admin).
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct HumanRoot {
61    pub id: String,
62    pub public_key: HybridPublicKey,
63    pub created_at: i64,
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub anchors: Option<Vec<Anchor>>,
66}
67
68/// An AI agent's identity.
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct AgentIdentity {
71    pub id: String,
72    pub public_key: HybridPublicKey,
73    pub name: String,
74    pub agent_type: String,
75    pub created_at: i64,
76}
77
78/// Signed authorization from a principal to an agent.
79///
80/// `scope` answers *what* the agent may do. `constraints` answer *where /
81/// when / how much* — first-class bounds evaluated at verify time against a
82/// caller-supplied VerifierContext.
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct DelegationCert {
85    pub cert_id: String,
86    pub version: i32,
87    pub issuer_id: String,
88    pub issuer_pub_key: HybridPublicKey,
89    pub subject_id: String,
90    pub subject_pub_key: HybridPublicKey,
91    pub scope: Vec<String>,
92    /// Always present in canonical JSON (`[]` when empty) so canonical bytes
93    /// are deterministic across issuers.
94    #[serde(default)]
95    pub constraints: Vec<Constraint>,
96    pub issued_at: i64,
97    pub expires_at: i64,
98    pub signature: HybridSignature,
99}
100
101/// First-class bound on when/where/how much an agent may exercise its scopes.
102///
103/// Wire format is a tagged JSON object. `type` discriminates the kind;
104/// remaining fields are kind-specific. Unknown `type` values MUST be
105/// rejected by conformant verifiers (fail-closed).
106///
107// Fields are declared in alphabetical JSON-key order so serde's default
108// struct serialization order produces canonical bytes that match the Go
109// reference and the other SDKs' lex-sorted output (SPEC §6.2). Do not
110// reorder — cross-SDK byte identicality depends on this.
111//
112// Serialization is custom (see impl Serialize below) to emit the
113// canonical per-kind shape rather than the default "skip if zero"
114// behavior. This closes the v1 zero-as-absence ambiguity: a geo_circle at
115// lat=0, lon=0 now emits lat:0, lon:0 explicitly instead of omitting them.
116#[derive(Debug, Clone, Default, Deserialize)]
117pub struct Constraint {
118    #[serde(default)]
119    pub count: i64,
120    #[serde(default)]
121    pub currency: String,
122    #[serde(default)]
123    pub end: String,
124    #[serde(default)]
125    pub lat: f64,
126    #[serde(default)]
127    pub lon: f64,
128    #[serde(default)]
129    pub max_alt_m: f64,
130    #[serde(default)]
131    pub max_amount: f64,
132    #[serde(default)]
133    pub max_lat: f64,
134    #[serde(default)]
135    pub max_lon: f64,
136    #[serde(default)]
137    pub max_mps: f64,
138    #[serde(default)]
139    pub min_alt_m: f64,
140    #[serde(default)]
141    pub min_lat: f64,
142    #[serde(default)]
143    pub min_lon: f64,
144    #[serde(default)]
145    pub points: Vec<[f64; 2]>,
146    #[serde(default)]
147    pub radius_m: f64,
148    #[serde(default)]
149    pub start: String,
150    #[serde(default)]
151    pub tz: String,
152    #[serde(rename = "type")]
153    pub kind: String,
154    #[serde(default)]
155    pub window_s: i64,
156}
157
158// Custom Serialize for Constraint — emits the canonical per-kind shape.
159// Mirrors Go's Constraint.MarshalJSON and TS canonicalConstraintDict.
160// Keys are emitted in alphabetical order, matching the other SDKs.
161impl Serialize for Constraint {
162    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
163        // Count fields up front so serde's map writer knows the length.
164        // Doing this the verbose way rather than with serialize_struct
165        // because the per-kind shape is dynamic, not a fixed struct.
166        let entries: Vec<(&'static str, FieldValue)> = match self.kind.as_str() {
167            "geo_circle" => vec![
168                ("lat", FieldValue::F64(self.lat)),
169                ("lon", FieldValue::F64(self.lon)),
170                ("radius_m", FieldValue::F64(self.radius_m)),
171                ("type", FieldValue::Str(self.kind.clone())),
172            ],
173            "geo_polygon" => vec![
174                ("points", FieldValue::Points(self.points.clone())),
175                ("type", FieldValue::Str(self.kind.clone())),
176            ],
177            "geo_bbox" => {
178                let mut v = vec![
179                    ("max_lat", FieldValue::F64(self.max_lat)),
180                    ("max_lon", FieldValue::F64(self.max_lon)),
181                    ("min_lat", FieldValue::F64(self.min_lat)),
182                    ("min_lon", FieldValue::F64(self.min_lon)),
183                ];
184                if self.min_alt_m != 0.0 || self.max_alt_m != 0.0 {
185                    // Insert altitude pair alphabetically: max_alt_m < max_lat.
186                    v.insert(0, ("max_alt_m", FieldValue::F64(self.max_alt_m)));
187                    // min_alt_m < min_lat → insert after max_lon (index 2).
188                    v.insert(3, ("min_alt_m", FieldValue::F64(self.min_alt_m)));
189                }
190                v.push(("type", FieldValue::Str(self.kind.clone())));
191                v
192            }
193            "time_window" => vec![
194                ("end", FieldValue::Str(self.end.clone())),
195                ("start", FieldValue::Str(self.start.clone())),
196                ("type", FieldValue::Str(self.kind.clone())),
197                ("tz", FieldValue::Str(self.tz.clone())),
198            ],
199            "max_speed_mps" => vec![
200                ("max_mps", FieldValue::F64(self.max_mps)),
201                ("type", FieldValue::Str(self.kind.clone())),
202            ],
203            "max_amount" => vec![
204                ("currency", FieldValue::Str(self.currency.clone())),
205                ("max_amount", FieldValue::F64(self.max_amount)),
206                ("type", FieldValue::Str(self.kind.clone())),
207            ],
208            "max_rate" => vec![
209                ("count", FieldValue::I64(self.count)),
210                ("type", FieldValue::Str(self.kind.clone())),
211                ("window_s", FieldValue::I64(self.window_s)),
212            ],
213            // Unknown kind: emit only the tag. Verifier returns constraint_unknown.
214            _ => vec![("type", FieldValue::Str(self.kind.clone()))],
215        };
216        let mut m = serializer.serialize_map(Some(entries.len()))?;
217        for (k, v) in entries {
218            match v {
219                FieldValue::F64(x) => m.serialize_entry(k, &x)?,
220                FieldValue::I64(x) => m.serialize_entry(k, &x)?,
221                FieldValue::Str(x) => m.serialize_entry(k, &x)?,
222                FieldValue::Points(x) => m.serialize_entry(k, &x)?,
223            }
224        }
225        m.end()
226    }
227}
228
229// Small sum type so the serialize impl can carry mixed-type values in one
230// vector. Kept private to this module.
231enum FieldValue {
232    F64(f64),
233    I64(i64),
234    Str(String),
235    Points(Vec<[f64; 2]>),
236}
237
238/// Application-supplied inputs for evaluating first-class constraints.
239/// A cert bearing a constraint whose required context field is absent will
240/// be rejected with `constraint_unverifiable` (fail-closed).
241#[derive(Default)]
242pub struct VerifierContext<'a> {
243    pub current_lat: Option<f64>,
244    pub current_lon: Option<f64>,
245    pub current_alt_m: Option<f64>,
246    pub current_speed_mps: Option<f64>,
247    pub requested_amount: Option<f64>,
248    pub requested_currency: Option<String>,
249    /// (cert_id, window_s) -> invocation count
250    pub invocations_in_window: Option<Box<dyn Fn(&str, i64) -> i64 + 'a>>,
251}
252
253/// Proof an agent presents to a verifier.
254///
255/// v1.1 optional stream binding: when `stream_id` and `stream_seq` are set,
256/// the bundle is "stream-bound" — it belongs to an ordered sequence of
257/// interactions sharing a stream_id. Both are signed into the challenge bytes
258/// (SPEC §6.4.2) so replay, reorder, or omission within the stream invalidate
259/// the signature.
260#[derive(Debug, Clone, Serialize, Deserialize)]
261pub struct ProofBundle {
262    pub agent_id: String,
263    pub agent_pub_key: HybridPublicKey,
264    pub delegations: Vec<DelegationCert>,
265    #[serde(with = "crate::canonical::base64_bytes")]
266    pub challenge: Vec<u8>,
267    pub challenge_at: i64,
268    pub challenge_sig: HybridSignature,
269    #[serde(
270        default,
271        skip_serializing_if = "Vec::is_empty",
272        with = "crate::canonical::base64_bytes"
273    )]
274    pub session_context: Vec<u8>,
275    #[serde(
276        default,
277        skip_serializing_if = "Vec::is_empty",
278        with = "crate::canonical::base64_bytes"
279    )]
280    pub stream_id: Vec<u8>,
281    #[serde(default, skip_serializing_if = "is_zero_i64")]
282    pub stream_seq: i64,
283}
284
285fn is_zero_i64(v: &i64) -> bool {
286    *v == 0
287}
288
289/// Identity status values in a VerifyResult (SPEC §5.9). Granular failure
290/// statuses (scope_denied, constraint_denied, etc) let callers route on the
291/// enum directly — they do not have to parse error_reason text.
292#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
293#[serde(rename_all = "snake_case")]
294pub enum IdentityStatus {
295    VerifiedHuman,
296    AuthorizedAgent,
297    Expired,
298    Revoked,
299    ScopeDenied,
300    ConstraintDenied,
301    ConstraintUnverifiable,
302    ConstraintUnknown,
303    DelegationNotAuthorized,
304    Invalid,
305    Unauthorized,
306}
307
308impl IdentityStatus {
309    pub fn as_str(&self) -> &'static str {
310        match self {
311            Self::VerifiedHuman => "verified_human",
312            Self::AuthorizedAgent => "authorized_agent",
313            Self::Expired => "expired",
314            Self::Revoked => "revoked",
315            Self::ScopeDenied => "scope_denied",
316            Self::ConstraintDenied => "constraint_denied",
317            Self::ConstraintUnverifiable => "constraint_unverifiable",
318            Self::ConstraintUnknown => "constraint_unknown",
319            Self::DelegationNotAuthorized => "delegation_not_authorized",
320            Self::Invalid => "invalid",
321            Self::Unauthorized => "unauthorized",
322        }
323    }
324
325    /// Parse the snake_case wire form back into the enum. Returns None if
326    /// the input is not a known status; callers should fail-closed.
327    pub fn from_wire(s: &str) -> Option<Self> {
328        Some(match s {
329            "verified_human" => Self::VerifiedHuman,
330            "authorized_agent" => Self::AuthorizedAgent,
331            "expired" => Self::Expired,
332            "revoked" => Self::Revoked,
333            "scope_denied" => Self::ScopeDenied,
334            "constraint_denied" => Self::ConstraintDenied,
335            "constraint_unverifiable" => Self::ConstraintUnverifiable,
336            "constraint_unknown" => Self::ConstraintUnknown,
337            "delegation_not_authorized" => Self::DelegationNotAuthorized,
338            "invalid" => Self::Invalid,
339            "unauthorized" => Self::Unauthorized,
340            _ => return None,
341        })
342    }
343}
344
345/// Deterministic output of `verify_bundle`. Always check `valid` first.
346#[derive(Debug, Clone, Serialize, Deserialize)]
347pub struct VerifyResult {
348    pub valid: bool,
349    pub identity_status: IdentityStatus,
350    #[serde(skip_serializing_if = "String::is_empty", default)]
351    pub human_id: String,
352    #[serde(skip_serializing_if = "String::is_empty", default)]
353    pub agent_id: String,
354    #[serde(skip_serializing_if = "String::is_empty", default)]
355    pub agent_name: String,
356    #[serde(skip_serializing_if = "String::is_empty", default)]
357    pub agent_type: String,
358    #[serde(skip_serializing_if = "Vec::is_empty", default)]
359    pub granted_scope: Vec<String>,
360    #[serde(skip_serializing_if = "String::is_empty", default)]
361    pub error_reason: String,
362    /// Resolved external-identity binding for `human_id`, populated when
363    /// `VerifyOptions.anchor_resolver` is set on a successful verification.
364    /// Lets downstream `AuditProvider`s record an unforgeable chain from
365    /// verification event → identity attestation. (SPEC §17.8)
366    #[serde(skip_serializing_if = "Option::is_none", default)]
367    pub anchor: Option<Anchor>,
368}
369
370/// Signed list of revoked cert IDs, served by the issuer.
371#[derive(Debug, Clone, Serialize, Deserialize)]
372pub struct RevocationList {
373    pub issuer_id: String,
374    pub updated_at: i64,
375    pub revoked_certs: Vec<String>,
376    pub signature: HybridSignature,
377}
378
379/// v1.1 signed push notification of newly revoked cert IDs.
380#[derive(Debug, Clone, Serialize, Deserialize)]
381pub struct RevocationPush {
382    pub issuer_id: String,
383    pub seq_no: i64,
384    pub entries: Vec<String>,
385    pub pushed_at: i64,
386    pub signature: HybridSignature,
387}
388
389/// v1.1 element in a hash-chain append-only witness log.
390#[derive(Debug, Clone, Serialize, Deserialize)]
391pub struct WitnessEntry {
392    #[serde(with = "crate::canonical::base64_bytes")]
393    pub prev_hash: Vec<u8>,
394    #[serde(with = "crate::canonical::base64_bytes")]
395    pub entry_data: Vec<u8>,
396    pub timestamp: i64,
397    pub witness_id: String,
398    pub signature: HybridSignature,
399}
400
401/// v1.1 verifier-issued credential that caches a verified chain. MAC =
402/// HMAC-SHA256(session_secret, session_token_sign_bytes(token)). The session
403/// secret is private to the verifier and never leaves its trust boundary.
404#[derive(Debug, Clone, Serialize, Deserialize)]
405pub struct SessionToken {
406    pub version: i32,
407    pub session_id: String,
408    pub agent_id: String,
409    pub agent_pub_key: HybridPublicKey,
410    pub human_id: String,
411    pub granted_scope: Vec<String>,
412    pub issued_at: i64,
413    pub valid_until: i64,
414    #[serde(with = "crate::canonical::base64_bytes")]
415    pub chain_hash: Vec<u8>,
416    #[serde(with = "crate::canonical::base64_bytes")]
417    pub mac: Vec<u8>,
418}
419
420/// v1.1 canonical envelope for a multi-party, atomic transaction.
421#[derive(Debug, Clone, Serialize, Deserialize)]
422pub struct TransactionReceipt {
423    pub version: i32,
424    pub transaction_id: String,
425    pub created_at: i64,
426    pub terms_schema_uri: String,
427    #[serde(with = "crate::canonical::base64_bytes")]
428    pub terms_canonical_json: Vec<u8>,
429    pub parties: Vec<ReceiptParty>,
430    pub party_signatures: Vec<ReceiptPartySignature>,
431}
432
433/// One party to a TransactionReceipt.
434#[derive(Debug, Clone, Serialize, Deserialize)]
435pub struct ReceiptParty {
436    pub party_id: String,
437    pub role: String,
438    pub agent_id: String,
439    pub agent_pub_key: HybridPublicKey,
440    pub proof_bundle: ProofBundle,
441}
442
443/// Hybrid signature by a party over the canonical receipt signable.
444#[derive(Debug, Clone, Serialize, Deserialize)]
445pub struct ReceiptPartySignature {
446    pub party_id: String,
447    pub signature: HybridSignature,
448}
449
450/// Outcome of verify_transaction_receipt.
451pub struct TransactionReceiptResult {
452    pub valid: bool,
453    pub error_reason: String,
454    pub party_results: Vec<VerifyResult>,
455}
456
457/// Signed continuity statement from an old root key to a new root key.
458#[derive(Debug, Clone, Serialize, Deserialize)]
459pub struct KeyRotationStatement {
460    pub version: i32,
461    pub old_id: String,
462    pub old_pub_key: HybridPublicKey,
463    pub new_id: String,
464    pub new_pub_key: HybridPublicKey,
465    pub rotated_at: i64,
466    pub reason: String,
467    pub signature_old: HybridSignature,
468    pub signature_new: HybridSignature,
469}
470
471/// Verifier state tracked per stream_id for v1.1 stream-bound bundles.
472///
473/// `last_seen_seq` is the highest sequence number the verifier has already
474/// accepted for `stream_id`; zero means no turns accepted yet, so the first
475/// valid bundle must carry `stream_seq == 1`.
476#[derive(Debug, Clone, Default)]
477pub struct StreamContext {
478    pub stream_id: Vec<u8>,
479    pub last_seen_seq: i64,
480}
481
482/// Pluggable provider for revocation state (SPEC §17.1).
483///
484/// Implementations return `Ok(true)` for revoked, `Ok(false)` for live, and
485/// `Err(...)` to surface a lookup failure. A provider error is fail-closed:
486/// the bundle is rejected with `error_reason="revocation_error: ..."` —
487/// SDKs MUST NOT treat a lookup failure as "not revoked." On the verifier's
488/// hot path; implementations should be O(1) at call time.
489pub trait RevocationProvider {
490    fn is_revoked(&self, cert_id: &str) -> Result<bool, String>;
491}
492
493/// Pluggable evaluator for verifier-local policy (SPEC §17.2).
494///
495/// Evaluated AFTER all cryptographic, temporal, revocation, constraint, and
496/// scope-intersection checks pass. `Ok(true)` allows; `Ok(false)` denies with
497/// `scope_denied`; `Err(...)` fails closed with `policy_error`.
498pub trait PolicyProvider {
499    fn evaluate_policy(
500        &self,
501        bundle: &ProofBundle,
502        context: &VerifierContext,
503    ) -> Result<bool, String>;
504}
505
506/// Pluggable audit-receipt persistence (SPEC §17.3).
507///
508/// Invoked on every `verify_bundle` call (success AND failure). Errors are
509/// swallowed — auditing MUST NOT alter the verdict.
510pub trait AuditProvider {
511    fn log_verification(&self, result: &VerifyResult, bundle: &ProofBundle);
512}
513
514/// Pluggable evaluator for extension constraint types (SPEC §17.7).
515///
516/// Built-in types (geo_*, time_window, max_*) are evaluated by the SDK
517/// directly; an evaluator is consulted only for types the SDK does not
518/// natively understand. Returning `Ok(true)` allows; `Ok(false)` denies as
519/// `constraint_denied`; `Err("constraint_unverifiable: ...")` routes to
520/// `constraint_unverifiable`; other `Err(...)` denies with the wrapped
521/// reason.
522pub trait ConstraintEvaluator {
523    fn evaluate(
524        &self,
525        constraint: &Constraint,
526        cert_id: &str,
527        context: &VerifierContext,
528        now: i64,
529    ) -> Result<(), String>;
530}
531
532/// Resolves a verified `human_id` to its external-identity binding
533/// (SPEC §17.8). Errors are non-fatal: the verifier MUST NOT fail the bundle
534/// because the resolver errored — it silently leaves `VerifyResult.anchor`
535/// `None` and continues.
536pub trait AnchorResolver {
537    fn resolve_anchor(&self, human_id: &str) -> Result<Option<Anchor>, String>;
538}
539
540/// HMAC-bound cached policy decision (SPEC §17.6). The policy equivalent
541/// of `SessionToken`: issued once by a commercial policy backend, accepted
542/// locally by the verifier for the rest of `valid_until` without re-calling
543/// the backend.
544#[derive(Debug, Clone, Serialize, Deserialize)]
545pub struct PolicyVerdict {
546    pub version: i32,
547    pub verdict_id: String,
548    pub agent_id: String,
549    pub scope: String,
550    pub allow: bool,
551    #[serde(with = "crate::canonical::base64_bytes")]
552    pub context_hash: Vec<u8>, // 32 bytes
553    pub issued_at: i64,
554    pub valid_until: i64,
555    #[serde(with = "crate::canonical::base64_bytes")]
556    pub mac: Vec<u8>, // 32 bytes — HMAC-SHA256
557}
558
559/// Verifier-signed attestation that a specific ProofBundle was verified at
560/// a specific moment with a specific outcome (SPEC §17.5).
561///
562/// Receipts chain by `prev_hash` (SHA-256 of previous receipt's canonical
563/// signable bytes) so a missing or backdated entry is detectable. Genesis
564/// uses 32 zero bytes.
565#[derive(Debug, Clone, Serialize, Deserialize)]
566pub struct VerificationReceipt {
567    pub version: i32,
568    pub verifier_id: String,
569    pub verifier_pub: HybridPublicKey,
570    #[serde(with = "crate::canonical::base64_bytes")]
571    pub bundle_hash: Vec<u8>, // 32 bytes
572    pub decision: String,
573    #[serde(skip_serializing_if = "String::is_empty", default)]
574    pub human_id: String,
575    #[serde(skip_serializing_if = "String::is_empty", default)]
576    pub agent_id: String,
577    #[serde(skip_serializing_if = "Vec::is_empty", default)]
578    pub granted_scope: Vec<String>,
579    #[serde(skip_serializing_if = "String::is_empty", default)]
580    pub error_reason: String,
581    pub verified_at: i64,
582    #[serde(with = "crate::canonical::base64_bytes")]
583    pub prev_hash: Vec<u8>, // 32 bytes; zeros for genesis
584    pub signature: HybridSignature,
585}
586
587/// Options passed to `verify_bundle`.
588pub struct VerifyOptions<'a> {
589    /// Required scope; empty string skips scope checking.
590    pub required_scope: String,
591    /// Legacy v1 revocation closure.
592    ///
593    /// **Deprecated:** Use `revocation` (SPEC §17.1) instead. The closure
594    /// has no way to surface lookup failures; `revocation` returns
595    /// `Result<bool, String>` and fails closed on error. Slated for removal
596    /// in v1.0.0-beta.1. When both fields are set, `revocation` wins.
597    #[deprecated(since = "1.0.0-alpha.7", note = "use `revocation` (SPEC §17.1) instead")]
598    pub is_revoked: Option<Box<dyn Fn(&str) -> bool + 'a>>,
599    /// Pluggable revocation provider (SPEC §17.1). Takes precedence over
600    /// `is_revoked`. A provider error fails the bundle as `revocation_error`.
601    pub revocation: Option<Box<dyn RevocationProvider + 'a>>,
602    /// Force a fresh revocation check for high-stakes endpoints. The SDK
603    /// cannot fetch revocation state itself; callers must provide is_revoked
604    /// or a revocation provider when this is true.
605    pub force_revocation_check: bool,
606    /// Override current time (unix seconds); None = SystemTime::now().
607    pub now: Option<i64>,
608    /// Optional verifier-reconstructed 32-byte v1.1 session context.
609    pub session_context: Vec<u8>,
610    /// Optional verifier-tracked v1.1 stream context.
611    pub stream: Option<StreamContext>,
612    /// Application inputs for evaluating first-class constraints. Default is
613    /// empty; constraint-bearing certs fail closed if required context is
614    /// absent.
615    pub context: VerifierContext<'a>,
616    /// Advanced verifier-local policy evaluator (SPEC §17.2). Evaluated after
617    /// all cryptographic checks pass. Deny → `scope_denied`; provider error →
618    /// `policy_error`.
619    pub policy: Option<Box<dyn PolicyProvider + 'a>>,
620    /// Audit-receipt persistence hook (SPEC §17.3). Invoked on every Verify
621    /// (success AND failure). Provider errors are swallowed — auditing cannot
622    /// alter the verdict.
623    pub audit: Option<Box<dyn AuditProvider + 'a>>,
624    /// Per-Verify registry of extension constraint evaluators (SPEC §17.7).
625    /// Built-in types are evaluated by the SDK directly; the registry is
626    /// only consulted for unknown types.
627    pub constraint_evaluators:
628        Option<std::collections::HashMap<String, Box<dyn ConstraintEvaluator + 'a>>>,
629    /// Fast-path cached policy decision (SPEC §17.6). When present and
630    /// valid (MAC matches `policy_secret`, within window, agent/scope/
631    /// context_hash matches), the verifier skips the live `policy` hook.
632    /// Stale verdicts fall back to live policy.
633    pub policy_verdict: Option<PolicyVerdict>,
634    /// HMAC secret used to verify `policy_verdict.mac`.
635    pub policy_secret: Option<Vec<u8>>,
636    /// Anchor resolver (SPEC §17.8). When set on a Valid=true verification,
637    /// the verifier populates `VerifyResult.anchor`. Resolver errors are
638    /// non-fatal.
639    pub anchor_resolver: Option<Box<dyn AnchorResolver + 'a>>,
640}
641
642impl<'a> Default for VerifyOptions<'a> {
643    fn default() -> Self {
644        // The Default impl must initialize the deprecated field for backwards
645        // compatibility. Suppressing the warning is intentional and isolated
646        // to this single construction site.
647        #[allow(deprecated)]
648        Self {
649            required_scope: String::new(),
650            is_revoked: None,
651            revocation: None,
652            force_revocation_check: false,
653            now: None,
654            session_context: Vec::new(),
655            stream: None,
656            context: VerifierContext::default(),
657            policy: None,
658            audit: None,
659            constraint_evaluators: None,
660            policy_verdict: None,
661            policy_secret: None,
662            anchor_resolver: None,
663        }
664    }
665}