1use std::collections::BTreeSet;
24
25use exo_authority::permission::Permission;
26use exo_core::{Did, Hash256, Signature, Timestamp, hash::hash_structured};
27use serde::{Deserialize, Serialize};
28
29use crate::error::AvcError;
30
31pub const AVC_CREDENTIAL_SIGNING_DOMAIN: &str = "exo.avc.credential.v1";
33pub const AVC_SCHEMA_VERSION: u16 = 1;
35pub const AVC_PROTOCOL_VERSION: u16 = 1;
37pub const AVC_MIN_SUPPORTED_PROTOCOL_VERSION: u16 = 1;
39pub const AVC_MAX_SUPPORTED_PROTOCOL_VERSION: u16 = AVC_PROTOCOL_VERSION;
41pub const AVC_PROTOCOL_DEPRECATION_WINDOW_DAYS: u16 = 180;
43pub const MAX_BASIS_POINTS: u32 = 10_000;
45
46pub fn require_supported_avc_protocol_version(
52 requested_protocol_version: Option<u16>,
53) -> Result<u16, AvcError> {
54 let got = requested_protocol_version.unwrap_or(AVC_PROTOCOL_VERSION);
55 if !(AVC_MIN_SUPPORTED_PROTOCOL_VERSION..=AVC_MAX_SUPPORTED_PROTOCOL_VERSION).contains(&got) {
56 return Err(AvcError::UnsupportedProtocol {
57 got,
58 min_supported: AVC_MIN_SUPPORTED_PROTOCOL_VERSION,
59 max_supported: AVC_MAX_SUPPORTED_PROTOCOL_VERSION,
60 });
61 }
62 Ok(got)
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
74pub enum AvcSubjectKind {
75 AiAgent {
77 model_id: String,
78 agent_version: Option<String>,
79 },
80 AgentSwarm { swarm_id: String },
82 Workflow { workflow_id: String },
84 Service { service_id: String },
86 Holon { holon_id: String },
88 OrganizationUnit { unit_id: String },
90 Unknown,
92}
93
94impl AvcSubjectKind {
95 fn validate(&self) -> Result<(), AvcError> {
96 match self {
97 Self::AiAgent { model_id, .. } => non_empty(model_id, "subject_kind.model_id"),
98 Self::AgentSwarm { swarm_id } => non_empty(swarm_id, "subject_kind.swarm_id"),
99 Self::Workflow { workflow_id } => non_empty(workflow_id, "subject_kind.workflow_id"),
100 Self::Service { service_id } => non_empty(service_id, "subject_kind.service_id"),
101 Self::Holon { holon_id } => non_empty(holon_id, "subject_kind.holon_id"),
102 Self::OrganizationUnit { unit_id } => non_empty(unit_id, "subject_kind.unit_id"),
103 Self::Unknown => Ok(()),
104 }
105 }
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
116#[repr(u8)]
117pub enum AutonomyLevel {
118 ObserveOnly = 0,
119 Recommend = 1,
120 Draft = 2,
121 ExecuteWithHumanApproval = 3,
122 ExecuteWithinBounds = 4,
123 DelegateWithinBounds = 5,
124}
125
126#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
132pub struct DelegatedIntent {
133 pub intent_id: Hash256,
135 pub purpose: String,
137 pub allowed_objectives: Vec<String>,
139 pub prohibited_objectives: Vec<String>,
141 pub autonomy_level: AutonomyLevel,
143 pub delegation_allowed: bool,
145}
146
147impl DelegatedIntent {
148 fn validate(&self) -> Result<(), AvcError> {
149 non_empty(&self.purpose, "delegated_intent.purpose")?;
150 for obj in &self.allowed_objectives {
151 non_empty(obj, "delegated_intent.allowed_objectives")?;
152 }
153 for obj in &self.prohibited_objectives {
154 non_empty(obj, "delegated_intent.prohibited_objectives")?;
155 }
156 Ok(())
157 }
158
159 fn normalize(&mut self) {
160 self.allowed_objectives = sort_dedup(self.allowed_objectives.drain(..));
161 self.prohibited_objectives = sort_dedup(self.prohibited_objectives.drain(..));
162 }
163}
164
165#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
170pub enum DataClass {
171 Public,
172 Internal,
173 Confidential,
174 Restricted,
175 PersonalData,
176 SensitivePersonalData,
177 Financial,
178 LegalPrivileged,
179 Custom(String),
180}
181
182impl DataClass {
183 fn validate(&self) -> Result<(), AvcError> {
184 if let Self::Custom(name) = self {
185 non_empty(name, "data_class.custom")?;
186 }
187 Ok(())
188 }
189}
190
191#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
201pub struct AuthorityScope {
202 pub permissions: Vec<Permission>,
203 pub tools: Vec<String>,
204 pub data_classes: Vec<DataClass>,
205 pub counterparties: Vec<Did>,
206 pub jurisdictions: Vec<String>,
207}
208
209impl AuthorityScope {
210 #[must_use]
212 pub fn empty() -> Self {
213 Self {
214 permissions: Vec::new(),
215 tools: Vec::new(),
216 data_classes: Vec::new(),
217 counterparties: Vec::new(),
218 jurisdictions: Vec::new(),
219 }
220 }
221
222 fn validate(&self) -> Result<(), AvcError> {
223 for tool in &self.tools {
224 non_empty(tool, "authority_scope.tools")?;
225 }
226 for class in &self.data_classes {
227 class.validate()?;
228 }
229 for jurisdiction in &self.jurisdictions {
230 non_empty(jurisdiction, "authority_scope.jurisdictions")?;
231 }
232 Ok(())
233 }
234
235 fn normalize(&mut self) {
236 self.permissions = sort_dedup_copy(self.permissions.iter().copied());
237 self.tools = sort_dedup(self.tools.drain(..));
238 self.data_classes = sort_dedup(self.data_classes.drain(..));
239 let mut cp: Vec<Did> = self.counterparties.drain(..).collect();
240 cp.sort();
241 cp.dedup();
242 self.counterparties = cp;
243 self.jurisdictions = sort_dedup(self.jurisdictions.drain(..));
244 }
245}
246
247#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
252pub struct TimeWindow {
253 pub not_before: Timestamp,
254 pub not_after: Timestamp,
255}
256
257impl TimeWindow {
258 fn validate(&self) -> Result<(), AvcError> {
259 if self.not_after <= self.not_before {
260 return Err(AvcError::InvalidTimestamp {
261 reason: "time_window.not_after must be strictly after not_before".into(),
262 });
263 }
264 Ok(())
265 }
266
267 #[must_use]
269 pub fn contains(&self, now: &Timestamp) -> bool {
270 now >= &self.not_before && now <= &self.not_after
271 }
272}
273
274#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
279pub struct AvcConstraints {
280 pub max_budget_minor_units: Option<u64>,
281 pub currency_code: Option<String>,
282 pub max_action_risk_bp: Option<u32>,
283 pub human_approval_required: bool,
284 pub approval_threshold_bp: Option<u32>,
285 pub max_delegation_depth: u32,
286 pub allowed_time_window: Option<TimeWindow>,
287 pub forbidden_actions: Vec<String>,
288 pub emergency_stop_refs: Vec<String>,
289}
290
291impl AvcConstraints {
292 #[must_use]
294 pub fn permissive() -> Self {
295 Self {
296 max_budget_minor_units: None,
297 currency_code: None,
298 max_action_risk_bp: None,
299 human_approval_required: false,
300 approval_threshold_bp: None,
301 max_delegation_depth: 0,
302 allowed_time_window: None,
303 forbidden_actions: Vec::new(),
304 emergency_stop_refs: Vec::new(),
305 }
306 }
307
308 fn validate(&self) -> Result<(), AvcError> {
309 if let Some(value) = self.max_action_risk_bp {
310 require_bp("max_action_risk_bp", value)?;
311 }
312 if let Some(value) = self.approval_threshold_bp {
313 require_bp("approval_threshold_bp", value)?;
314 }
315 if let Some(window) = &self.allowed_time_window {
316 window.validate()?;
317 }
318 if let Some(currency) = &self.currency_code {
319 non_empty(currency, "constraints.currency_code")?;
320 }
321 for action in &self.forbidden_actions {
322 non_empty(action, "constraints.forbidden_actions")?;
323 }
324 for stop in &self.emergency_stop_refs {
325 non_empty(stop, "constraints.emergency_stop_refs")?;
326 }
327 Ok(())
328 }
329
330 fn normalize(&mut self) {
331 self.forbidden_actions = sort_dedup(self.forbidden_actions.drain(..));
332 self.emergency_stop_refs = sort_dedup(self.emergency_stop_refs.drain(..));
333 }
334}
335
336#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
341pub struct ConsentRef {
342 pub consent_id: Hash256,
343 pub required: bool,
344}
345
346#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
347pub struct PolicyRef {
348 pub policy_id: Hash256,
349 pub policy_version: u16,
350 pub required: bool,
351}
352
353#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
363pub struct AuthorityChainRef {
364 pub chain_hash: Hash256,
365}
366
367#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
373pub struct AutonomousVolitionCredential {
374 pub schema_version: u16,
375 pub issuer_did: Did,
376 pub principal_did: Did,
377 pub subject_did: Did,
378 pub holder_did: Option<Did>,
379 pub subject_kind: AvcSubjectKind,
380 pub created_at: Timestamp,
381 pub expires_at: Option<Timestamp>,
382 pub delegated_intent: DelegatedIntent,
383 pub authority_scope: AuthorityScope,
384 pub constraints: AvcConstraints,
385 pub authority_chain: Option<AuthorityChainRef>,
386 pub consent_refs: Vec<ConsentRef>,
387 pub policy_refs: Vec<PolicyRef>,
388 pub parent_avc_id: Option<Hash256>,
389 pub signature: Signature,
390}
391
392#[derive(Debug, Clone, PartialEq, Eq)]
394pub struct AvcDraft {
395 pub schema_version: u16,
396 pub issuer_did: Did,
397 pub principal_did: Did,
398 pub subject_did: Did,
399 pub holder_did: Option<Did>,
400 pub subject_kind: AvcSubjectKind,
401 pub created_at: Timestamp,
402 pub expires_at: Option<Timestamp>,
403 pub delegated_intent: DelegatedIntent,
404 pub authority_scope: AuthorityScope,
405 pub constraints: AvcConstraints,
406 pub authority_chain: Option<AuthorityChainRef>,
407 pub consent_refs: Vec<ConsentRef>,
408 pub policy_refs: Vec<PolicyRef>,
409 pub parent_avc_id: Option<Hash256>,
410}
411
412impl AvcDraft {
413 pub fn normalize_and_validate(&mut self) -> Result<(), AvcError> {
420 if self.schema_version != AVC_SCHEMA_VERSION {
421 return Err(AvcError::UnsupportedSchema {
422 got: self.schema_version,
423 supported: AVC_SCHEMA_VERSION,
424 });
425 }
426 self.subject_kind.validate()?;
427 self.delegated_intent.validate()?;
428 self.delegated_intent.normalize();
429 self.authority_scope.validate()?;
430 self.authority_scope.normalize();
431 self.constraints.validate()?;
432 self.constraints.normalize();
433
434 if let Some(expires) = self.expires_at {
435 if expires <= self.created_at {
436 return Err(AvcError::InvalidTimestamp {
437 reason: "expires_at must be strictly after created_at".into(),
438 });
439 }
440 }
441
442 self.consent_refs.sort();
443 self.consent_refs.dedup();
444 self.policy_refs.sort();
445 self.policy_refs.dedup();
446
447 Ok(())
448 }
449}
450
451#[derive(Serialize)]
452struct AvcSigningPayload<'a> {
453 domain: &'static str,
454 schema_version: u16,
455 issuer_did: &'a Did,
456 principal_did: &'a Did,
457 subject_did: &'a Did,
458 holder_did: Option<&'a Did>,
459 subject_kind: &'a AvcSubjectKind,
460 created_at: &'a Timestamp,
461 expires_at: Option<&'a Timestamp>,
462 delegated_intent: &'a DelegatedIntent,
463 authority_scope: &'a AuthorityScope,
464 constraints: &'a AvcConstraints,
465 authority_chain: Option<&'a AuthorityChainRef>,
466 consent_refs: &'a [ConsentRef],
467 policy_refs: &'a [PolicyRef],
468 parent_avc_id: Option<&'a Hash256>,
469}
470
471impl AutonomousVolitionCredential {
472 pub fn signing_payload(&self) -> Result<Vec<u8>, AvcError> {
481 let payload = AvcSigningPayload {
482 domain: AVC_CREDENTIAL_SIGNING_DOMAIN,
483 schema_version: self.schema_version,
484 issuer_did: &self.issuer_did,
485 principal_did: &self.principal_did,
486 subject_did: &self.subject_did,
487 holder_did: self.holder_did.as_ref(),
488 subject_kind: &self.subject_kind,
489 created_at: &self.created_at,
490 expires_at: self.expires_at.as_ref(),
491 delegated_intent: &self.delegated_intent,
492 authority_scope: &self.authority_scope,
493 constraints: &self.constraints,
494 authority_chain: self.authority_chain.as_ref(),
495 consent_refs: &self.consent_refs,
496 policy_refs: &self.policy_refs,
497 parent_avc_id: self.parent_avc_id.as_ref(),
498 };
499 let mut buf = Vec::new();
500 ciborium::ser::into_writer(&payload, &mut buf)?;
501 Ok(buf)
502 }
503
504 pub fn id(&self) -> Result<Hash256, AvcError> {
512 Ok(Hash256::digest(&self.signing_payload()?))
513 }
514
515 pub fn content_hash(&self) -> Result<Hash256, AvcError> {
522 hash_structured(&AvcSigningPayload {
523 domain: AVC_CREDENTIAL_SIGNING_DOMAIN,
524 schema_version: self.schema_version,
525 issuer_did: &self.issuer_did,
526 principal_did: &self.principal_did,
527 subject_did: &self.subject_did,
528 holder_did: self.holder_did.as_ref(),
529 subject_kind: &self.subject_kind,
530 created_at: &self.created_at,
531 expires_at: self.expires_at.as_ref(),
532 delegated_intent: &self.delegated_intent,
533 authority_scope: &self.authority_scope,
534 constraints: &self.constraints,
535 authority_chain: self.authority_chain.as_ref(),
536 consent_refs: &self.consent_refs,
537 policy_refs: &self.policy_refs,
538 parent_avc_id: self.parent_avc_id.as_ref(),
539 })
540 .map_err(AvcError::from)
541 }
542
543 #[must_use]
546 pub fn effective_holder(&self) -> &Did {
547 self.holder_did.as_ref().unwrap_or(&self.subject_did)
548 }
549}
550
551pub fn issue_avc<F>(mut draft: AvcDraft, sign: F) -> Result<AutonomousVolitionCredential, AvcError>
562where
563 F: FnOnce(&[u8]) -> Signature,
564{
565 draft.normalize_and_validate()?;
566
567 let mut credential = AutonomousVolitionCredential {
568 schema_version: draft.schema_version,
569 issuer_did: draft.issuer_did,
570 principal_did: draft.principal_did,
571 subject_did: draft.subject_did,
572 holder_did: draft.holder_did,
573 subject_kind: draft.subject_kind,
574 created_at: draft.created_at,
575 expires_at: draft.expires_at,
576 delegated_intent: draft.delegated_intent,
577 authority_scope: draft.authority_scope,
578 constraints: draft.constraints,
579 authority_chain: draft.authority_chain,
580 consent_refs: draft.consent_refs,
581 policy_refs: draft.policy_refs,
582 parent_avc_id: draft.parent_avc_id,
583 signature: Signature::empty(),
584 };
585
586 let payload = credential.signing_payload()?;
587 credential.signature = sign(&payload);
588 Ok(credential)
589}
590
591fn non_empty(value: &str, field: &'static str) -> Result<(), AvcError> {
596 if value.trim().is_empty() {
597 Err(AvcError::EmptyField { field })
598 } else {
599 Ok(())
600 }
601}
602
603fn require_bp(field: &'static str, value: u32) -> Result<(), AvcError> {
604 if value > MAX_BASIS_POINTS {
605 Err(AvcError::BasisPointOutOfRange { field, value })
606 } else {
607 Ok(())
608 }
609}
610
611fn sort_dedup<T: Ord, I: IntoIterator<Item = T>>(items: I) -> Vec<T> {
612 let set: BTreeSet<T> = items.into_iter().collect();
613 set.into_iter().collect()
614}
615
616fn sort_dedup_copy<T: Ord + Copy, I: IntoIterator<Item = T>>(items: I) -> Vec<T> {
617 let set: BTreeSet<T> = items.into_iter().collect();
618 set.into_iter().collect()
619}
620
621#[cfg(test)]
622pub(crate) mod test_support {
623 use super::*;
624
625 pub fn did(label: &str) -> Did {
626 Did::new(&format!("did:exo:{label}")).expect("test DID")
627 }
628
629 pub fn ts(physical: u64) -> Timestamp {
630 Timestamp::new(physical, 0)
631 }
632
633 pub fn h256(byte: u8) -> Hash256 {
634 Hash256::from_bytes([byte; 32])
635 }
636
637 pub fn permissive_intent(purpose: &str) -> DelegatedIntent {
638 DelegatedIntent {
639 intent_id: h256(0xAA),
640 purpose: purpose.into(),
641 allowed_objectives: vec!["primary".into()],
642 prohibited_objectives: vec![],
643 autonomy_level: AutonomyLevel::Draft,
644 delegation_allowed: true,
645 }
646 }
647
648 pub fn permissive_scope() -> AuthorityScope {
649 AuthorityScope {
650 permissions: vec![Permission::Read, Permission::Write],
651 tools: vec!["alpha".into(), "beta".into()],
652 data_classes: vec![DataClass::Public, DataClass::Internal],
653 counterparties: vec![],
654 jurisdictions: vec!["US".into()],
655 }
656 }
657
658 pub fn baseline_draft() -> AvcDraft {
659 AvcDraft {
660 schema_version: AVC_SCHEMA_VERSION,
661 issuer_did: did("issuer"),
662 principal_did: did("issuer"),
663 subject_did: did("agent"),
664 holder_did: None,
665 subject_kind: AvcSubjectKind::AiAgent {
666 model_id: "alpha".into(),
667 agent_version: Some("1.0.0".into()),
668 },
669 created_at: ts(1_000_000),
670 expires_at: Some(ts(2_000_000)),
671 delegated_intent: permissive_intent("research"),
672 authority_scope: permissive_scope(),
673 constraints: AvcConstraints::permissive(),
674 authority_chain: None,
675 consent_refs: vec![],
676 policy_refs: vec![],
677 parent_avc_id: None,
678 }
679 }
680}
681
682#[cfg(test)]
683mod tests {
684 use super::{test_support::*, *};
685
686 fn fixed_signature() -> Signature {
687 Signature::from_bytes([7u8; 64])
688 }
689
690 #[test]
691 fn issue_avc_succeeds_for_valid_draft() {
692 let draft = baseline_draft();
693 let cred = issue_avc(draft, |_| fixed_signature()).unwrap();
694 assert_eq!(cred.signature, fixed_signature());
695 }
696
697 #[test]
698 fn issue_avc_normalizes_collections_and_dedupes() {
699 let mut draft = baseline_draft();
700 draft.authority_scope.tools = vec!["beta".into(), "alpha".into(), "alpha".into()];
701 draft.authority_scope.permissions =
702 vec![Permission::Write, Permission::Read, Permission::Read];
703 let cred = issue_avc(draft, |_| fixed_signature()).unwrap();
704 assert_eq!(cred.authority_scope.tools, vec!["alpha", "beta"]);
705 assert_eq!(
706 cred.authority_scope.permissions,
707 vec![Permission::Read, Permission::Write]
708 );
709 }
710
711 #[test]
712 fn issue_avc_rejects_unsupported_schema() {
713 let mut draft = baseline_draft();
714 draft.schema_version = 99;
715 let err = issue_avc(draft, |_| fixed_signature()).unwrap_err();
716 assert!(matches!(err, AvcError::UnsupportedSchema { got: 99, .. }));
717 }
718
719 #[test]
720 fn protocol_version_support_accepts_legacy_and_current_rejects_future() {
721 assert_eq!(
722 require_supported_avc_protocol_version(None).unwrap(),
723 AVC_PROTOCOL_VERSION
724 );
725 assert_eq!(
726 require_supported_avc_protocol_version(Some(AVC_PROTOCOL_VERSION)).unwrap(),
727 AVC_PROTOCOL_VERSION
728 );
729 let err = require_supported_avc_protocol_version(Some(AVC_PROTOCOL_VERSION + 1))
730 .expect_err("future AVC protocol version must fail closed");
731 assert!(matches!(
732 err,
733 AvcError::UnsupportedProtocol {
734 got,
735 min_supported: AVC_MIN_SUPPORTED_PROTOCOL_VERSION,
736 max_supported: AVC_MAX_SUPPORTED_PROTOCOL_VERSION,
737 } if got == AVC_PROTOCOL_VERSION + 1
738 ));
739 }
740
741 #[test]
742 fn issue_avc_rejects_empty_purpose() {
743 let mut draft = baseline_draft();
744 draft.delegated_intent.purpose = " ".into();
745 let err = issue_avc(draft, |_| fixed_signature()).unwrap_err();
746 assert!(
747 matches!(err, AvcError::EmptyField { field } if field == "delegated_intent.purpose")
748 );
749 }
750
751 #[test]
752 fn issue_avc_rejects_empty_allowed_objective() {
753 let mut draft = baseline_draft();
754 draft.delegated_intent.allowed_objectives = vec!["valid".into(), " ".into()];
755 let err = issue_avc(draft, |_| fixed_signature()).unwrap_err();
756 assert!(matches!(err, AvcError::EmptyField { .. }));
757 }
758
759 #[test]
760 fn issue_avc_rejects_empty_prohibited_objective() {
761 let mut draft = baseline_draft();
762 draft.delegated_intent.prohibited_objectives = vec!["".into()];
763 let err = issue_avc(draft, |_| fixed_signature()).unwrap_err();
764 assert!(matches!(err, AvcError::EmptyField { .. }));
765 }
766
767 #[test]
768 fn issue_avc_rejects_empty_tool_in_scope() {
769 let mut draft = baseline_draft();
770 draft.authority_scope.tools = vec!["".into()];
771 let err = issue_avc(draft, |_| fixed_signature()).unwrap_err();
772 assert!(matches!(err, AvcError::EmptyField { .. }));
773 }
774
775 #[test]
776 fn issue_avc_rejects_empty_jurisdiction() {
777 let mut draft = baseline_draft();
778 draft.authority_scope.jurisdictions = vec!["".into()];
779 let err = issue_avc(draft, |_| fixed_signature()).unwrap_err();
780 assert!(matches!(err, AvcError::EmptyField { .. }));
781 }
782
783 #[test]
784 fn issue_avc_rejects_empty_data_class_custom() {
785 let mut draft = baseline_draft();
786 draft.authority_scope.data_classes = vec![DataClass::Custom(" ".into())];
787 let err = issue_avc(draft, |_| fixed_signature()).unwrap_err();
788 assert!(matches!(err, AvcError::EmptyField { .. }));
789 }
790
791 #[test]
792 fn issue_avc_rejects_empty_currency_code() {
793 let mut draft = baseline_draft();
794 draft.constraints.currency_code = Some(" ".into());
795 let err = issue_avc(draft, |_| fixed_signature()).unwrap_err();
796 assert!(matches!(err, AvcError::EmptyField { .. }));
797 }
798
799 #[test]
800 fn issue_avc_rejects_empty_forbidden_action() {
801 let mut draft = baseline_draft();
802 draft.constraints.forbidden_actions = vec!["".into()];
803 let err = issue_avc(draft, |_| fixed_signature()).unwrap_err();
804 assert!(matches!(err, AvcError::EmptyField { .. }));
805 }
806
807 #[test]
808 fn issue_avc_rejects_empty_emergency_stop_ref() {
809 let mut draft = baseline_draft();
810 draft.constraints.emergency_stop_refs = vec!["".into()];
811 let err = issue_avc(draft, |_| fixed_signature()).unwrap_err();
812 assert!(matches!(err, AvcError::EmptyField { .. }));
813 }
814
815 #[test]
816 fn issue_avc_rejects_basis_points_out_of_range() {
817 let mut draft = baseline_draft();
818 draft.constraints.max_action_risk_bp = Some(11_000);
819 let err = issue_avc(draft, |_| fixed_signature()).unwrap_err();
820 assert!(matches!(err, AvcError::BasisPointOutOfRange { .. }));
821 }
822
823 #[test]
824 fn issue_avc_rejects_approval_threshold_out_of_range() {
825 let mut draft = baseline_draft();
826 draft.constraints.approval_threshold_bp = Some(99_999);
827 let err = issue_avc(draft, |_| fixed_signature()).unwrap_err();
828 assert!(matches!(err, AvcError::BasisPointOutOfRange { .. }));
829 }
830
831 #[test]
832 fn issue_avc_rejects_expiry_at_or_before_created_at() {
833 let mut draft = baseline_draft();
834 draft.expires_at = Some(draft.created_at);
835 let err = issue_avc(draft, |_| fixed_signature()).unwrap_err();
836 assert!(matches!(err, AvcError::InvalidTimestamp { .. }));
837 }
838
839 #[test]
840 fn issue_avc_rejects_inverted_time_window() {
841 let mut draft = baseline_draft();
842 draft.constraints.allowed_time_window = Some(TimeWindow {
843 not_before: ts(2_000),
844 not_after: ts(1_000),
845 });
846 let err = issue_avc(draft, |_| fixed_signature()).unwrap_err();
847 assert!(matches!(err, AvcError::InvalidTimestamp { .. }));
848 }
849
850 #[test]
851 fn issue_avc_rejects_empty_subject_kind_field() {
852 let mut draft = baseline_draft();
853 draft.subject_kind = AvcSubjectKind::AgentSwarm {
854 swarm_id: "".into(),
855 };
856 assert!(issue_avc(draft, |_| fixed_signature()).is_err());
857
858 let mut draft = baseline_draft();
859 draft.subject_kind = AvcSubjectKind::Workflow {
860 workflow_id: "".into(),
861 };
862 assert!(issue_avc(draft, |_| fixed_signature()).is_err());
863
864 let mut draft = baseline_draft();
865 draft.subject_kind = AvcSubjectKind::Service {
866 service_id: "".into(),
867 };
868 assert!(issue_avc(draft, |_| fixed_signature()).is_err());
869
870 let mut draft = baseline_draft();
871 draft.subject_kind = AvcSubjectKind::Holon {
872 holon_id: "".into(),
873 };
874 assert!(issue_avc(draft, |_| fixed_signature()).is_err());
875
876 let mut draft = baseline_draft();
877 draft.subject_kind = AvcSubjectKind::OrganizationUnit { unit_id: "".into() };
878 assert!(issue_avc(draft, |_| fixed_signature()).is_err());
879 }
880
881 #[test]
882 fn subject_kind_unknown_validates() {
883 let mut draft = baseline_draft();
884 draft.subject_kind = AvcSubjectKind::Unknown;
885 let cred = issue_avc(draft, |_| fixed_signature()).unwrap();
886 assert!(matches!(cred.subject_kind, AvcSubjectKind::Unknown));
887 }
888
889 #[test]
890 fn id_is_deterministic() {
891 let draft = baseline_draft();
892 let cred1 = issue_avc(draft.clone(), |_| fixed_signature()).unwrap();
893 let cred2 = issue_avc(draft, |_| fixed_signature()).unwrap();
894 assert_eq!(cred1.id().unwrap(), cred2.id().unwrap());
895 }
896
897 #[test]
898 fn id_changes_when_signed_field_changes() {
899 let draft1 = baseline_draft();
900 let mut draft2 = draft1.clone();
901 draft2.delegated_intent.purpose = "different".into();
902 let cred1 = issue_avc(draft1, |_| fixed_signature()).unwrap();
903 let cred2 = issue_avc(draft2, |_| fixed_signature()).unwrap();
904 assert_ne!(cred1.id().unwrap(), cred2.id().unwrap());
905 }
906
907 #[test]
908 fn signing_payload_contains_domain_tag() {
909 let cred = issue_avc(baseline_draft(), |_| fixed_signature()).unwrap();
910 let bytes = cred.signing_payload().unwrap();
911 let needle = AVC_CREDENTIAL_SIGNING_DOMAIN.as_bytes();
912 assert!(bytes.windows(needle.len()).any(|w| w == needle));
913 }
914
915 #[test]
916 fn signing_payload_excludes_signature_so_id_is_signature_independent() {
917 let mut cred = issue_avc(baseline_draft(), |_| fixed_signature()).unwrap();
918 let id1 = cred.id().unwrap();
919 cred.signature = Signature::from_bytes([0x42u8; 64]);
920 let id2 = cred.id().unwrap();
921 assert_eq!(id1, id2);
922 }
923
924 #[test]
925 fn id_changes_when_holder_changes() {
926 let mut draft1 = baseline_draft();
927 draft1.holder_did = Some(did("holder-a"));
928 let mut draft2 = draft1.clone();
929 draft2.holder_did = Some(did("holder-b"));
930 let id1 = issue_avc(draft1, |_| fixed_signature())
931 .unwrap()
932 .id()
933 .unwrap();
934 let id2 = issue_avc(draft2, |_| fixed_signature())
935 .unwrap()
936 .id()
937 .unwrap();
938 assert_ne!(id1, id2);
939 }
940
941 #[test]
942 fn id_changes_when_authority_chain_changes() {
943 let mut draft1 = baseline_draft();
944 draft1.authority_chain = Some(AuthorityChainRef {
945 chain_hash: h256(0x11),
946 });
947 let mut draft2 = draft1.clone();
948 draft2.authority_chain = Some(AuthorityChainRef {
949 chain_hash: h256(0x22),
950 });
951 let id1 = issue_avc(draft1, |_| fixed_signature())
952 .unwrap()
953 .id()
954 .unwrap();
955 let id2 = issue_avc(draft2, |_| fixed_signature())
956 .unwrap()
957 .id()
958 .unwrap();
959 assert_ne!(id1, id2);
960 }
961
962 #[test]
963 fn content_hash_matches_id() {
964 let cred = issue_avc(baseline_draft(), |_| fixed_signature()).unwrap();
965 assert_eq!(cred.content_hash().unwrap(), cred.id().unwrap());
966 }
967
968 #[test]
969 fn effective_holder_defaults_to_subject() {
970 let cred = issue_avc(baseline_draft(), |_| fixed_signature()).unwrap();
971 assert_eq!(cred.effective_holder(), &cred.subject_did);
972 }
973
974 #[test]
975 fn effective_holder_uses_explicit_holder_when_present() {
976 let mut draft = baseline_draft();
977 draft.holder_did = Some(did("holder-x"));
978 let cred = issue_avc(draft, |_| fixed_signature()).unwrap();
979 assert_eq!(cred.effective_holder(), &did("holder-x"));
980 }
981
982 #[test]
983 fn time_window_contains_inclusive_bounds() {
984 let window = TimeWindow {
985 not_before: ts(100),
986 not_after: ts(200),
987 };
988 assert!(window.contains(&ts(100)));
989 assert!(window.contains(&ts(150)));
990 assert!(window.contains(&ts(200)));
991 assert!(!window.contains(&ts(99)));
992 assert!(!window.contains(&ts(201)));
993 }
994
995 #[test]
996 fn autonomy_level_orderable() {
997 assert!(AutonomyLevel::ObserveOnly < AutonomyLevel::Recommend);
998 assert!(AutonomyLevel::Recommend < AutonomyLevel::Draft);
999 assert!(AutonomyLevel::Draft < AutonomyLevel::ExecuteWithHumanApproval);
1000 assert!(AutonomyLevel::ExecuteWithHumanApproval < AutonomyLevel::ExecuteWithinBounds);
1001 assert!(AutonomyLevel::ExecuteWithinBounds < AutonomyLevel::DelegateWithinBounds);
1002 }
1003
1004 #[test]
1005 fn permissions_normalize_deterministically() {
1006 let mut draft = baseline_draft();
1007 draft.authority_scope.permissions = vec![
1008 Permission::Govern,
1009 Permission::Read,
1010 Permission::Write,
1011 Permission::Read,
1012 ];
1013 let cred = issue_avc(draft, |_| fixed_signature()).unwrap();
1014 assert_eq!(
1015 cred.authority_scope.permissions,
1016 vec![Permission::Read, Permission::Write, Permission::Govern]
1017 );
1018 }
1019
1020 #[test]
1021 fn consent_and_policy_refs_normalize() {
1022 let mut draft = baseline_draft();
1023 draft.consent_refs = vec![
1024 ConsentRef {
1025 consent_id: h256(2),
1026 required: true,
1027 },
1028 ConsentRef {
1029 consent_id: h256(1),
1030 required: true,
1031 },
1032 ConsentRef {
1033 consent_id: h256(2),
1034 required: true,
1035 },
1036 ];
1037 draft.policy_refs = vec![
1038 PolicyRef {
1039 policy_id: h256(5),
1040 policy_version: 1,
1041 required: true,
1042 },
1043 PolicyRef {
1044 policy_id: h256(5),
1045 policy_version: 1,
1046 required: true,
1047 },
1048 ];
1049 let cred = issue_avc(draft, |_| fixed_signature()).unwrap();
1050 assert_eq!(cred.consent_refs.len(), 2);
1051 assert!(cred.consent_refs[0].consent_id <= cred.consent_refs[1].consent_id);
1052 assert_eq!(cred.policy_refs.len(), 1);
1053 }
1054
1055 #[test]
1056 fn permissive_constraints_validate() {
1057 let constraints = AvcConstraints::permissive();
1058 assert!(constraints.validate().is_ok());
1059 }
1060
1061 #[test]
1062 fn empty_authority_scope_validates() {
1063 let mut scope = AuthorityScope::empty();
1064 scope.normalize();
1065 assert!(scope.validate().is_ok());
1066 }
1067}