Skip to main content

exo_avc/
validation.rs

1// Copyright 2026 Exochain Foundation
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at:
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14//
15// SPDX-License-Identifier: Apache-2.0
16
17//! AVC validation — fail-closed adjudication of a credential and an
18//! optional action against a registry.
19//!
20//! Validation is **deterministic**: it consumes a `now` timestamp from
21//! the caller (no wall-clock reads), iterates registry data through
22//! `BTreeMap`/`BTreeSet`, and produces decisions whose reason codes are
23//! sorted and deduplicated.
24//!
25//! Validation is **fail-closed**: any unresolved key, missing required
26//! reference, malformed structural value, scope violation, expiration,
27//! or revocation produces an explicit `Deny` with reason codes describing
28//! the failure. Errors are reserved for transport-level failures (CBOR
29//! encoding, registry I/O) and must never silently translate into
30//! `Allow`.
31
32use std::collections::BTreeSet;
33
34use exo_authority::permission::Permission;
35use exo_core::{Did, Hash256, PublicKey, Signature, Timestamp, crypto, hash::hash_structured};
36use serde::{Deserialize, Serialize};
37
38use crate::{
39    credential::{
40        AVC_SCHEMA_VERSION, AuthorityScope, AutonomousVolitionCredential, AvcConstraints, DataClass,
41    },
42    error::AvcError,
43    llm_usage_receipt::{
44        AVC_LLM_USAGE_EVIDENCE_DOMAIN, LlmUsageCustodyMode, LlmUsageEvidence,
45        validate_llm_usage_evidence,
46    },
47    receipt::AvcTrustReceipt,
48    registry::AvcRegistryRead,
49};
50
51/// Signing domain tag for AVC human approval evidence.
52pub const AVC_HUMAN_APPROVAL_SIGNING_DOMAIN: &str = "exo.avc.human-approval.v1";
53/// Signing domain tag for AVC subject action proofs.
54pub const AVC_ACTION_SIGNING_DOMAIN: &str = "exo.avc.action.v1";
55/// Signing domain tag for AVC receipt action commitments.
56pub const AVC_ACTION_COMMITMENT_DOMAIN: &str = "exo.avc.action.commitment.v1";
57/// Signing domain tag for canonical receipt action descriptors.
58pub const AVC_ACTION_DESCRIPTOR_DOMAIN: &str = "exo.avc.action.descriptor.v1";
59/// Canonical AVC action name for EXOCHAIN LYNK Protocol usage receipts.
60pub const AVC_LLM_USAGE_ACTION_NAME: &str = "llm.usage.receipt.emit";
61
62// ---------------------------------------------------------------------------
63// Decision / Reason
64// ---------------------------------------------------------------------------
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
67pub enum AvcDecision {
68    Allow,
69    Deny,
70    HumanApprovalRequired,
71    ChallengeRequired,
72}
73
74#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
75pub enum AvcReasonCode {
76    Valid,
77    InvalidSignature,
78    InvalidIssuer,
79    InvalidSubject,
80    InvalidHolder,
81    Expired,
82    NotYetValid,
83    Revoked,
84    Suspended,
85    Quarantined,
86    AuthorityChainMissing,
87    AuthorityChainInvalid,
88    ScopeWidening,
89    PermissionDenied,
90    ToolDenied,
91    CounterpartyDenied,
92    DataClassDenied,
93    BudgetExceeded,
94    RiskExceeded,
95    HumanApprovalMissing,
96    HumanApprovalInvalid,
97    HumanApprovalExpired,
98    DelegationNotAllowed,
99    ConsentMissing,
100    PolicyMissing,
101    MalformedCredential,
102    ForbiddenAction,
103    OutsideTimeWindow,
104}
105
106// ---------------------------------------------------------------------------
107// Validation request / result
108// ---------------------------------------------------------------------------
109
110#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
111pub struct AvcActionRequest {
112    pub action_id: Hash256,
113    pub actor_did: Did,
114    pub requested_permission: Permission,
115    pub tool: Option<String>,
116    pub target_did: Option<Did>,
117    pub data_class: Option<DataClass>,
118    pub estimated_budget_minor_units: Option<u64>,
119    pub estimated_risk_bp: Option<u32>,
120    #[serde(default)]
121    pub human_approval: Option<AvcHumanApproval>,
122    pub requires_human_approval: bool,
123    /// Free-form action name used to enforce `forbidden_actions`.
124    pub action_name: Option<String>,
125}
126
127#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
128pub struct AvcActionDescriptor {
129    pub schema_version: u16,
130    pub action_id: Hash256,
131    pub actor_did: Did,
132    pub requested_permission: Permission,
133    pub tool: Option<String>,
134    pub target_did: Option<Did>,
135    pub data_class: Option<DataClass>,
136    pub estimated_budget_minor_units: Option<u64>,
137    pub estimated_risk_bp: Option<u32>,
138    pub requires_human_approval: bool,
139    pub human_approval_present: bool,
140    pub action_name: Option<String>,
141}
142
143impl AvcActionDescriptor {
144    #[must_use]
145    pub fn from_action(action: &AvcActionRequest) -> Self {
146        Self {
147            schema_version: AVC_SCHEMA_VERSION,
148            action_id: action.action_id,
149            actor_did: action.actor_did.clone(),
150            requested_permission: action.requested_permission,
151            tool: action.tool.clone(),
152            target_did: action.target_did.clone(),
153            data_class: action.data_class.clone(),
154            estimated_budget_minor_units: action.estimated_budget_minor_units,
155            estimated_risk_bp: action.estimated_risk_bp,
156            requires_human_approval: action.requires_human_approval,
157            human_approval_present: action.human_approval.is_some(),
158            action_name: action.action_name.clone(),
159        }
160    }
161}
162
163#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
164pub struct AvcHumanApproval {
165    pub approver_did: Did,
166    pub approved_at: Timestamp,
167    pub expires_at: Option<Timestamp>,
168    pub signature: Signature,
169}
170
171#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
172pub struct AvcValidationRequest {
173    pub credential: AutonomousVolitionCredential,
174    pub action: Option<AvcActionRequest>,
175    pub now: Timestamp,
176}
177
178#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
179pub struct AvcValidationResult {
180    pub credential_id: Hash256,
181    pub decision: AvcDecision,
182    pub reason_codes: Vec<AvcReasonCode>,
183    pub normalized_holder_did: Did,
184    pub valid_until: Option<Timestamp>,
185    pub receipt: Option<AvcTrustReceipt>,
186}
187
188#[derive(Serialize)]
189struct HumanApprovalSigningPayload<'a> {
190    domain: &'static str,
191    schema_version: u16,
192    credential_id: &'a Hash256,
193    action_id: &'a Hash256,
194    actor_did: &'a Did,
195    requested_permission: &'a Permission,
196    tool: Option<&'a String>,
197    target_did: Option<&'a Did>,
198    data_class: Option<&'a DataClass>,
199    estimated_budget_minor_units: Option<u64>,
200    estimated_risk_bp: Option<u32>,
201    action_name: Option<&'a String>,
202    approver_did: &'a Did,
203    approved_at: &'a Timestamp,
204    expires_at: Option<&'a Timestamp>,
205}
206
207#[derive(Serialize)]
208struct AvcActionSigningPayload<'a> {
209    domain: &'static str,
210    schema_version: u16,
211    credential_id: &'a Hash256,
212    action: &'a AvcActionRequest,
213    validation_now: &'a Timestamp,
214}
215
216#[derive(Serialize)]
217struct AvcActionCommitmentPayload<'a> {
218    domain: &'static str,
219    schema_version: u16,
220    credential_id: &'a Hash256,
221    action: &'a AvcActionRequest,
222    validation_now: &'a Timestamp,
223}
224
225#[derive(Serialize)]
226struct AvcActionDescriptorPayload<'a> {
227    domain: &'static str,
228    descriptor: &'a AvcActionDescriptor,
229}
230
231// ---------------------------------------------------------------------------
232// Validation entry point
233// ---------------------------------------------------------------------------
234
235/// Validate a credential and optional action against a registry.
236///
237/// Decisions are deterministic: the same inputs always yield the same
238/// reason codes in the same order.
239///
240/// # Errors
241/// Returns [`AvcError::Serialization`] if the credential cannot be CBOR
242/// encoded for ID computation. All other failures flow as `Deny`
243/// decisions with reason codes.
244pub fn validate_avc<R: AvcRegistryRead>(
245    request: &AvcValidationRequest,
246    registry: &R,
247) -> Result<AvcValidationResult, AvcError> {
248    let credential = &request.credential;
249    let credential_id = credential.id()?;
250    let normalized_holder_did = credential.effective_holder().clone();
251    let mut reasons: BTreeSet<AvcReasonCode> = BTreeSet::new();
252    let mut human_approval_required = false;
253
254    // Structural checks first — these would otherwise misroute later checks.
255    if credential.created_at > request.now {
256        reasons.insert(AvcReasonCode::NotYetValid);
257    }
258    if let Some(expires) = credential.expires_at {
259        if expires <= request.now {
260            reasons.insert(AvcReasonCode::Expired);
261        }
262    }
263    if let Some(window) = &credential.constraints.allowed_time_window {
264        if !window.contains(&request.now) {
265            reasons.insert(AvcReasonCode::OutsideTimeWindow);
266        }
267    }
268
269    // Signature: resolve issuer key and verify.
270    if credential.signature.is_empty() {
271        reasons.insert(AvcReasonCode::InvalidSignature);
272    } else {
273        match registry.resolve_public_key(&credential.issuer_did) {
274            None => {
275                reasons.insert(AvcReasonCode::InvalidIssuer);
276            }
277            Some(pubkey) => {
278                if !verify_signature(credential, &pubkey)? {
279                    reasons.insert(AvcReasonCode::InvalidSignature);
280                }
281            }
282        }
283    }
284
285    // Authority chain when issuer != principal.
286    if credential.issuer_did != credential.principal_did {
287        match &credential.authority_chain {
288            None => {
289                reasons.insert(AvcReasonCode::AuthorityChainMissing);
290            }
291            Some(chain_ref) => {
292                if !registry.authority_chain_valid(&chain_ref.chain_hash, &request.now) {
293                    reasons.insert(AvcReasonCode::AuthorityChainInvalid);
294                }
295            }
296        }
297    }
298    enforce_registered_issuer_grant(credential, registry, &mut reasons);
299
300    // Revocation.
301    if registry.is_revoked(&credential_id) {
302        reasons.insert(AvcReasonCode::Revoked);
303    }
304
305    // Required consent / policy refs.
306    for consent_ref in &credential.consent_refs {
307        if consent_ref.required && !registry.consent_ref_exists(&consent_ref.consent_id) {
308            reasons.insert(AvcReasonCode::ConsentMissing);
309        }
310    }
311    for policy_ref in &credential.policy_refs {
312        if policy_ref.required
313            && !registry.policy_ref_exists(&policy_ref.policy_id, policy_ref.policy_version)
314        {
315            reasons.insert(AvcReasonCode::PolicyMissing);
316        }
317    }
318
319    // Action fit.
320    if let Some(action) = &request.action {
321        evaluate_action(
322            credential,
323            action,
324            &normalized_holder_did,
325            registry,
326            &request.now,
327            &mut reasons,
328            &mut human_approval_required,
329        )?;
330    }
331
332    let mut sorted: Vec<AvcReasonCode> = reasons.into_iter().collect();
333    let decision = if sorted.is_empty() {
334        sorted.push(AvcReasonCode::Valid);
335        AvcDecision::Allow
336    } else if human_approval_required
337        && reasons_are_only(&sorted, AvcReasonCode::HumanApprovalMissing)
338    {
339        AvcDecision::HumanApprovalRequired
340    } else {
341        AvcDecision::Deny
342    };
343
344    Ok(AvcValidationResult {
345        credential_id,
346        decision,
347        reason_codes: sorted,
348        normalized_holder_did,
349        valid_until: credential.expires_at,
350        receipt: None,
351    })
352}
353
354fn reasons_are_only(reasons: &[AvcReasonCode], expected: AvcReasonCode) -> bool {
355    reasons.len() == 1 && reasons[0] == expected
356}
357
358fn verify_signature(
359    credential: &AutonomousVolitionCredential,
360    pubkey: &PublicKey,
361) -> Result<bool, AvcError> {
362    // Caller ensures `signature.is_empty()` is false before invoking this
363    // helper (see validate_avc). `crypto::verify` itself returns `false`
364    // for `Signature::Empty` defensively, so an empty value here is
365    // simply rejected rather than producing a false positive.
366    let payload = credential.signing_payload()?;
367    Ok(crypto::verify(&payload, &credential.signature, pubkey))
368}
369
370fn enforce_registered_issuer_grant<R: AvcRegistryRead>(
371    credential: &AutonomousVolitionCredential,
372    registry: &R,
373    reasons: &mut BTreeSet<AvcReasonCode>,
374) {
375    let Some(granted_permissions) =
376        registry.resolve_issuer_permission_grant(&credential.issuer_did)
377    else {
378        return;
379    };
380    if granted_permissions.is_empty()
381        || credential
382            .authority_scope
383            .permissions
384            .iter()
385            .any(|permission| !granted_permissions.contains(permission))
386    {
387        reasons.insert(AvcReasonCode::ScopeWidening);
388    }
389}
390
391/// Compute the canonical signing payload for a human approval over a
392/// specific AVC credential/action pair.
393///
394/// The caller-provided `requires_human_approval` flag is deliberately
395/// excluded because it is not proof of approval. Authorization depends
396/// on this signed approval evidence and the trusted human-approver key
397/// registry instead.
398///
399/// # Errors
400/// Returns [`AvcError::Serialization`] if canonical CBOR encoding fails.
401pub fn human_approval_signature_payload(
402    credential: &AutonomousVolitionCredential,
403    action: &AvcActionRequest,
404    approval: &AvcHumanApproval,
405) -> Result<Vec<u8>, AvcError> {
406    let credential_id = credential.id()?;
407    let payload = HumanApprovalSigningPayload {
408        domain: AVC_HUMAN_APPROVAL_SIGNING_DOMAIN,
409        schema_version: AVC_SCHEMA_VERSION,
410        credential_id: &credential_id,
411        action_id: &action.action_id,
412        actor_did: &action.actor_did,
413        requested_permission: &action.requested_permission,
414        tool: action.tool.as_ref(),
415        target_did: action.target_did.as_ref(),
416        data_class: action.data_class.as_ref(),
417        estimated_budget_minor_units: action.estimated_budget_minor_units,
418        estimated_risk_bp: action.estimated_risk_bp,
419        action_name: action.action_name.as_ref(),
420        approver_did: &approval.approver_did,
421        approved_at: &approval.approved_at,
422        expires_at: approval.expires_at.as_ref(),
423    };
424    let mut buf = Vec::new();
425    ciborium::ser::into_writer(&payload, &mut buf)?;
426    Ok(buf)
427}
428
429/// Compute the canonical signing payload for a subject's action proof.
430///
431/// The payload binds the action to the AVC credential ID and the validation
432/// timestamp supplied to the node. This prevents a detached action signature
433/// from being replayed against a different credential or validation context.
434///
435/// # Errors
436/// Returns [`AvcError::Serialization`] if canonical CBOR encoding fails.
437pub fn avc_action_signature_payload(
438    credential: &AutonomousVolitionCredential,
439    action: &AvcActionRequest,
440    validation_now: &Timestamp,
441) -> Result<Vec<u8>, AvcError> {
442    let credential_id = credential.id()?;
443    let payload = AvcActionSigningPayload {
444        domain: AVC_ACTION_SIGNING_DOMAIN,
445        schema_version: AVC_SCHEMA_VERSION,
446        credential_id: &credential_id,
447        action,
448        validation_now,
449    };
450    let mut buf = Vec::new();
451    ciborium::ser::into_writer(&payload, &mut buf)?;
452    Ok(buf)
453}
454
455/// Compute a deterministic commitment over the subject-signed action content.
456///
457/// The commitment binds the full action request to the content-addressed AVC
458/// credential ID and the validation timestamp used by the subject action
459/// signature. It does not claim external anchoring.
460///
461/// # Errors
462/// Returns [`AvcError::Serialization`] if canonical CBOR encoding fails.
463pub fn avc_action_commitment_hash(
464    credential: &AutonomousVolitionCredential,
465    action: &AvcActionRequest,
466    validation_now: &Timestamp,
467) -> Result<Hash256, AvcError> {
468    let credential_id = credential.id()?;
469    let payload = AvcActionCommitmentPayload {
470        domain: AVC_ACTION_COMMITMENT_DOMAIN,
471        schema_version: AVC_SCHEMA_VERSION,
472        credential_id: &credential_id,
473        action,
474        validation_now,
475    };
476    hash_structured(&payload).map_err(AvcError::from)
477}
478
479/// Compute a deterministic hash for the minimal action descriptor embedded in
480/// trust receipts. The descriptor is intentionally narrower than the signed
481/// action request: high-value proof material remains committed by
482/// [`avc_action_commitment_hash`], while the receipt carries enough canonical
483/// action meaning for court/audit reconstruction.
484///
485/// # Errors
486/// Returns [`AvcError::Serialization`] if canonical CBOR encoding fails.
487pub fn avc_action_descriptor_hash(descriptor: &AvcActionDescriptor) -> Result<Hash256, AvcError> {
488    hash_structured(&AvcActionDescriptorPayload {
489        domain: AVC_ACTION_DESCRIPTOR_DOMAIN,
490        descriptor,
491    })
492    .map_err(AvcError::from)
493}
494
495/// Build the AVC action request represented by validated EXOCHAIN LYNK
496/// Protocol evidence.
497///
498/// # Errors
499/// Returns [`AvcError`] when LYNK evidence is structurally invalid.
500pub fn avc_llm_usage_action_request(
501    evidence: &LlmUsageEvidence,
502) -> Result<AvcActionRequest, AvcError> {
503    validate_llm_usage_evidence(evidence)?;
504    Ok(AvcActionRequest {
505        action_id: evidence.action_id,
506        actor_did: evidence.actor_did.clone(),
507        requested_permission: Permission::Execute,
508        tool: Some(AVC_LLM_USAGE_EVIDENCE_DOMAIN.into()),
509        target_did: None,
510        data_class: Some(llm_usage_custody_data_class(evidence.custody_mode)),
511        estimated_budget_minor_units: evidence.usage.cost_minor_units,
512        estimated_risk_bp: None,
513        human_approval: None,
514        requires_human_approval: false,
515        action_name: Some(AVC_LLM_USAGE_ACTION_NAME.into()),
516    })
517}
518
519fn llm_usage_custody_data_class(custody_mode: LlmUsageCustodyMode) -> DataClass {
520    match custody_mode {
521        LlmUsageCustodyMode::ReceiptMinimized => DataClass::Internal,
522        LlmUsageCustodyMode::ExternalPayloadRef => DataClass::Confidential,
523        LlmUsageCustodyMode::DagDbCustody => DataClass::Restricted,
524    }
525}
526
527fn evaluate_action<R: AvcRegistryRead>(
528    credential: &AutonomousVolitionCredential,
529    action: &AvcActionRequest,
530    normalized_holder: &Did,
531    registry: &R,
532    now: &Timestamp,
533    reasons: &mut BTreeSet<AvcReasonCode>,
534    human_approval_required: &mut bool,
535) -> Result<(), AvcError> {
536    if action.actor_did != *normalized_holder && action.actor_did != credential.subject_did {
537        reasons.insert(AvcReasonCode::InvalidHolder);
538    }
539
540    if !credential
541        .authority_scope
542        .permissions
543        .contains(&action.requested_permission)
544    {
545        reasons.insert(AvcReasonCode::PermissionDenied);
546    }
547
548    enforce_tool(&credential.authority_scope, action, reasons);
549    enforce_data_class(&credential.authority_scope, action, reasons);
550    enforce_counterparty(&credential.authority_scope, action, reasons);
551    enforce_budget(&credential.constraints, action, reasons);
552    enforce_risk(
553        credential,
554        &credential.constraints,
555        action,
556        registry,
557        now,
558        reasons,
559        human_approval_required,
560    )?;
561    enforce_forbidden_action(&credential.constraints, action, reasons);
562    Ok(())
563}
564
565fn enforce_tool(
566    scope: &AuthorityScope,
567    action: &AvcActionRequest,
568    reasons: &mut BTreeSet<AvcReasonCode>,
569) {
570    let Some(tool) = &action.tool else {
571        return;
572    };
573    if scope.tools.is_empty() || !scope.tools.iter().any(|t| t == tool) {
574        reasons.insert(AvcReasonCode::ToolDenied);
575    }
576}
577
578fn enforce_data_class(
579    scope: &AuthorityScope,
580    action: &AvcActionRequest,
581    reasons: &mut BTreeSet<AvcReasonCode>,
582) {
583    let Some(class) = &action.data_class else {
584        return;
585    };
586    if !scope.data_classes.iter().any(|c| c == class) {
587        reasons.insert(AvcReasonCode::DataClassDenied);
588    }
589}
590
591fn enforce_counterparty(
592    scope: &AuthorityScope,
593    action: &AvcActionRequest,
594    reasons: &mut BTreeSet<AvcReasonCode>,
595) {
596    let Some(target) = &action.target_did else {
597        return;
598    };
599    if !scope.counterparties.is_empty() && !scope.counterparties.iter().any(|d| d == target) {
600        reasons.insert(AvcReasonCode::CounterpartyDenied);
601    }
602}
603
604fn enforce_budget(
605    constraints: &AvcConstraints,
606    action: &AvcActionRequest,
607    reasons: &mut BTreeSet<AvcReasonCode>,
608) {
609    if let (Some(cap), Some(estimate)) = (
610        constraints.max_budget_minor_units,
611        action.estimated_budget_minor_units,
612    ) {
613        if estimate > cap {
614            reasons.insert(AvcReasonCode::BudgetExceeded);
615        }
616    }
617}
618
619fn enforce_risk<R: AvcRegistryRead>(
620    credential: &AutonomousVolitionCredential,
621    constraints: &AvcConstraints,
622    action: &AvcActionRequest,
623    registry: &R,
624    now: &Timestamp,
625    reasons: &mut BTreeSet<AvcReasonCode>,
626    human_approval_required: &mut bool,
627) -> Result<(), AvcError> {
628    let risk_threshold_requires_approval = if let (Some(threshold), Some(estimate)) =
629        (constraints.approval_threshold_bp, action.estimated_risk_bp)
630    {
631        estimate >= threshold
632    } else {
633        false
634    };
635    if let (Some(cap), Some(estimate)) = (constraints.max_action_risk_bp, action.estimated_risk_bp)
636    {
637        if estimate > cap {
638            reasons.insert(AvcReasonCode::RiskExceeded);
639        }
640    }
641
642    let approval_required = constraints.human_approval_required || risk_threshold_requires_approval;
643    if approval_required {
644        *human_approval_required = true;
645    }
646    if approval_required || action.human_approval.is_some() {
647        match verify_human_approval(credential, action, registry, now)? {
648            Ok(()) => {}
649            Err(reason) => {
650                reasons.insert(reason);
651            }
652        }
653    }
654    Ok(())
655}
656
657fn verify_human_approval<R: AvcRegistryRead>(
658    credential: &AutonomousVolitionCredential,
659    action: &AvcActionRequest,
660    registry: &R,
661    now: &Timestamp,
662) -> Result<Result<(), AvcReasonCode>, AvcError> {
663    let Some(approval) = &action.human_approval else {
664        return Ok(Err(AvcReasonCode::HumanApprovalMissing));
665    };
666    if approval.signature.is_empty() || approval.approved_at > *now {
667        return Ok(Err(AvcReasonCode::HumanApprovalInvalid));
668    }
669    if let Some(expires_at) = approval.expires_at {
670        if expires_at <= approval.approved_at {
671            return Ok(Err(AvcReasonCode::HumanApprovalInvalid));
672        }
673        if expires_at <= *now {
674            return Ok(Err(AvcReasonCode::HumanApprovalExpired));
675        }
676    }
677
678    let Some(public_key) = registry.resolve_human_approval_key(&approval.approver_did) else {
679        return Ok(Err(AvcReasonCode::HumanApprovalInvalid));
680    };
681    let payload = human_approval_signature_payload(credential, action, approval)?;
682    if crypto::verify(&payload, &approval.signature, &public_key) {
683        Ok(Ok(()))
684    } else {
685        Ok(Err(AvcReasonCode::HumanApprovalInvalid))
686    }
687}
688
689fn enforce_forbidden_action(
690    constraints: &AvcConstraints,
691    action: &AvcActionRequest,
692    reasons: &mut BTreeSet<AvcReasonCode>,
693) {
694    let Some(name) = &action.action_name else {
695        return;
696    };
697    if constraints.forbidden_actions.iter().any(|a| a == name) {
698        reasons.insert(AvcReasonCode::ForbiddenAction);
699    }
700}
701
702#[cfg(test)]
703mod tests {
704    use exo_core::crypto::KeyPair;
705
706    use super::*;
707    use crate::{
708        credential::{
709            AVC_SCHEMA_VERSION, AuthorityChainRef, AvcConstraints, AvcDraft, AvcSubjectKind,
710            ConsentRef, PolicyRef, TimeWindow, issue_avc, test_support::*,
711        },
712        llm_usage_receipt::{EncryptedPayloadRef, ProviderUsageMetrics},
713        registry::{AvcRegistryWrite, InMemoryAvcRegistry},
714        revocation::{AvcRevocationReason, revoke_avc},
715    };
716
717    const ISSUER_SEED: [u8; 32] = [0x11; 32];
718    const HUMAN_APPROVER_SEED: [u8; 32] = [0x44; 32];
719
720    fn issuer_keypair() -> KeyPair {
721        KeyPair::from_secret_bytes(ISSUER_SEED).expect("valid seed")
722    }
723
724    fn human_approver_keypair() -> KeyPair {
725        KeyPair::from_secret_bytes(HUMAN_APPROVER_SEED).expect("valid seed")
726    }
727
728    /// Build a registry seeded with the issuer's public key.
729    struct Harness {
730        registry: InMemoryAvcRegistry,
731    }
732
733    impl Harness {
734        fn new() -> Self {
735            let mut registry = InMemoryAvcRegistry::new();
736            registry.put_public_key(did("issuer"), issuer_keypair().public);
737            Self { registry }
738        }
739
740        fn issue(&self, draft: AvcDraft) -> AutonomousVolitionCredential {
741            issue_avc(draft, |bytes| issuer_keypair().sign(bytes)).unwrap()
742        }
743    }
744
745    fn baseline_request(
746        cred: AutonomousVolitionCredential,
747        now: Timestamp,
748    ) -> AvcValidationRequest {
749        AvcValidationRequest {
750            credential: cred,
751            action: None,
752            now,
753        }
754    }
755
756    fn baseline_action(actor: Did) -> AvcActionRequest {
757        AvcActionRequest {
758            action_id: h256(0x55),
759            actor_did: actor,
760            requested_permission: Permission::Read,
761            tool: None,
762            target_did: None,
763            data_class: None,
764            estimated_budget_minor_units: None,
765            estimated_risk_bp: None,
766            human_approval: None,
767            requires_human_approval: false,
768            action_name: None,
769        }
770    }
771
772    fn baseline_llm_usage_evidence(custody_mode: LlmUsageCustodyMode) -> LlmUsageEvidence {
773        let mut evidence = LlmUsageEvidence {
774            schema_version: AVC_SCHEMA_VERSION,
775            tenant_id: "tenant-alpha".into(),
776            namespace: "default".into(),
777            actor_did: did("agent"),
778            provider: "openai".into(),
779            provider_endpoint: "responses".into(),
780            model_id: "gpt-test".into(),
781            provider_request_id_hash: Some(h256(0xA1)),
782            session_id_hash: Some(h256(0xA2)),
783            idempotency_key_hash: h256(0xA3),
784            action_id: h256(0xA4),
785            prompt_hash: h256(0xA5),
786            completion_hash: Some(h256(0xA6)),
787            tool_call_hash: None,
788            tool_result_hash: None,
789            usage: ProviderUsageMetrics {
790                input_tokens: 10,
791                output_tokens: 20,
792                total_tokens: 30,
793                cached_input_tokens: None,
794                reasoning_tokens: None,
795                cost_minor_units: Some(125),
796                cost_currency: Some("USD".into()),
797                usage_complete: true,
798            },
799            custody_mode,
800            encrypted_payload_refs: Vec::new(),
801            custody_policy_hash: h256(0xA7),
802            created_at: ts(1_600_000),
803        };
804        if custody_mode == LlmUsageCustodyMode::ExternalPayloadRef {
805            evidence.encrypted_payload_refs = vec![EncryptedPayloadRef {
806                ref_id_hash: h256(0xB1),
807                ciphertext_hash: h256(0xB2),
808                storage_policy_hash: h256(0xB3),
809                key_policy_hash: h256(0xB4),
810                payload_kind: "provider_exchange".into(),
811                byte_length: 128,
812            }];
813        }
814        evidence
815    }
816
817    fn attach_signed_human_approval(
818        credential: &AutonomousVolitionCredential,
819        action: &mut AvcActionRequest,
820        approver_did: Did,
821        approved_at: Timestamp,
822        expires_at: Option<Timestamp>,
823        approver_keypair: &KeyPair,
824    ) {
825        action.human_approval = Some(AvcHumanApproval {
826            approver_did,
827            approved_at,
828            expires_at,
829            signature: Signature::empty(),
830        });
831        let payload = human_approval_signature_payload(
832            credential,
833            action,
834            action
835                .human_approval
836                .as_ref()
837                .expect("approval placeholder"),
838        )
839        .expect("canonical approval payload");
840        action
841            .human_approval
842            .as_mut()
843            .expect("approval placeholder")
844            .signature = approver_keypair.sign(&payload);
845    }
846
847    #[test]
848    fn llm_usage_action_request_uses_execute_and_evidence_fields() {
849        let evidence = baseline_llm_usage_evidence(LlmUsageCustodyMode::ReceiptMinimized);
850        let action = avc_llm_usage_action_request(&evidence).expect("LYNK action");
851
852        assert_eq!(action.action_id, evidence.action_id);
853        assert_eq!(action.actor_did, evidence.actor_did);
854        assert_eq!(action.requested_permission, Permission::Execute);
855        assert_eq!(action.tool, Some(AVC_LLM_USAGE_EVIDENCE_DOMAIN.into()));
856        assert_eq!(action.data_class, Some(DataClass::Internal));
857        assert_eq!(action.estimated_budget_minor_units, Some(125));
858        assert_eq!(action.estimated_risk_bp, None);
859        assert_eq!(action.action_name, Some(AVC_LLM_USAGE_ACTION_NAME.into()));
860        assert!(!action.requires_human_approval);
861        assert!(action.human_approval.is_none());
862    }
863
864    #[test]
865    fn llm_usage_action_request_derives_data_class_from_custody_mode() {
866        let minimized = avc_llm_usage_action_request(&baseline_llm_usage_evidence(
867            LlmUsageCustodyMode::ReceiptMinimized,
868        ))
869        .expect("minimized action");
870        let external = avc_llm_usage_action_request(&baseline_llm_usage_evidence(
871            LlmUsageCustodyMode::ExternalPayloadRef,
872        ))
873        .expect("external action");
874        let dagdb = avc_llm_usage_action_request(&baseline_llm_usage_evidence(
875            LlmUsageCustodyMode::DagDbCustody,
876        ))
877        .expect("dagdb action");
878
879        assert_eq!(minimized.data_class, Some(DataClass::Internal));
880        assert_eq!(external.data_class, Some(DataClass::Confidential));
881        assert_eq!(dagdb.data_class, Some(DataClass::Restricted));
882    }
883
884    #[test]
885    fn llm_usage_action_request_fails_closed_on_invalid_evidence() {
886        let mut evidence = baseline_llm_usage_evidence(LlmUsageCustodyMode::ReceiptMinimized);
887        evidence.provider_endpoint = " ".into();
888
889        assert!(matches!(
890            avc_llm_usage_action_request(&evidence),
891            Err(AvcError::EmptyField {
892                field: "llm_usage.provider_endpoint"
893            })
894        ));
895    }
896
897    #[test]
898    fn valid_credential_allows() {
899        let h = Harness::new();
900        let cred = h.issue(baseline_draft());
901        let request = baseline_request(cred, ts(1_500_000));
902        let result = validate_avc(&request, &h.registry).unwrap();
903        assert_eq!(result.decision, AvcDecision::Allow);
904        assert_eq!(result.reason_codes, vec![AvcReasonCode::Valid]);
905    }
906
907    #[test]
908    fn allows_credential_when_issuer_has_no_registered_grant() {
909        let h = Harness::new();
910        let mut draft = baseline_draft();
911        draft.authority_scope.permissions = vec![Permission::Read, Permission::Write];
912        let cred = h.issue(draft);
913
914        let result = validate_avc(&baseline_request(cred, ts(1_500_000)), &h.registry).unwrap();
915
916        assert_eq!(result.decision, AvcDecision::Allow);
917        assert_eq!(result.reason_codes, vec![AvcReasonCode::Valid]);
918    }
919
920    #[test]
921    fn allows_credential_scope_within_registered_issuer_grant() {
922        let mut h = Harness::new();
923        h.registry
924            .put_issuer_permission_grant(did("issuer"), vec![Permission::Read]);
925        let mut draft = baseline_draft();
926        draft.authority_scope.permissions = vec![Permission::Read];
927        let cred = h.issue(draft);
928
929        let result = validate_avc(&baseline_request(cred, ts(1_500_000)), &h.registry).unwrap();
930
931        assert_eq!(result.decision, AvcDecision::Allow);
932        assert_eq!(result.reason_codes, vec![AvcReasonCode::Valid]);
933    }
934
935    #[test]
936    fn allows_credential_scope_with_duplicate_registered_grant_entries() {
937        let mut h = Harness::new();
938        h.registry.put_issuer_permission_grant(
939            did("issuer"),
940            vec![Permission::Write, Permission::Read, Permission::Write],
941        );
942        let mut draft = baseline_draft();
943        draft.authority_scope.permissions = vec![Permission::Read, Permission::Write];
944        let cred = h.issue(draft);
945
946        let result = validate_avc(&baseline_request(cred, ts(1_500_000)), &h.registry).unwrap();
947
948        assert_eq!(result.decision, AvcDecision::Allow);
949        assert_eq!(result.reason_codes, vec![AvcReasonCode::Valid]);
950    }
951
952    #[test]
953    fn action_signature_payload_is_domain_separated_and_context_bound() {
954        let h = Harness::new();
955        let cred = h.issue(baseline_draft());
956        let action = baseline_action(cred.subject_did.clone());
957        let payload_one = avc_action_signature_payload(&cred, &action, &ts(1_500_000)).unwrap();
958        let payload_two = avc_action_signature_payload(&cred, &action, &ts(1_500_001)).unwrap();
959        let needle = AVC_ACTION_SIGNING_DOMAIN.as_bytes();
960
961        assert!(payload_one.windows(needle.len()).any(|w| w == needle));
962        assert_ne!(payload_one, payload_two);
963    }
964
965    #[test]
966    fn denies_unknown_issuer_key() {
967        let h = Harness::new();
968        let mut draft = baseline_draft();
969        draft.issuer_did = did("ghost");
970        draft.principal_did = did("ghost"); // ghost is also principal so authority chain not required
971        let cred = h.issue(draft);
972        let result = validate_avc(&baseline_request(cred, ts(1_500_000)), &h.registry).unwrap();
973        assert_eq!(result.decision, AvcDecision::Deny);
974        assert!(result.reason_codes.contains(&AvcReasonCode::InvalidIssuer));
975    }
976
977    #[test]
978    fn denies_empty_signature() {
979        let h = Harness::new();
980        let mut cred = h.issue(baseline_draft());
981        cred.signature = Signature::empty();
982        let result = validate_avc(&baseline_request(cred, ts(1_500_000)), &h.registry).unwrap();
983        assert_eq!(result.decision, AvcDecision::Deny);
984        assert!(
985            result
986                .reason_codes
987                .contains(&AvcReasonCode::InvalidSignature)
988        );
989    }
990
991    #[test]
992    fn denies_invalid_signature_when_payload_tampered() {
993        let h = Harness::new();
994        let mut cred = h.issue(baseline_draft());
995        // Mutate after signing — payload no longer matches signature.
996        cred.delegated_intent.purpose = "tampered".into();
997        let result = validate_avc(&baseline_request(cred, ts(1_500_000)), &h.registry).unwrap();
998        assert_eq!(result.decision, AvcDecision::Deny);
999        assert!(
1000            result
1001                .reason_codes
1002                .contains(&AvcReasonCode::InvalidSignature)
1003        );
1004    }
1005
1006    #[test]
1007    fn denies_wrong_key_signature() {
1008        let h = Harness::new();
1009        let other = KeyPair::from_secret_bytes([0x99; 32]).unwrap();
1010        let mut cred = h.issue(baseline_draft());
1011        // Re-sign with a different key.
1012        let payload = cred.signing_payload().unwrap();
1013        cred.signature = other.sign(&payload);
1014        let result = validate_avc(&baseline_request(cred, ts(1_500_000)), &h.registry).unwrap();
1015        assert_eq!(result.decision, AvcDecision::Deny);
1016        assert!(
1017            result
1018                .reason_codes
1019                .contains(&AvcReasonCode::InvalidSignature)
1020        );
1021    }
1022
1023    #[test]
1024    fn denies_expired_credential() {
1025        let h = Harness::new();
1026        let cred = h.issue(baseline_draft());
1027        let result = validate_avc(&baseline_request(cred, ts(3_000_000)), &h.registry).unwrap();
1028        assert_eq!(result.decision, AvcDecision::Deny);
1029        assert!(result.reason_codes.contains(&AvcReasonCode::Expired));
1030    }
1031
1032    #[test]
1033    fn denies_not_yet_valid_credential() {
1034        let h = Harness::new();
1035        let cred = h.issue(baseline_draft());
1036        let result = validate_avc(&baseline_request(cred, ts(0)), &h.registry).unwrap();
1037        assert_eq!(result.decision, AvcDecision::Deny);
1038        assert!(result.reason_codes.contains(&AvcReasonCode::NotYetValid));
1039    }
1040
1041    #[test]
1042    fn denies_outside_time_window() {
1043        let h = Harness::new();
1044        let mut draft = baseline_draft();
1045        draft.constraints.allowed_time_window = Some(TimeWindow {
1046            not_before: ts(1_400_000),
1047            not_after: ts(1_450_000),
1048        });
1049        let cred = h.issue(draft);
1050        let result = validate_avc(&baseline_request(cred, ts(1_500_000)), &h.registry).unwrap();
1051        assert!(
1052            result
1053                .reason_codes
1054                .contains(&AvcReasonCode::OutsideTimeWindow)
1055        );
1056    }
1057
1058    #[test]
1059    fn denies_revoked_credential() {
1060        let mut h = Harness::new();
1061        let cred = h.issue(baseline_draft());
1062        let id = cred.id().unwrap();
1063        h.registry.put_credential(cred.clone()).unwrap();
1064        let revocation = revoke_avc(
1065            id,
1066            did("issuer"),
1067            AvcRevocationReason::IssuerRevoked,
1068            ts(1_250_000),
1069            |bytes| issuer_keypair().sign(bytes),
1070        )
1071        .unwrap();
1072        h.registry.put_revocation(revocation).unwrap();
1073        let result = validate_avc(&baseline_request(cred, ts(1_500_000)), &h.registry).unwrap();
1074        assert_eq!(result.decision, AvcDecision::Deny);
1075        assert!(result.reason_codes.contains(&AvcReasonCode::Revoked));
1076    }
1077
1078    #[test]
1079    fn denies_missing_authority_chain_when_issuer_differs_from_principal() {
1080        let h = Harness::new();
1081        let mut draft = baseline_draft();
1082        draft.principal_did = did("principal");
1083        // No authority_chain supplied.
1084        let cred = h.issue(draft);
1085        let result = validate_avc(&baseline_request(cred, ts(1_500_000)), &h.registry).unwrap();
1086        assert!(
1087            result
1088                .reason_codes
1089                .contains(&AvcReasonCode::AuthorityChainMissing)
1090        );
1091    }
1092
1093    #[test]
1094    fn denies_invalid_authority_chain_hash() {
1095        let h = Harness::new();
1096        let mut draft = baseline_draft();
1097        draft.principal_did = did("principal");
1098        draft.authority_chain = Some(AuthorityChainRef {
1099            chain_hash: h256(0xDE),
1100        });
1101        let cred = h.issue(draft);
1102        let result = validate_avc(&baseline_request(cred, ts(1_500_000)), &h.registry).unwrap();
1103        assert!(
1104            result
1105                .reason_codes
1106                .contains(&AvcReasonCode::AuthorityChainInvalid)
1107        );
1108    }
1109
1110    #[test]
1111    fn accepts_valid_authority_chain_hash() {
1112        let mut h = Harness::new();
1113        let mut draft = baseline_draft();
1114        draft.principal_did = did("principal");
1115        draft.authority_chain = Some(AuthorityChainRef {
1116            chain_hash: h256(0xDE),
1117        });
1118        h.registry.mark_authority_chain_valid(h256(0xDE));
1119        let cred = h.issue(draft);
1120        let result = validate_avc(&baseline_request(cred, ts(1_500_000)), &h.registry).unwrap();
1121        assert_eq!(result.decision, AvcDecision::Allow);
1122    }
1123
1124    #[test]
1125    fn denies_missing_required_consent_ref() {
1126        let h = Harness::new();
1127        let mut draft = baseline_draft();
1128        draft.consent_refs = vec![ConsentRef {
1129            consent_id: h256(0xC0),
1130            required: true,
1131        }];
1132        let cred = h.issue(draft);
1133        let result = validate_avc(&baseline_request(cred, ts(1_500_000)), &h.registry).unwrap();
1134        assert!(result.reason_codes.contains(&AvcReasonCode::ConsentMissing));
1135    }
1136
1137    #[test]
1138    fn allows_when_optional_consent_ref_missing() {
1139        let h = Harness::new();
1140        let mut draft = baseline_draft();
1141        draft.consent_refs = vec![ConsentRef {
1142            consent_id: h256(0xC0),
1143            required: false,
1144        }];
1145        let cred = h.issue(draft);
1146        let result = validate_avc(&baseline_request(cred, ts(1_500_000)), &h.registry).unwrap();
1147        assert_eq!(result.decision, AvcDecision::Allow);
1148    }
1149
1150    #[test]
1151    fn denies_missing_required_policy_ref() {
1152        let h = Harness::new();
1153        let mut draft = baseline_draft();
1154        draft.policy_refs = vec![PolicyRef {
1155            policy_id: h256(0xB1),
1156            policy_version: 2,
1157            required: true,
1158        }];
1159        let cred = h.issue(draft);
1160        let result = validate_avc(&baseline_request(cred, ts(1_500_000)), &h.registry).unwrap();
1161        assert!(result.reason_codes.contains(&AvcReasonCode::PolicyMissing));
1162    }
1163
1164    #[test]
1165    fn denies_actor_mismatch() {
1166        let h = Harness::new();
1167        let cred = h.issue(baseline_draft());
1168        let mut request = baseline_request(cred, ts(1_500_000));
1169        request.action = Some(baseline_action(did("imposter")));
1170        let result = validate_avc(&request, &h.registry).unwrap();
1171        assert!(result.reason_codes.contains(&AvcReasonCode::InvalidHolder));
1172    }
1173
1174    #[test]
1175    fn denies_permission_outside_scope() {
1176        let h = Harness::new();
1177        let cred = h.issue(baseline_draft());
1178        let actor = cred.subject_did.clone();
1179        let mut action = baseline_action(actor);
1180        action.requested_permission = Permission::Govern;
1181        let mut request = baseline_request(cred, ts(1_500_000));
1182        request.action = Some(action);
1183        let result = validate_avc(&request, &h.registry).unwrap();
1184        assert!(
1185            result
1186                .reason_codes
1187                .contains(&AvcReasonCode::PermissionDenied)
1188        );
1189    }
1190
1191    #[test]
1192    fn denies_credential_scope_wider_than_registered_issuer_grant() {
1193        let mut h = Harness::new();
1194        h.registry.put_issuer_permission_grant(
1195            did("issuer"),
1196            vec![
1197                Permission::Read,
1198                Permission::Write,
1199                Permission::Execute,
1200                Permission::Delegate,
1201            ],
1202        );
1203        let mut draft = baseline_draft();
1204        draft.authority_scope.permissions = vec![Permission::Govern];
1205        let cred = h.issue(draft);
1206        let actor = cred.subject_did.clone();
1207        let mut action = baseline_action(actor);
1208        action.requested_permission = Permission::Govern;
1209        let mut request = baseline_request(cred, ts(1_500_000));
1210        request.action = Some(action);
1211
1212        let result = validate_avc(&request, &h.registry).unwrap();
1213
1214        assert_eq!(result.decision, AvcDecision::Deny);
1215        assert!(
1216            result.reason_codes.contains(&AvcReasonCode::ScopeWidening),
1217            "root issuer grants must cap credential-declared permissions"
1218        );
1219    }
1220
1221    #[test]
1222    fn denies_all_credentials_from_issuer_capped_to_empty_permission_set() {
1223        let mut h = Harness::new();
1224        h.registry
1225            .put_issuer_permission_grant(did("issuer"), vec![]);
1226        let mut draft = baseline_draft();
1227        draft.authority_scope.permissions = vec![];
1228        let cred = h.issue(draft);
1229        let request = baseline_request(cred, ts(1_500_000));
1230
1231        let result = validate_avc(&request, &h.registry).unwrap();
1232
1233        assert_eq!(result.decision, AvcDecision::Deny);
1234        assert!(
1235            result.reason_codes.contains(&AvcReasonCode::ScopeWidening),
1236            "an issuer capped to no permissions must be able to sign nothing"
1237        );
1238    }
1239
1240    #[test]
1241    fn denies_any_credential_permission_outside_registered_issuer_grant() {
1242        let mut h = Harness::new();
1243        h.registry
1244            .put_issuer_permission_grant(did("issuer"), vec![Permission::Read]);
1245        let mut draft = baseline_draft();
1246        draft.authority_scope.permissions = vec![Permission::Read, Permission::Write];
1247        let cred = h.issue(draft);
1248
1249        let result = validate_avc(&baseline_request(cred, ts(1_500_000)), &h.registry).unwrap();
1250
1251        assert_eq!(result.decision, AvcDecision::Deny);
1252        assert!(
1253            result.reason_codes.contains(&AvcReasonCode::ScopeWidening),
1254            "any credential permission outside the issuer grant must fail closed"
1255        );
1256    }
1257
1258    #[test]
1259    fn denies_tool_outside_scope() {
1260        let h = Harness::new();
1261        let cred = h.issue(baseline_draft());
1262        let actor = cred.subject_did.clone();
1263        let mut action = baseline_action(actor);
1264        action.tool = Some("ungoverned".into());
1265        let mut request = baseline_request(cred, ts(1_500_000));
1266        request.action = Some(action);
1267        let result = validate_avc(&request, &h.registry).unwrap();
1268        assert!(result.reason_codes.contains(&AvcReasonCode::ToolDenied));
1269    }
1270
1271    #[test]
1272    fn empty_tool_scope_denies_any_tool_action() {
1273        let h = Harness::new();
1274        let mut draft = baseline_draft();
1275        draft.authority_scope.tools = vec![];
1276        let cred = h.issue(draft);
1277        let actor = cred.subject_did.clone();
1278        let mut action = baseline_action(actor);
1279        action.tool = Some("anything".into());
1280        let mut request = baseline_request(cred, ts(1_500_000));
1281        request.action = Some(action);
1282        let result = validate_avc(&request, &h.registry).unwrap();
1283        assert!(result.reason_codes.contains(&AvcReasonCode::ToolDenied));
1284    }
1285
1286    #[test]
1287    fn empty_tool_scope_allows_action_without_tool() {
1288        let h = Harness::new();
1289        let mut draft = baseline_draft();
1290        draft.authority_scope.tools = vec![];
1291        let cred = h.issue(draft);
1292        let actor = cred.subject_did.clone();
1293        let action = baseline_action(actor);
1294        let mut request = baseline_request(cred, ts(1_500_000));
1295        request.action = Some(action);
1296        let result = validate_avc(&request, &h.registry).unwrap();
1297        assert_eq!(result.decision, AvcDecision::Allow);
1298    }
1299
1300    #[test]
1301    fn denies_data_class_outside_scope() {
1302        let h = Harness::new();
1303        let cred = h.issue(baseline_draft());
1304        let actor = cred.subject_did.clone();
1305        let mut action = baseline_action(actor);
1306        action.data_class = Some(DataClass::SensitivePersonalData);
1307        let mut request = baseline_request(cred, ts(1_500_000));
1308        request.action = Some(action);
1309        let result = validate_avc(&request, &h.registry).unwrap();
1310        assert!(
1311            result
1312                .reason_codes
1313                .contains(&AvcReasonCode::DataClassDenied)
1314        );
1315    }
1316
1317    #[test]
1318    fn denies_counterparty_when_allowlist_present() {
1319        let h = Harness::new();
1320        let mut draft = baseline_draft();
1321        draft.authority_scope.counterparties = vec![did("approved-cp")];
1322        let cred = h.issue(draft);
1323        let actor = cred.subject_did.clone();
1324        let mut action = baseline_action(actor);
1325        action.target_did = Some(did("malicious-cp"));
1326        let mut request = baseline_request(cred, ts(1_500_000));
1327        request.action = Some(action);
1328        let result = validate_avc(&request, &h.registry).unwrap();
1329        assert!(
1330            result
1331                .reason_codes
1332                .contains(&AvcReasonCode::CounterpartyDenied)
1333        );
1334    }
1335
1336    #[test]
1337    fn empty_counterparty_list_allows_any_target() {
1338        let h = Harness::new();
1339        let cred = h.issue(baseline_draft());
1340        let actor = cred.subject_did.clone();
1341        let mut action = baseline_action(actor);
1342        action.target_did = Some(did("any"));
1343        let mut request = baseline_request(cred, ts(1_500_000));
1344        request.action = Some(action);
1345        let result = validate_avc(&request, &h.registry).unwrap();
1346        assert_eq!(result.decision, AvcDecision::Allow);
1347    }
1348
1349    #[test]
1350    fn denies_budget_exceeded() {
1351        let h = Harness::new();
1352        let mut draft = baseline_draft();
1353        draft.constraints.max_budget_minor_units = Some(1_000);
1354        let cred = h.issue(draft);
1355        let actor = cred.subject_did.clone();
1356        let mut action = baseline_action(actor);
1357        action.estimated_budget_minor_units = Some(2_000);
1358        let mut request = baseline_request(cred, ts(1_500_000));
1359        request.action = Some(action);
1360        let result = validate_avc(&request, &h.registry).unwrap();
1361        assert!(result.reason_codes.contains(&AvcReasonCode::BudgetExceeded));
1362    }
1363
1364    #[test]
1365    fn in_scope_action_at_budget_and_risk_caps_allows() {
1366        let h = Harness::new();
1367        let mut draft = baseline_draft();
1368        draft.authority_scope.counterparties = vec![did("approved-cp")];
1369        draft.constraints.max_budget_minor_units = Some(1_000);
1370        draft.constraints.max_action_risk_bp = Some(1_000);
1371        let cred = h.issue(draft);
1372        let actor = cred.subject_did.clone();
1373        let mut action = baseline_action(actor);
1374        action.tool = Some("alpha".into());
1375        action.data_class = Some(DataClass::Public);
1376        action.target_did = Some(did("approved-cp"));
1377        action.estimated_budget_minor_units = Some(1_000);
1378        action.estimated_risk_bp = Some(1_000);
1379        let mut request = baseline_request(cred, ts(1_500_000));
1380        request.action = Some(action);
1381        let result = validate_avc(&request, &h.registry).unwrap();
1382        assert_eq!(result.decision, AvcDecision::Allow);
1383        assert_eq!(result.reason_codes, vec![AvcReasonCode::Valid]);
1384    }
1385
1386    #[test]
1387    fn in_scope_action_with_allowed_tool_allows() {
1388        let h = Harness::new();
1389        let cred = h.issue(baseline_draft());
1390        let actor = cred.subject_did.clone();
1391        let mut action = baseline_action(actor);
1392        action.tool = Some("alpha".into());
1393        let mut request = baseline_request(cred, ts(1_500_000));
1394        request.action = Some(action);
1395        let result = validate_avc(&request, &h.registry).unwrap();
1396        assert_eq!(result.decision, AvcDecision::Allow);
1397        assert_eq!(result.reason_codes, vec![AvcReasonCode::Valid]);
1398    }
1399
1400    #[test]
1401    fn non_expiring_credential_allows_explicit_holder_action() {
1402        let h = Harness::new();
1403        let mut draft = baseline_draft();
1404        draft.holder_did = Some(did("holder"));
1405        draft.expires_at = None;
1406        let cred = h.issue(draft);
1407        let mut request = baseline_request(cred, ts(1_500_000));
1408        request.action = Some(baseline_action(did("holder")));
1409        let result = validate_avc(&request, &h.registry).unwrap();
1410        assert_eq!(result.decision, AvcDecision::Allow);
1411        assert_eq!(result.reason_codes, vec![AvcReasonCode::Valid]);
1412        assert_eq!(result.normalized_holder_did, did("holder"));
1413        assert_eq!(result.valid_until, None);
1414    }
1415
1416    #[test]
1417    fn subject_actor_remains_valid_when_holder_is_explicit() {
1418        let h = Harness::new();
1419        let mut draft = baseline_draft();
1420        draft.holder_did = Some(did("holder"));
1421        let cred = h.issue(draft);
1422        let mut request = baseline_request(cred, ts(1_500_000));
1423        request.action = Some(baseline_action(did("agent")));
1424        let result = validate_avc(&request, &h.registry).unwrap();
1425        assert_eq!(result.decision, AvcDecision::Allow);
1426        assert_eq!(result.reason_codes, vec![AvcReasonCode::Valid]);
1427        assert_eq!(result.normalized_holder_did, did("holder"));
1428    }
1429
1430    #[test]
1431    fn risk_at_approval_threshold_requires_human_approval() {
1432        let h = Harness::new();
1433        let mut draft = baseline_draft();
1434        draft.constraints.max_action_risk_bp = Some(10_000);
1435        draft.constraints.approval_threshold_bp = Some(5_000);
1436        let cred = h.issue(draft);
1437        let actor = cred.subject_did.clone();
1438        let mut action = baseline_action(actor);
1439        action.estimated_risk_bp = Some(5_000);
1440        let mut request = baseline_request(cred, ts(1_500_000));
1441        request.action = Some(action);
1442        let result = validate_avc(&request, &h.registry).unwrap();
1443        assert_eq!(result.decision, AvcDecision::HumanApprovalRequired);
1444        assert_eq!(
1445            result.reason_codes,
1446            vec![AvcReasonCode::HumanApprovalMissing]
1447        );
1448    }
1449
1450    #[test]
1451    fn denies_risk_exceeded() {
1452        let h = Harness::new();
1453        let mut draft = baseline_draft();
1454        draft.constraints.max_action_risk_bp = Some(1_000);
1455        let cred = h.issue(draft);
1456        let actor = cred.subject_did.clone();
1457        let mut action = baseline_action(actor);
1458        action.estimated_risk_bp = Some(5_000);
1459        let mut request = baseline_request(cred, ts(1_500_000));
1460        request.action = Some(action);
1461        let result = validate_avc(&request, &h.registry).unwrap();
1462        assert!(result.reason_codes.contains(&AvcReasonCode::RiskExceeded));
1463    }
1464
1465    #[test]
1466    fn risk_above_threshold_returns_human_approval_required() {
1467        let h = Harness::new();
1468        let mut draft = baseline_draft();
1469        draft.constraints.max_action_risk_bp = Some(10_000);
1470        draft.constraints.approval_threshold_bp = Some(5_000);
1471        let cred = h.issue(draft);
1472        let actor = cred.subject_did.clone();
1473        let mut action = baseline_action(actor);
1474        action.estimated_risk_bp = Some(7_500);
1475        let mut request = baseline_request(cred, ts(1_500_000));
1476        request.action = Some(action);
1477        let result = validate_avc(&request, &h.registry).unwrap();
1478        assert_eq!(result.decision, AvcDecision::HumanApprovalRequired);
1479        assert_eq!(
1480            result.reason_codes,
1481            vec![AvcReasonCode::HumanApprovalMissing]
1482        );
1483    }
1484
1485    #[test]
1486    fn risk_above_threshold_ignores_caller_approval_flag() {
1487        let h = Harness::new();
1488        let mut draft = baseline_draft();
1489        draft.constraints.max_action_risk_bp = Some(10_000);
1490        draft.constraints.approval_threshold_bp = Some(5_000);
1491        let cred = h.issue(draft);
1492        let actor = cred.subject_did.clone();
1493        let mut action = baseline_action(actor);
1494        action.estimated_risk_bp = Some(7_500);
1495        action.requires_human_approval = true;
1496        let mut request = baseline_request(cred, ts(1_500_000));
1497        request.action = Some(action);
1498        let result = validate_avc(&request, &h.registry).unwrap();
1499        assert_eq!(result.decision, AvcDecision::HumanApprovalRequired);
1500        assert_eq!(
1501            result.reason_codes,
1502            vec![AvcReasonCode::HumanApprovalMissing]
1503        );
1504    }
1505
1506    #[test]
1507    fn credential_human_approval_required_blocks_action_without_evidence() {
1508        let h = Harness::new();
1509        let mut draft = baseline_draft();
1510        draft.constraints.human_approval_required = true;
1511        let cred = h.issue(draft);
1512        let actor = cred.subject_did.clone();
1513        let action = baseline_action(actor);
1514        let mut request = baseline_request(cred, ts(1_500_000));
1515        request.action = Some(action);
1516        let result = validate_avc(&request, &h.registry).unwrap();
1517        assert_eq!(result.decision, AvcDecision::HumanApprovalRequired);
1518        assert_eq!(
1519            result.reason_codes,
1520            vec![AvcReasonCode::HumanApprovalMissing]
1521        );
1522    }
1523
1524    #[test]
1525    fn signed_human_approval_satisfies_credential_requirement() {
1526        let mut h = Harness::new();
1527        let approver_keypair = human_approver_keypair();
1528        let approver_did = did("human-approver");
1529        h.registry
1530            .put_human_approval_key(approver_did.clone(), approver_keypair.public);
1531        let mut draft = baseline_draft();
1532        draft.constraints.human_approval_required = true;
1533        let cred = h.issue(draft);
1534        let actor = cred.subject_did.clone();
1535        let mut action = baseline_action(actor);
1536        attach_signed_human_approval(
1537            &cred,
1538            &mut action,
1539            approver_did,
1540            ts(1_400_000),
1541            Some(ts(1_900_000)),
1542            &approver_keypair,
1543        );
1544        let mut request = baseline_request(cred, ts(1_500_000));
1545        request.action = Some(action);
1546        let result = validate_avc(&request, &h.registry).unwrap();
1547        assert_eq!(result.decision, AvcDecision::Allow);
1548        assert_eq!(result.reason_codes, vec![AvcReasonCode::Valid]);
1549    }
1550
1551    #[test]
1552    fn signed_human_approval_satisfies_risk_threshold() {
1553        let mut h = Harness::new();
1554        let approver_keypair = human_approver_keypair();
1555        let approver_did = did("human-approver");
1556        h.registry
1557            .put_human_approval_key(approver_did.clone(), approver_keypair.public);
1558        let mut draft = baseline_draft();
1559        draft.constraints.max_action_risk_bp = Some(10_000);
1560        draft.constraints.approval_threshold_bp = Some(5_000);
1561        let cred = h.issue(draft);
1562        let actor = cred.subject_did.clone();
1563        let mut action = baseline_action(actor);
1564        action.estimated_risk_bp = Some(7_500);
1565        attach_signed_human_approval(
1566            &cred,
1567            &mut action,
1568            approver_did,
1569            ts(1_400_000),
1570            None,
1571            &approver_keypair,
1572        );
1573        let mut request = baseline_request(cred, ts(1_500_000));
1574        request.action = Some(action);
1575        let result = validate_avc(&request, &h.registry).unwrap();
1576        assert_eq!(result.decision, AvcDecision::Allow);
1577        assert_eq!(result.reason_codes, vec![AvcReasonCode::Valid]);
1578    }
1579
1580    #[test]
1581    fn valid_optional_human_approval_evidence_allows_unrequired_action() {
1582        let mut h = Harness::new();
1583        let approver_keypair = human_approver_keypair();
1584        let approver_did = did("human-approver");
1585        h.registry
1586            .put_human_approval_key(approver_did.clone(), approver_keypair.public);
1587        let cred = h.issue(baseline_draft());
1588        let actor = cred.subject_did.clone();
1589        let mut action = baseline_action(actor);
1590        attach_signed_human_approval(
1591            &cred,
1592            &mut action,
1593            approver_did,
1594            ts(1_400_000),
1595            Some(ts(1_900_000)),
1596            &approver_keypair,
1597        );
1598        let mut request = baseline_request(cred, ts(1_500_000));
1599        request.action = Some(action);
1600        let result = validate_avc(&request, &h.registry).unwrap();
1601        assert_eq!(result.decision, AvcDecision::Allow);
1602        assert_eq!(result.reason_codes, vec![AvcReasonCode::Valid]);
1603    }
1604
1605    #[test]
1606    fn human_approval_from_untrusted_approver_is_invalid() {
1607        let h = Harness::new();
1608        let approver_keypair = human_approver_keypair();
1609        let approver_did = did("human-approver");
1610        let mut draft = baseline_draft();
1611        draft.constraints.human_approval_required = true;
1612        let cred = h.issue(draft);
1613        let actor = cred.subject_did.clone();
1614        let mut action = baseline_action(actor);
1615        attach_signed_human_approval(
1616            &cred,
1617            &mut action,
1618            approver_did,
1619            ts(1_400_000),
1620            Some(ts(1_900_000)),
1621            &approver_keypair,
1622        );
1623        let mut request = baseline_request(cred, ts(1_500_000));
1624        request.action = Some(action);
1625        let result = validate_avc(&request, &h.registry).unwrap();
1626        assert_eq!(result.decision, AvcDecision::Deny);
1627        assert_eq!(
1628            result.reason_codes,
1629            vec![AvcReasonCode::HumanApprovalInvalid]
1630        );
1631    }
1632
1633    #[test]
1634    fn issuer_public_key_alone_does_not_authorize_human_approval() {
1635        let h = Harness::new();
1636        let issuer_keypair = issuer_keypair();
1637        let mut draft = baseline_draft();
1638        draft.constraints.human_approval_required = true;
1639        let cred = h.issue(draft);
1640        let actor = cred.subject_did.clone();
1641        let mut action = baseline_action(actor);
1642        attach_signed_human_approval(
1643            &cred,
1644            &mut action,
1645            did("issuer"),
1646            ts(1_400_000),
1647            Some(ts(1_900_000)),
1648            &issuer_keypair,
1649        );
1650        let mut request = baseline_request(cred, ts(1_500_000));
1651        request.action = Some(action);
1652        let result = validate_avc(&request, &h.registry).unwrap();
1653        assert_eq!(result.decision, AvcDecision::Deny);
1654        assert_eq!(
1655            result.reason_codes,
1656            vec![AvcReasonCode::HumanApprovalInvalid]
1657        );
1658    }
1659
1660    #[test]
1661    fn optional_human_approval_evidence_must_still_verify() {
1662        let h = Harness::new();
1663        let approver_keypair = human_approver_keypair();
1664        let cred = h.issue(baseline_draft());
1665        let actor = cred.subject_did.clone();
1666        let mut action = baseline_action(actor);
1667        attach_signed_human_approval(
1668            &cred,
1669            &mut action,
1670            did("human-approver"),
1671            ts(1_400_000),
1672            Some(ts(1_900_000)),
1673            &approver_keypair,
1674        );
1675        let mut request = baseline_request(cred, ts(1_500_000));
1676        request.action = Some(action);
1677        let result = validate_avc(&request, &h.registry).unwrap();
1678        assert_eq!(result.decision, AvcDecision::Deny);
1679        assert_eq!(
1680            result.reason_codes,
1681            vec![AvcReasonCode::HumanApprovalInvalid]
1682        );
1683    }
1684
1685    #[test]
1686    fn human_approval_signature_binds_action_fields() {
1687        let mut h = Harness::new();
1688        let approver_keypair = human_approver_keypair();
1689        let approver_did = did("human-approver");
1690        h.registry
1691            .put_human_approval_key(approver_did.clone(), approver_keypair.public);
1692        let mut draft = baseline_draft();
1693        draft.constraints.max_action_risk_bp = Some(10_000);
1694        draft.constraints.approval_threshold_bp = Some(5_000);
1695        let cred = h.issue(draft);
1696        let actor = cred.subject_did.clone();
1697        let mut action = baseline_action(actor);
1698        action.estimated_risk_bp = Some(7_500);
1699        attach_signed_human_approval(
1700            &cred,
1701            &mut action,
1702            approver_did,
1703            ts(1_400_000),
1704            None,
1705            &approver_keypair,
1706        );
1707        action.estimated_risk_bp = Some(7_501);
1708        let mut request = baseline_request(cred, ts(1_500_000));
1709        request.action = Some(action);
1710        let result = validate_avc(&request, &h.registry).unwrap();
1711        assert_eq!(result.decision, AvcDecision::Deny);
1712        assert_eq!(
1713            result.reason_codes,
1714            vec![AvcReasonCode::HumanApprovalInvalid]
1715        );
1716    }
1717
1718    #[test]
1719    fn expired_human_approval_is_rejected() {
1720        let mut h = Harness::new();
1721        let approver_keypair = human_approver_keypair();
1722        let approver_did = did("human-approver");
1723        h.registry
1724            .put_human_approval_key(approver_did.clone(), approver_keypair.public);
1725        let mut draft = baseline_draft();
1726        draft.constraints.human_approval_required = true;
1727        let cred = h.issue(draft);
1728        let actor = cred.subject_did.clone();
1729        let mut action = baseline_action(actor);
1730        attach_signed_human_approval(
1731            &cred,
1732            &mut action,
1733            approver_did,
1734            ts(1_300_000),
1735            Some(ts(1_400_000)),
1736            &approver_keypair,
1737        );
1738        let mut request = baseline_request(cred, ts(1_500_000));
1739        request.action = Some(action);
1740        let result = validate_avc(&request, &h.registry).unwrap();
1741        assert_eq!(result.decision, AvcDecision::Deny);
1742        assert_eq!(
1743            result.reason_codes,
1744            vec![AvcReasonCode::HumanApprovalExpired]
1745        );
1746    }
1747
1748    #[test]
1749    fn human_approval_with_empty_signature_is_invalid() {
1750        let mut h = Harness::new();
1751        let approver_keypair = human_approver_keypair();
1752        let approver_did = did("human-approver");
1753        h.registry
1754            .put_human_approval_key(approver_did.clone(), approver_keypair.public);
1755        let mut draft = baseline_draft();
1756        draft.constraints.human_approval_required = true;
1757        let cred = h.issue(draft);
1758        let actor = cred.subject_did.clone();
1759        let mut action = baseline_action(actor);
1760        action.human_approval = Some(AvcHumanApproval {
1761            approver_did,
1762            approved_at: ts(1_400_000),
1763            expires_at: Some(ts(1_900_000)),
1764            signature: Signature::empty(),
1765        });
1766        let mut request = baseline_request(cred, ts(1_500_000));
1767        request.action = Some(action);
1768        let result = validate_avc(&request, &h.registry).unwrap();
1769        assert_eq!(result.decision, AvcDecision::Deny);
1770        assert_eq!(
1771            result.reason_codes,
1772            vec![AvcReasonCode::HumanApprovalInvalid]
1773        );
1774    }
1775
1776    #[test]
1777    fn human_approval_expiring_at_approval_time_is_invalid() {
1778        let mut h = Harness::new();
1779        let approver_keypair = human_approver_keypair();
1780        let approver_did = did("human-approver");
1781        h.registry
1782            .put_human_approval_key(approver_did.clone(), approver_keypair.public);
1783        let mut draft = baseline_draft();
1784        draft.constraints.human_approval_required = true;
1785        let cred = h.issue(draft);
1786        let actor = cred.subject_did.clone();
1787        let mut action = baseline_action(actor);
1788        attach_signed_human_approval(
1789            &cred,
1790            &mut action,
1791            approver_did,
1792            ts(1_400_000),
1793            Some(ts(1_400_000)),
1794            &approver_keypair,
1795        );
1796        let mut request = baseline_request(cred, ts(1_500_000));
1797        request.action = Some(action);
1798        let result = validate_avc(&request, &h.registry).unwrap();
1799        assert_eq!(result.decision, AvcDecision::Deny);
1800        assert_eq!(
1801            result.reason_codes,
1802            vec![AvcReasonCode::HumanApprovalInvalid]
1803        );
1804    }
1805
1806    #[test]
1807    fn human_approval_expiring_at_now_is_expired() {
1808        let mut h = Harness::new();
1809        let approver_keypair = human_approver_keypair();
1810        let approver_did = did("human-approver");
1811        h.registry
1812            .put_human_approval_key(approver_did.clone(), approver_keypair.public);
1813        let mut draft = baseline_draft();
1814        draft.constraints.human_approval_required = true;
1815        let cred = h.issue(draft);
1816        let actor = cred.subject_did.clone();
1817        let mut action = baseline_action(actor);
1818        attach_signed_human_approval(
1819            &cred,
1820            &mut action,
1821            approver_did,
1822            ts(1_400_000),
1823            Some(ts(1_500_000)),
1824            &approver_keypair,
1825        );
1826        let mut request = baseline_request(cred, ts(1_500_000));
1827        request.action = Some(action);
1828        let result = validate_avc(&request, &h.registry).unwrap();
1829        assert_eq!(result.decision, AvcDecision::Deny);
1830        assert_eq!(
1831            result.reason_codes,
1832            vec![AvcReasonCode::HumanApprovalExpired]
1833        );
1834    }
1835
1836    #[test]
1837    fn human_approval_with_future_approval_time_is_invalid() {
1838        let mut h = Harness::new();
1839        let approver_keypair = human_approver_keypair();
1840        let approver_did = did("human-approver");
1841        h.registry
1842            .put_human_approval_key(approver_did.clone(), approver_keypair.public);
1843        let mut draft = baseline_draft();
1844        draft.constraints.human_approval_required = true;
1845        let cred = h.issue(draft);
1846        let actor = cred.subject_did.clone();
1847        let mut action = baseline_action(actor);
1848        attach_signed_human_approval(
1849            &cred,
1850            &mut action,
1851            approver_did,
1852            ts(1_600_000),
1853            Some(ts(1_900_000)),
1854            &approver_keypair,
1855        );
1856        let mut request = baseline_request(cred, ts(1_500_000));
1857        request.action = Some(action);
1858        let result = validate_avc(&request, &h.registry).unwrap();
1859        assert_eq!(result.decision, AvcDecision::Deny);
1860        assert_eq!(
1861            result.reason_codes,
1862            vec![AvcReasonCode::HumanApprovalInvalid]
1863        );
1864    }
1865
1866    #[test]
1867    fn human_approval_expiring_before_approval_time_is_invalid() {
1868        let mut h = Harness::new();
1869        let approver_keypair = human_approver_keypair();
1870        let approver_did = did("human-approver");
1871        h.registry
1872            .put_human_approval_key(approver_did.clone(), approver_keypair.public);
1873        let mut draft = baseline_draft();
1874        draft.constraints.human_approval_required = true;
1875        let cred = h.issue(draft);
1876        let actor = cred.subject_did.clone();
1877        let mut action = baseline_action(actor);
1878        attach_signed_human_approval(
1879            &cred,
1880            &mut action,
1881            approver_did,
1882            ts(1_400_000),
1883            Some(ts(1_399_999)),
1884            &approver_keypair,
1885        );
1886        let mut request = baseline_request(cred, ts(1_500_000));
1887        request.action = Some(action);
1888        let result = validate_avc(&request, &h.registry).unwrap();
1889        assert_eq!(result.decision, AvcDecision::Deny);
1890        assert_eq!(
1891            result.reason_codes,
1892            vec![AvcReasonCode::HumanApprovalInvalid]
1893        );
1894    }
1895
1896    #[test]
1897    fn risk_below_approval_threshold_allows_without_human_approval() {
1898        let h = Harness::new();
1899        let mut draft = baseline_draft();
1900        draft.constraints.max_action_risk_bp = Some(10_000);
1901        draft.constraints.approval_threshold_bp = Some(5_000);
1902        let cred = h.issue(draft);
1903        let actor = cred.subject_did.clone();
1904        let mut action = baseline_action(actor);
1905        action.estimated_risk_bp = Some(4_999);
1906        let mut request = baseline_request(cred, ts(1_500_000));
1907        request.action = Some(action);
1908        let result = validate_avc(&request, &h.registry).unwrap();
1909        assert_eq!(result.decision, AvcDecision::Allow);
1910        assert_eq!(result.reason_codes, vec![AvcReasonCode::Valid]);
1911    }
1912
1913    #[test]
1914    fn risk_threshold_without_estimate_allows_without_human_approval() {
1915        let h = Harness::new();
1916        let mut draft = baseline_draft();
1917        draft.constraints.max_action_risk_bp = Some(10_000);
1918        draft.constraints.approval_threshold_bp = Some(5_000);
1919        let cred = h.issue(draft);
1920        let actor = cred.subject_did.clone();
1921        let action = baseline_action(actor);
1922        let mut request = baseline_request(cred, ts(1_500_000));
1923        request.action = Some(action);
1924        let result = validate_avc(&request, &h.registry).unwrap();
1925        assert_eq!(result.decision, AvcDecision::Allow);
1926        assert_eq!(result.reason_codes, vec![AvcReasonCode::Valid]);
1927    }
1928
1929    #[test]
1930    fn denies_forbidden_action_name() {
1931        let h = Harness::new();
1932        let mut draft = baseline_draft();
1933        draft.constraints.forbidden_actions = vec!["payment.execute".into()];
1934        let cred = h.issue(draft);
1935        let actor = cred.subject_did.clone();
1936        let mut action = baseline_action(actor);
1937        action.action_name = Some("payment.execute".into());
1938        let mut request = baseline_request(cred, ts(1_500_000));
1939        request.action = Some(action);
1940        let result = validate_avc(&request, &h.registry).unwrap();
1941        assert!(
1942            result
1943                .reason_codes
1944                .contains(&AvcReasonCode::ForbiddenAction)
1945        );
1946    }
1947
1948    #[test]
1949    fn reason_codes_are_sorted_and_deduped() {
1950        let h = Harness::new();
1951        // Construct a credential that fails several checks at once.
1952        let mut draft = baseline_draft();
1953        draft.principal_did = did("principal"); // forces authority chain
1954        // Keep tool empty; action will request a tool.
1955        draft.authority_scope.tools = vec![];
1956        let cred = h.issue(draft);
1957        let actor = cred.subject_did.clone();
1958        let mut action = baseline_action(actor);
1959        action.tool = Some("forbidden".into());
1960        action.requested_permission = Permission::Govern; // not in scope
1961        let mut request = baseline_request(cred, ts(3_000_000)); // also expired
1962        request.action = Some(action);
1963        let result = validate_avc(&request, &h.registry).unwrap();
1964        assert_eq!(result.decision, AvcDecision::Deny);
1965
1966        let mut sorted = result.reason_codes.clone();
1967        sorted.sort();
1968        assert_eq!(sorted, result.reason_codes, "reason codes must be sorted");
1969
1970        let mut deduped = result.reason_codes.clone();
1971        deduped.dedup();
1972        assert_eq!(deduped, result.reason_codes, "reason codes must be deduped");
1973    }
1974
1975    #[test]
1976    fn validation_does_not_consult_payment_state() {
1977        // No quote/settlement registry exists; validation should still succeed.
1978        let h = Harness::new();
1979        let cred = h.issue(baseline_draft());
1980        let r1 = validate_avc(&baseline_request(cred.clone(), ts(1_500_000)), &h.registry).unwrap();
1981        let r2 = validate_avc(&baseline_request(cred, ts(1_500_000)), &h.registry).unwrap();
1982        assert_eq!(r1, r2);
1983    }
1984
1985    #[test]
1986    fn validation_request_round_trip_serializes() {
1987        let h = Harness::new();
1988        let cred = h.issue(baseline_draft());
1989        let request = baseline_request(cred, ts(1_500_000));
1990        let mut buf = Vec::new();
1991        ciborium::ser::into_writer(&request, &mut buf).unwrap();
1992        let decoded: AvcValidationRequest = ciborium::de::from_reader(buf.as_slice()).unwrap();
1993        assert_eq!(decoded, request);
1994    }
1995
1996    #[test]
1997    fn unsupported_subject_with_unknown_kind_still_allows() {
1998        let h = Harness::new();
1999        let mut draft = baseline_draft();
2000        draft.subject_kind = AvcSubjectKind::Unknown;
2001        let cred = h.issue(draft);
2002        let result = validate_avc(&baseline_request(cred, ts(1_500_000)), &h.registry).unwrap();
2003        assert_eq!(result.decision, AvcDecision::Allow);
2004    }
2005
2006    #[test]
2007    fn validation_request_now_inside_window_is_inclusive() {
2008        let h = Harness::new();
2009        let mut draft = baseline_draft();
2010        draft.constraints.allowed_time_window = Some(TimeWindow {
2011            not_before: ts(1_500_000),
2012            not_after: ts(1_500_000_000),
2013        });
2014        let cred = h.issue(draft);
2015        let result = validate_avc(&baseline_request(cred, ts(1_500_000)), &h.registry).unwrap();
2016        assert_eq!(result.decision, AvcDecision::Allow);
2017    }
2018
2019    #[test]
2020    fn confirms_schema_constant_is_one() {
2021        assert_eq!(AVC_SCHEMA_VERSION, 1);
2022    }
2023
2024    #[test]
2025    fn validation_with_only_constraints_passes_when_no_action() {
2026        let h = Harness::new();
2027        let mut draft = baseline_draft();
2028        draft.constraints = AvcConstraints {
2029            max_budget_minor_units: Some(1_000),
2030            currency_code: Some("USD".into()),
2031            max_action_risk_bp: Some(2_000),
2032            human_approval_required: false,
2033            approval_threshold_bp: Some(5_000),
2034            max_delegation_depth: 1,
2035            allowed_time_window: None,
2036            forbidden_actions: vec!["bad".into()],
2037            emergency_stop_refs: vec!["stop".into()],
2038        };
2039        let cred = h.issue(draft);
2040        let result = validate_avc(&baseline_request(cred, ts(1_500_000)), &h.registry).unwrap();
2041        assert_eq!(result.decision, AvcDecision::Allow);
2042    }
2043}