Skip to main content

ratify_protocol/
verify.rs

1//! Verify — the core verifier. Mirrors the Go reference verify.go exactly.
2
3#[cfg(not(feature = "std"))]
4use alloc::{format, string::String, string::ToString, vec::Vec};
5
6use alloc::collections::BTreeMap;
7
8use crate::constraints::evaluate_constraints;
9
10use crate::crypto::{
11    transaction_receipt_sign_bytes, verify_both, verify_challenge_signature_with_stream,
12    verify_delegation_signature_e, verify_session_token_e,
13};
14use crate::scope::{intersect_scopes, SCOPE_IDENTITY_DELEGATE};
15use crate::types::{
16    HybridPublicKey, HybridSignature, IdentityStatus, ProofBundle, SessionToken,
17    TransactionReceipt, TransactionReceiptResult, VerifyOptions, VerifyResult,
18    CHALLENGE_WINDOW_SECONDS, ED25519_PUBLIC_KEY_SIZE, MAX_DELEGATION_CHAIN_DEPTH,
19    MLDSA65_PUBLIC_KEY_SIZE, PROTOCOL_VERSION,
20};
21
22/// `verify_bundle` is the entry point. Audit hook (SPEC §17.3) wraps the
23/// inner verifier so it fires on every call — success AND failure — and its
24/// errors are swallowed so auditing never alters the verdict.
25pub fn verify_bundle(bundle: &ProofBundle, opts: &VerifyOptions) -> VerifyResult {
26    let res = verify_bundle_inner(bundle, opts);
27    if let Some(audit) = &opts.audit {
28        audit.log_verification(&res, bundle);
29    }
30    res
31}
32
33fn verify_bundle_inner(bundle: &ProofBundle, opts: &VerifyOptions) -> VerifyResult {
34    let now = opts.now.unwrap_or_else(|| {
35        #[cfg(feature = "std")]
36        {
37            use std::time::{SystemTime, UNIX_EPOCH};
38            SystemTime::now()
39                .duration_since(UNIX_EPOCH)
40                .unwrap_or_default()
41                .as_secs() as i64
42        }
43        #[cfg(not(feature = "std"))]
44        {
45            // Without std, callers MUST supply opts.now.
46            panic!("verify_bundle: opts.now must be set when std feature is disabled")
47        }
48    });
49
50    // --- Structure ---
51    if bundle.delegations.is_empty() {
52        return invalid(
53            "no_delegations",
54            "proof bundle contains no delegation certificates",
55        );
56    }
57    if bundle.delegations.len() > MAX_DELEGATION_CHAIN_DEPTH {
58        return invalid("chain_too_deep", "delegation chain exceeds maximum depth");
59    }
60    if bundle.challenge.is_empty() {
61        return invalid("no_challenge", "proof bundle contains no challenge");
62    }
63    if !bundle.session_context.is_empty() && bundle.session_context.len() != 32 {
64        return invalid(
65            "invalid_session_context",
66            &format!(
67                "session_context must be 32 bytes, got {}",
68                bundle.session_context.len()
69            ),
70        );
71    }
72    if !opts.session_context.is_empty() && opts.session_context.len() != 32 {
73        return invalid(
74            "invalid_session_context",
75            &format!(
76                "verify option session_context must be 32 bytes, got {}",
77                opts.session_context.len()
78            ),
79        );
80    }
81    if !opts.session_context.is_empty() {
82        if bundle.session_context.is_empty() {
83            return invalid(
84                "missing_session_context",
85                "verifier requires a session-bound challenge but bundle has no session_context",
86            );
87        }
88        if bundle.session_context != opts.session_context {
89            return invalid(
90                "session_context_mismatch",
91                "bundle session_context does not match verifier context",
92            );
93        }
94    } else if !bundle.session_context.is_empty() {
95        return invalid(
96            "session_context_unverifiable",
97            "bundle has session_context but verifier did not provide one",
98        );
99    }
100
101    // --- v1.1 stream binding checks (SPEC §5.8, §6.4.2) ---
102    if !bundle.stream_id.is_empty() && bundle.stream_id.len() != 32 {
103        return invalid(
104            "invalid_stream_id",
105            &format!("stream_id must be 32 bytes, got {}", bundle.stream_id.len()),
106        );
107    }
108    if bundle.stream_id.is_empty() && bundle.stream_seq != 0 {
109        return invalid("invalid_stream_seq", "stream_seq set without stream_id");
110    }
111    if !bundle.stream_id.is_empty() && bundle.stream_seq < 1 {
112        return invalid(
113            "invalid_stream_seq",
114            &format!("stream_seq must be >=1, got {}", bundle.stream_seq),
115        );
116    }
117    if let Some(stream) = &opts.stream {
118        if stream.stream_id.len() != 32 {
119            return invalid(
120                "invalid_stream_id",
121                &format!(
122                    "verify option stream_id must be 32 bytes, got {}",
123                    stream.stream_id.len()
124                ),
125            );
126        }
127        if bundle.stream_id.is_empty() {
128            return invalid(
129                "missing_stream_context",
130                "verifier requires a stream-bound challenge but bundle has no stream_id",
131            );
132        }
133        if bundle.stream_id != stream.stream_id {
134            return invalid(
135                "stream_id_mismatch",
136                "bundle stream_id does not match verifier stream context",
137            );
138        }
139        let expected = stream.last_seen_seq + 1;
140        if bundle.stream_seq <= stream.last_seen_seq {
141            return invalid(
142                "stream_seq_replay",
143                &format!(
144                    "stream_seq {} already seen (last={})",
145                    bundle.stream_seq, stream.last_seen_seq
146                ),
147            );
148        }
149        if bundle.stream_seq != expected {
150            return invalid(
151                "stream_seq_skip",
152                &format!(
153                    "stream_seq {} skips expected {}",
154                    bundle.stream_seq, expected
155                ),
156            );
157        }
158    } else if !bundle.stream_id.is_empty() {
159        return invalid(
160            "stream_context_unverifiable",
161            "bundle has stream_id but verifier did not provide a stream context",
162        );
163    }
164
165    if let Some(err) = validate_hybrid_pubkey_lens(&bundle.agent_pub_key, "agent") {
166        return invalid("invalid_agent_key", &err);
167    }
168
169    let first_cert = &bundle.delegations[0];
170    let human_id = bundle.delegations.last().unwrap().issuer_id.clone();
171
172    if !hybrid_pub_key_equal(&bundle.agent_pub_key, &first_cert.subject_pub_key) {
173        return invalid(
174            "key_mismatch",
175            "agent public key does not match delegation subject",
176        );
177    }
178    if bundle.agent_id != first_cert.subject_id {
179        return invalid(
180            "id_mismatch",
181            "agent ID does not match delegation subject ID",
182        );
183    }
184
185    #[allow(deprecated)]
186    let legacy_revoke = opts.is_revoked.as_ref();
187    if opts.force_revocation_check && legacy_revoke.is_none() && opts.revocation.is_none() {
188        return invalid(
189            "force_revocation_no_callback",
190            "force_revocation_check is true but neither is_revoked nor revocation provider is set",
191        );
192    }
193
194    // --- Per-cert ---
195    for (i, cert) in bundle.delegations.iter().enumerate() {
196        if cert.version != PROTOCOL_VERSION {
197            return invalid(
198                "version_mismatch",
199                &format!("cert {} has unsupported version {}", i, cert.version),
200            );
201        }
202        if now > cert.expires_at {
203            return expired(&human_id, &bundle.agent_id);
204        }
205        if now < cert.issued_at {
206            return invalid("not_yet_valid", &format!("cert {} is not yet valid", i));
207        }
208        // Revocation: provider (SPEC §17.1) takes precedence over legacy closure.
209        if let Some(provider) = &opts.revocation {
210            match provider.is_revoked(&cert.cert_id) {
211                Err(e) => {
212                    return invalid(
213                        "revocation_error",
214                        &format!("cert {}: revocation lookup failed: {}", i, e),
215                    )
216                }
217                Ok(true) => return revoked(&human_id, &bundle.agent_id),
218                Ok(false) => {}
219            }
220        } else if let Some(check) = legacy_revoke {
221            if check(&cert.cert_id) {
222                return revoked(&human_id, &bundle.agent_id);
223            }
224        }
225        if let Err(sig_err) = verify_delegation_signature_e(cert) {
226            return invalid("bad_signature", &format!("cert {}: {}", i, sig_err));
227        }
228        // Constraint evaluation — each cert's first-class constraints must all
229        // pass against the caller-supplied VerifierContext. Fail-closed.
230        if let Err(constraint_err) = evaluate_constraints(
231            cert,
232            &opts.context,
233            now,
234            opts.constraint_evaluators.as_ref(),
235        ) {
236            // Route constraint failures to the specific identity_status so
237            // audit layers can distinguish unverifiable / unknown / denied.
238            // Matches Go/TS/Python.
239            let status = if constraint_err.contains("constraint_unverifiable") {
240                "constraint_unverifiable"
241            } else if constraint_err.contains("constraint_unknown") {
242                "constraint_unknown"
243            } else {
244                "constraint_denied"
245            };
246            return fail_with_status(status, &format!("cert {}: {}", i, constraint_err));
247        }
248        // Chain linkage
249        if i + 1 < bundle.delegations.len() {
250            let next = &bundle.delegations[i + 1];
251            if cert.issuer_id != next.subject_id {
252                return invalid(
253                    "broken_chain",
254                    &format!("cert {} issuer does not match cert {} subject", i, i + 1),
255                );
256            }
257            if !hybrid_pub_key_equal(&cert.issuer_pub_key, &next.subject_pub_key) {
258                return invalid(
259                    "broken_chain_keys",
260                    &format!(
261                        "cert {} issuer key does not match cert {} subject key",
262                        i,
263                        i + 1
264                    ),
265                );
266            }
267            // Sub-delegation gate: parent cert must have granted identity:delegate.
268            if !next.scope.iter().any(|s| s == SCOPE_IDENTITY_DELEGATE) {
269                return fail_with_status(
270                    "delegation_not_authorized",
271                    &format!(
272                        "cert {} issued by a subject whose parent cert {} did not grant \"{}\"",
273                        i,
274                        i + 1,
275                        SCOPE_IDENTITY_DELEGATE
276                    ),
277                );
278            }
279        }
280    }
281
282    // --- Liveness ---
283    let challenge_age = now - bundle.challenge_at;
284    if challenge_age < 0 || challenge_age > CHALLENGE_WINDOW_SECONDS {
285        return invalid(
286            "stale_challenge",
287            &format!(
288                "challenge is {} seconds old (max {})",
289                challenge_age, CHALLENGE_WINDOW_SECONDS
290            ),
291        );
292    }
293    if let Err(err) = verify_challenge_signature_with_stream(
294        &bundle.challenge,
295        bundle.challenge_at,
296        &bundle.session_context,
297        &bundle.stream_id,
298        bundle.stream_seq,
299        &bundle.challenge_sig,
300        &bundle.agent_pub_key,
301    ) {
302        return invalid(
303            "bad_challenge_sig",
304            &format!("challenge signature verification failed: {}", err),
305        );
306    }
307
308    // --- Effective scope ---
309    let scope_refs: Vec<&[String]> = bundle
310        .delegations
311        .iter()
312        .map(|c| c.scope.as_slice())
313        .collect();
314    let effective = intersect_scopes(&scope_refs);
315
316    if !opts.required_scope.is_empty() && !effective.iter().any(|s| s == &opts.required_scope) {
317        return fail_with_status(
318            "scope_denied",
319            &format!(
320                "required scope \"{}\" not in effective delegation scope",
321                opts.required_scope
322            ),
323        );
324    }
325
326    let mut result = VerifyResult {
327        valid: true,
328        identity_status: IdentityStatus::AuthorizedAgent,
329        human_id: human_id.clone(),
330        agent_id: bundle.agent_id.clone(),
331        agent_name: String::new(),
332        agent_type: String::new(),
333        granted_scope: effective,
334        error_reason: String::new(),
335        anchor: None,
336    };
337
338    // --- Anchor resolution (SPEC §17.8) ---
339    // Best-effort: populate anchor on the success result so downstream
340    // AuditProviders observe an identity-bound receipt. Resolver errors are
341    // non-fatal.
342    if let Some(resolver) = &opts.anchor_resolver {
343        if let Ok(Some(anchor)) = resolver.resolve_anchor(&human_id) {
344            result.anchor = Some(anchor);
345        }
346    }
347
348    // --- Advanced Policy Gating (SPEC §17.2 / §17.6) ---
349    //
350    // Fast path: if a PolicyVerdict is supplied AND verifies cleanly, skip
351    // the live Policy provider entirely.
352    if let (Some(verdict), Some(secret)) = (&opts.policy_verdict, &opts.policy_secret) {
353        if !opts.required_scope.is_empty() {
354            match crate::receipts::verifier_context_hash(&opts.context) {
355                Err(e) => {
356                    return invalid(
357                        "policy_error",
358                        &format!("verifier context hash failed: {}", e),
359                    );
360                }
361                Ok(ctx_hash) => {
362                    let v_err = crate::receipts::verify_policy_verdict(
363                        verdict,
364                        secret,
365                        &bundle.agent_id,
366                        &opts.required_scope,
367                        &ctx_hash,
368                        now,
369                    );
370                    match v_err {
371                        Ok(()) => return result,
372                        Err(e) if e.starts_with("policy_verdict_denied") => {
373                            return fail_with_status(
374                                "scope_denied",
375                                "policy verdict (cached) denied access",
376                            );
377                        }
378                        Err(_) => {
379                            // stale verdict — fall through to live policy
380                        }
381                    }
382                }
383            }
384        }
385    }
386
387    if let Some(policy) = &opts.policy {
388        match policy.evaluate_policy(bundle, &opts.context) {
389            Err(e) => {
390                return invalid(
391                    "policy_error",
392                    &format!("advanced policy evaluation failed: {}", e),
393                )
394            }
395            Ok(false) => {
396                return fail_with_status(
397                    "scope_denied",
398                    "advanced policy evaluation denied access",
399                )
400            }
401            Ok(true) => {}
402        }
403    }
404
405    result
406}
407
408// ----------------------------------------------------------------------
409
410fn hybrid_pub_key_equal(a: &HybridPublicKey, b: &HybridPublicKey) -> bool {
411    a.ed25519 == b.ed25519 && a.ml_dsa_65 == b.ml_dsa_65
412}
413
414fn validate_hybrid_pubkey_lens(pub_key: &HybridPublicKey, label: &str) -> Option<String> {
415    if pub_key.ed25519.len() != ED25519_PUBLIC_KEY_SIZE {
416        return Some(format!(
417            "{} Ed25519 public key has wrong length: {}",
418            label,
419            pub_key.ed25519.len()
420        ));
421    }
422    if pub_key.ml_dsa_65.len() != MLDSA65_PUBLIC_KEY_SIZE {
423        return Some(format!(
424            "{} ML-DSA-65 public key has wrong length: {}",
425            label,
426            pub_key.ml_dsa_65.len()
427        ));
428    }
429    None
430}
431
432fn invalid(reason: &str, msg: &str) -> VerifyResult {
433    VerifyResult {
434        valid: false,
435        identity_status: IdentityStatus::Invalid,
436        human_id: String::new(),
437        agent_id: String::new(),
438        agent_name: String::new(),
439        agent_type: String::new(),
440        granted_scope: Vec::new(),
441        error_reason: format!("{}: {}", reason, msg),
442        anchor: None,
443    }
444}
445
446/// fail_with_status is used when the failure maps to its own identity_status
447/// (scope_denied, constraint_denied, constraint_unverifiable,
448/// delegation_not_authorized). Unknown `status` strings fall back to Invalid
449/// — the wire form for error_reason still reflects the intended status so
450/// audits aren't lossy.
451fn fail_with_status(status: &str, msg: &str) -> VerifyResult {
452    let st = IdentityStatus::from_wire(status).unwrap_or(IdentityStatus::Invalid);
453    VerifyResult {
454        valid: false,
455        identity_status: st,
456        human_id: String::new(),
457        agent_id: String::new(),
458        agent_name: String::new(),
459        agent_type: String::new(),
460        granted_scope: Vec::new(),
461        error_reason: format!("{}: {}", status, msg),
462        anchor: None,
463    }
464}
465
466fn expired(human_id: &str, agent_id: &str) -> VerifyResult {
467    VerifyResult {
468        valid: false,
469        identity_status: IdentityStatus::Expired,
470        human_id: human_id.to_string(),
471        agent_id: agent_id.to_string(),
472        agent_name: String::new(),
473        agent_type: String::new(),
474        granted_scope: Vec::new(),
475        error_reason: "delegation certificate has expired".to_string(),
476        anchor: None,
477    }
478}
479
480fn revoked(human_id: &str, agent_id: &str) -> VerifyResult {
481    VerifyResult {
482        valid: false,
483        identity_status: IdentityStatus::Revoked,
484        human_id: human_id.to_string(),
485        agent_id: agent_id.to_string(),
486        agent_name: String::new(),
487        agent_type: String::new(),
488        granted_scope: Vec::new(),
489        error_reason: "delegation certificate has been revoked".to_string(),
490        anchor: None,
491    }
492}
493
494// ----------------------------------------------------------------------
495// v1.1 transaction receipt verification
496// ----------------------------------------------------------------------
497
498/// Verify a TransactionReceipt: envelope checks, per-party bundle
499/// verification, and party signature verification over the canonical signable.
500pub fn verify_transaction_receipt(
501    receipt: &TransactionReceipt,
502    now: i64,
503) -> TransactionReceiptResult {
504    if receipt.version != PROTOCOL_VERSION {
505        return receipt_fail(&format!(
506            "version_mismatch: unsupported version {}",
507            receipt.version
508        ));
509    }
510    if receipt.transaction_id.is_empty() {
511        return receipt_fail("missing_transaction_id: transaction_id must not be empty");
512    }
513    if receipt.terms_schema_uri.is_empty() {
514        return receipt_fail("missing_terms_schema_uri: terms_schema_uri must not be empty");
515    }
516    if receipt.terms_canonical_json.is_empty() {
517        return receipt_fail(
518            "missing_terms_canonical_json: terms_canonical_json must not be empty",
519        );
520    }
521    if receipt.parties.is_empty() {
522        return receipt_fail("no_parties: receipt must list at least one party");
523    }
524
525    // Party IDs must be unique.
526    let mut party_idx: BTreeMap<&str, usize> = BTreeMap::new();
527    for (i, p) in receipt.parties.iter().enumerate() {
528        if p.party_id.is_empty() {
529            return receipt_fail(&format!("empty_party_id: party {} has no party_id", i));
530        }
531        if party_idx.contains_key(p.party_id.as_str()) {
532            return receipt_fail(&format!(
533                "duplicate_party_id: {:?} listed more than once",
534                p.party_id
535            ));
536        }
537        party_idx.insert(&p.party_id, i);
538    }
539
540    // Each listed party must have exactly one signature; every signature's
541    // party_id must refer to a listed party.
542    let mut sig_by_party: BTreeMap<&str, usize> = BTreeMap::new();
543    for (i, s) in receipt.party_signatures.iter().enumerate() {
544        if !party_idx.contains_key(s.party_id.as_str()) {
545            return receipt_fail(&format!(
546                "unknown_party_signature: signature {} references unknown party_id {:?}",
547                i, s.party_id
548            ));
549        }
550        if sig_by_party.contains_key(s.party_id.as_str()) {
551            return receipt_fail(&format!(
552                "duplicate_party_signature: party {:?} has multiple signatures",
553                s.party_id
554            ));
555        }
556        sig_by_party.insert(&s.party_id, i);
557    }
558    for p in &receipt.parties {
559        if !sig_by_party.contains_key(p.party_id.as_str()) {
560            return receipt_fail(&format!(
561                "missing_party_signature: party {:?} has no signature",
562                p.party_id
563            ));
564        }
565    }
566
567    // Canonical signable bytes.
568    let signable = transaction_receipt_sign_bytes(receipt);
569
570    let mut party_results = Vec::with_capacity(receipt.parties.len());
571    for p in &receipt.parties {
572        // Proof bundle's agent_id / agent_pub_key MUST match the party's.
573        if p.proof_bundle.agent_id != p.agent_id {
574            return receipt_fail_with_results(
575                &format!(
576                    "party_agent_id_mismatch: party {:?} proof_bundle.agent_id={:?} != party.agent_id={:?}",
577                    p.party_id, p.proof_bundle.agent_id, p.agent_id
578                ),
579                party_results,
580            );
581        }
582        if !hybrid_pub_key_equal(&p.proof_bundle.agent_pub_key, &p.agent_pub_key) {
583            return receipt_fail_with_results(
584                &format!(
585                    "party_agent_key_mismatch: party {:?} proof_bundle.agent_pub_key != party.agent_pub_key",
586                    p.party_id
587                ),
588                party_results,
589            );
590        }
591
592        // Bundle verification.
593        let bundle_opts = VerifyOptions {
594            now: Some(now),
595            ..VerifyOptions::default()
596        };
597        let r = verify_bundle(&p.proof_bundle, &bundle_opts);
598        party_results.push(r.clone());
599        if !r.valid {
600            return receipt_fail_with_results(
601                &format!(
602                    "party_bundle_invalid: party {:?} status={} reason={}",
603                    p.party_id,
604                    r.identity_status.as_str(),
605                    r.error_reason
606                ),
607                party_results,
608            );
609        }
610
611        // Party signature check over the atomic signable.
612        let sig_idx = sig_by_party[p.party_id.as_str()];
613        let sig = &receipt.party_signatures[sig_idx].signature;
614        if let Err(e) = verify_both(&signable, sig, &p.agent_pub_key) {
615            return receipt_fail_with_results(
616                &format!("party_signature_invalid: party {:?}: {}", p.party_id, e),
617                party_results,
618            );
619        }
620    }
621
622    TransactionReceiptResult {
623        valid: true,
624        error_reason: String::new(),
625        party_results,
626    }
627}
628
629fn receipt_fail(reason: &str) -> TransactionReceiptResult {
630    TransactionReceiptResult {
631        valid: false,
632        error_reason: reason.to_string(),
633        party_results: Vec::new(),
634    }
635}
636
637fn receipt_fail_with_results(
638    reason: &str,
639    party_results: Vec<VerifyResult>,
640) -> TransactionReceiptResult {
641    TransactionReceiptResult {
642        valid: false,
643        error_reason: reason.to_string(),
644        party_results,
645    }
646}
647
648// ----------------------------------------------------------------------
649// v1.1 session cert cache (ROADMAP 2.3) streamed-turn verification
650// ----------------------------------------------------------------------
651
652/// Fast-path verifier for streamed turns that present a SessionToken in
653/// place of the full cert chain. Checks HMAC, validity window, challenge
654/// freshness, and hybrid challenge signature against token.agent_pub_key.
655/// The chain is NOT re-verified — that's the point of the token.
656#[allow(clippy::too_many_arguments)]
657pub fn verify_streamed_turn(
658    token: &SessionToken,
659    session_secret: &[u8],
660    challenge: &[u8],
661    challenge_at: i64,
662    challenge_sig: &HybridSignature,
663    session_context: &[u8],
664    stream_id: &[u8],
665    stream_seq: i64,
666    now: i64,
667) -> VerifyResult {
668    if let Err(e) = verify_session_token_e(token, session_secret, now) {
669        return invalid("session_token_invalid", &e);
670    }
671    if challenge.is_empty() {
672        return invalid("no_challenge", "streamed turn contains no challenge");
673    }
674    if !session_context.is_empty() && session_context.len() != 32 {
675        return invalid(
676            "invalid_session_context",
677            &format!(
678                "session_context must be 32 bytes, got {}",
679                session_context.len()
680            ),
681        );
682    }
683    if !stream_id.is_empty() && stream_id.len() != 32 {
684        return invalid(
685            "invalid_stream_id",
686            &format!("stream_id must be 32 bytes, got {}", stream_id.len()),
687        );
688    }
689    if !stream_id.is_empty() && stream_seq < 1 {
690        return invalid(
691            "invalid_stream_seq",
692            &format!("stream_seq must be >=1, got {}", stream_seq),
693        );
694    }
695    let challenge_age = now - challenge_at;
696    if challenge_age < 0 || challenge_age > CHALLENGE_WINDOW_SECONDS {
697        return invalid(
698            "stale_challenge",
699            &format!(
700                "challenge is {} seconds old (max {})",
701                challenge_age, CHALLENGE_WINDOW_SECONDS
702            ),
703        );
704    }
705    if let Err(err) = verify_challenge_signature_with_stream(
706        challenge,
707        challenge_at,
708        session_context,
709        stream_id,
710        stream_seq,
711        challenge_sig,
712        &token.agent_pub_key,
713    ) {
714        return invalid(
715            "bad_challenge_sig",
716            &format!("challenge signature verification failed: {}", err),
717        );
718    }
719    VerifyResult {
720        valid: true,
721        identity_status: IdentityStatus::AuthorizedAgent,
722        human_id: token.human_id.clone(),
723        agent_id: token.agent_id.clone(),
724        agent_name: String::new(),
725        agent_type: String::new(),
726        granted_scope: token.granted_scope.clone(),
727        error_reason: String::new(),
728        anchor: None,
729    }
730}