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