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, ®istry).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}