Skip to main content

exo_avc/
lib.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//! # exo-avc — Autonomous Volition Credential
18//!
19//! `AVC` is a portable, signed, machine-verifiable credential that
20//! defines what an autonomous actor is **authorized to pursue** under a
21//! human or organizational principal.
22//!
23//! Identity proves *who* an actor is. Authority proves *who delegated*
24//! power. Consent proves *what posture* applies. AVC proves *what
25//! autonomous intent is allowed* before action occurs.
26//!
27//! In this crate, **volition** strictly means delegated operational
28//! intent. It does **not** denote consciousness, sentience, emotion, or
29//! human-like free will.
30//!
31//! ## Determinism contract
32//!
33//! - All collections in signed payloads are sorted and deduplicated.
34//! - All hashing is BLAKE3 over canonical CBOR — only ordered maps and
35//!   sets (`BTreeMap`, `BTreeSet`), no platform-dependent integer widths,
36//!   and no floating-point arithmetic.
37//! - Validation never reads system time; the caller passes `now`.
38//! - Validation is fail-closed: any unresolved key, missing required
39//!   reference, malformed structural value, scope violation, expiration,
40//!   or revocation produces an explicit `Deny` decision with reason
41//!   codes describing why.
42//!
43//! ## High-level API
44//!
45//! ```
46//! use exo_avc::{
47//!     AutonomyLevel, AuthorityScope, AvcConstraints, AvcDraft, AvcSubjectKind,
48//!     DelegatedIntent, InMemoryAvcRegistry, AvcRegistryWrite, AvcValidationRequest,
49//!     AvcDecision, issue_avc, validate_avc, AVC_SCHEMA_VERSION,
50//! };
51//! use exo_authority::permission::Permission;
52//! use exo_core::{Did, Hash256, Timestamp};
53//! use exo_core::crypto::KeyPair;
54//!
55//! let issuer_keypair = KeyPair::from_secret_bytes([0x11; 32]).unwrap();
56//! let issuer_did = Did::new("did:exo:issuer").unwrap();
57//! let mut registry = InMemoryAvcRegistry::new();
58//! registry.put_public_key(issuer_did.clone(), issuer_keypair.public);
59//!
60//! let draft = AvcDraft {
61//!     schema_version: AVC_SCHEMA_VERSION,
62//!     issuer_did: issuer_did.clone(),
63//!     principal_did: issuer_did.clone(),
64//!     subject_did: Did::new("did:exo:agent").unwrap(),
65//!     holder_did: None,
66//!     subject_kind: AvcSubjectKind::AiAgent {
67//!         model_id: "alpha".into(),
68//!         agent_version: None,
69//!     },
70//!     created_at: Timestamp::new(1_000, 0),
71//!     expires_at: Some(Timestamp::new(2_000, 0)),
72//!     delegated_intent: DelegatedIntent {
73//!         intent_id: Hash256::from_bytes([0xAA; 32]),
74//!         purpose: "research".into(),
75//!         allowed_objectives: vec!["primary".into()],
76//!         prohibited_objectives: vec![],
77//!         autonomy_level: AutonomyLevel::Draft,
78//!         delegation_allowed: false,
79//!     },
80//!     authority_scope: AuthorityScope {
81//!         permissions: vec![Permission::Read],
82//!         tools: vec![],
83//!         data_classes: vec![],
84//!         counterparties: vec![],
85//!         jurisdictions: vec!["US".into()],
86//!     },
87//!     constraints: AvcConstraints::permissive(),
88//!     authority_chain: None,
89//!     consent_refs: vec![],
90//!     policy_refs: vec![],
91//!     parent_avc_id: None,
92//! };
93//!
94//! let credential = issue_avc(draft, |bytes| issuer_keypair.sign(bytes)).unwrap();
95//! let request = AvcValidationRequest {
96//!     credential,
97//!     action: None,
98//!     now: Timestamp::new(1_500, 0),
99//! };
100//! let result = validate_avc(&request, &registry).unwrap();
101//! assert_eq!(result.decision, AvcDecision::Allow);
102//! ```
103
104#![cfg_attr(test, allow(clippy::expect_used, clippy::unwrap_used))]
105
106pub mod credential;
107pub mod delegation;
108pub mod error;
109pub mod receipt;
110pub mod registry;
111pub mod revocation;
112pub mod validation;
113
114pub use credential::{
115    AVC_CREDENTIAL_SIGNING_DOMAIN, AVC_MAX_SUPPORTED_PROTOCOL_VERSION,
116    AVC_MIN_SUPPORTED_PROTOCOL_VERSION, AVC_PROTOCOL_DEPRECATION_WINDOW_DAYS, AVC_PROTOCOL_VERSION,
117    AVC_SCHEMA_VERSION, AuthorityChainRef, AuthorityScope, AutonomousVolitionCredential,
118    AutonomyLevel, AvcConstraints, AvcDraft, AvcSubjectKind, ConsentRef, DataClass,
119    DelegatedIntent, MAX_BASIS_POINTS, PolicyRef, TimeWindow, issue_avc,
120    require_supported_avc_protocol_version,
121};
122pub use delegation::{delegate_avc, parent_id_of};
123pub use error::AvcError;
124pub use receipt::{
125    AVC_RECEIPT_EVIDENCE_SUBJECT_DOMAIN, AVC_RECEIPT_EXTERNAL_TIMESTAMP_DOMAIN,
126    AVC_RECEIPT_SIGNING_DOMAIN, AvcReceiptEvidenceSubject, AvcReceiptExternalTimestampProof,
127    AvcReceiptExternalTimestampProofKind, AvcReceiptRfc3161TimestampProof,
128    AvcReceiptRfc3161TrustAnchorKind, AvcReceiptTimestampProvenance, AvcTrustReceipt,
129    AvcTrustReceiptEvidence, create_trust_receipt, create_trust_receipt_with_evidence,
130};
131pub use registry::{
132    AvcRegistryDurableState, AvcRegistryRead, AvcRegistryWrite, InMemoryAvcRegistry,
133};
134pub use revocation::{
135    AVC_REVOCATION_SIGNING_DOMAIN, AvcRevocation, AvcRevocationReason, revoke_avc,
136};
137pub use validation::{
138    AVC_ACTION_COMMITMENT_DOMAIN, AVC_ACTION_DESCRIPTOR_DOMAIN, AVC_ACTION_SIGNING_DOMAIN,
139    AVC_HUMAN_APPROVAL_SIGNING_DOMAIN, AvcActionDescriptor, AvcActionRequest, AvcDecision,
140    AvcHumanApproval, AvcReasonCode, AvcValidationRequest, AvcValidationResult,
141    avc_action_commitment_hash, avc_action_descriptor_hash, avc_action_signature_payload,
142    human_approval_signature_payload, validate_avc,
143};
144
145/// All AVC signing domains as a sorted slice — used by hygiene tests
146/// and external auditors who need to ensure no domain collisions.
147pub const AVC_SIGNING_DOMAINS: &[&str] = &[
148    AVC_ACTION_COMMITMENT_DOMAIN,
149    AVC_ACTION_DESCRIPTOR_DOMAIN,
150    AVC_ACTION_SIGNING_DOMAIN,
151    AVC_CREDENTIAL_SIGNING_DOMAIN,
152    AVC_HUMAN_APPROVAL_SIGNING_DOMAIN,
153    AVC_RECEIPT_EVIDENCE_SUBJECT_DOMAIN,
154    AVC_RECEIPT_EXTERNAL_TIMESTAMP_DOMAIN,
155    AVC_RECEIPT_SIGNING_DOMAIN,
156    AVC_REVOCATION_SIGNING_DOMAIN,
157];
158
159#[cfg(test)]
160mod hygiene_tests {
161    use super::*;
162
163    #[test]
164    fn signing_domains_are_distinct() {
165        let mut sorted = AVC_SIGNING_DOMAINS.to_vec();
166        sorted.sort_unstable();
167        let original_len = sorted.len();
168        sorted.dedup();
169        assert_eq!(sorted.len(), original_len, "signing domains must be unique");
170    }
171
172    #[test]
173    fn signing_domains_are_versioned() {
174        for d in AVC_SIGNING_DOMAINS {
175            assert!(d.contains(".v1"), "domain {d} must be version-tagged");
176        }
177    }
178
179    #[test]
180    fn no_hashmap_or_hashset_in_production_sources() {
181        let sources = [
182            include_str!("credential.rs"),
183            include_str!("delegation.rs"),
184            include_str!("error.rs"),
185            include_str!("lib.rs"),
186            include_str!("receipt.rs"),
187            include_str!("registry.rs"),
188            include_str!("revocation.rs"),
189            include_str!("validation.rs"),
190        ];
191        let banned_map = ["Hash", "Map"].concat();
192        let banned_set = ["Hash", "Set"].concat();
193        for src in sources {
194            // Strip everything from `#[cfg(test)]` onward — tests may
195            // reference banned tokens in identifiers.
196            let production = src.split("#[cfg(test)]").next().unwrap();
197            assert!(
198                !production.contains(&banned_map),
199                "AVC production sources must not use HashMap"
200            );
201            assert!(
202                !production.contains(&banned_set),
203                "AVC production sources must not use HashSet"
204            );
205        }
206    }
207
208    #[test]
209    fn no_floating_point_in_production_sources() {
210        let sources = [
211            include_str!("credential.rs"),
212            include_str!("delegation.rs"),
213            include_str!("error.rs"),
214            include_str!("lib.rs"),
215            include_str!("receipt.rs"),
216            include_str!("registry.rs"),
217            include_str!("revocation.rs"),
218            include_str!("validation.rs"),
219        ];
220        for src in sources {
221            let production = src.split("#[cfg(test)]").next().unwrap();
222            for token in [": f32", ": f64", "as f32", "as f64", "f32::", "f64::"] {
223                assert!(
224                    !production.contains(token),
225                    "AVC production sources must not contain `{token}`"
226                );
227            }
228        }
229    }
230}