1use std::collections::{BTreeMap, BTreeSet};
22
23use exo_core::Did;
24use serde::{Deserialize, Serialize};
25
26#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
32pub struct Permission(pub String);
33
34impl Permission {
35 #[must_use]
36 pub fn new(value: impl Into<String>) -> Self {
37 Self(value.into())
38 }
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
43pub struct PermissionSet {
44 pub permissions: Vec<Permission>,
45}
46
47impl PermissionSet {
48 #[must_use]
49 pub fn new(permissions: Vec<Permission>) -> Self {
50 Self { permissions }
51 }
52
53 pub fn contains(&self, p: &Permission) -> bool {
54 self.permissions.contains(p)
55 }
56
57 pub fn is_empty(&self) -> bool {
58 self.permissions.is_empty()
59 }
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
68pub enum GovernmentBranch {
69 Legislative,
70 Executive,
71 Judicial,
72}
73
74impl GovernmentBranch {
75 #[must_use]
76 pub const fn as_str(self) -> &'static str {
77 match self {
78 Self::Legislative => "legislative",
79 Self::Executive => "executive",
80 Self::Judicial => "judicial",
81 }
82 }
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
91#[serde(rename_all = "kebab-case")]
92pub enum GovernedRoleName {
93 Senator,
94 Legislator,
95 Voter,
96 Executive,
97 ExecutiveAdmin,
98 Operator,
99 Worker,
100 Judge,
101 TransitionJudge,
102}
103
104impl GovernedRoleName {
105 #[must_use]
106 pub const fn as_str(self) -> &'static str {
107 match self {
108 Self::Senator => "senator",
109 Self::Legislator => "legislator",
110 Self::Voter => "voter",
111 Self::Executive => "executive",
112 Self::ExecutiveAdmin => "executive-admin",
113 Self::Operator => "operator",
114 Self::Worker => "worker",
115 Self::Judge => "judge",
116 Self::TransitionJudge => "transition-judge",
117 }
118 }
119
120 #[must_use]
121 pub const fn branch(self) -> GovernmentBranch {
122 match self {
123 Self::Senator | Self::Legislator | Self::Voter => GovernmentBranch::Legislative,
124 Self::Executive | Self::ExecutiveAdmin | Self::Operator | Self::Worker => {
125 GovernmentBranch::Executive
126 }
127 Self::Judge | Self::TransitionJudge => GovernmentBranch::Judicial,
128 }
129 }
130
131 #[must_use]
132 pub fn parse(value: &str) -> Option<Self> {
133 match value {
134 "senator" => Some(Self::Senator),
135 "legislator" => Some(Self::Legislator),
136 "voter" => Some(Self::Voter),
137 "executive" => Some(Self::Executive),
138 "executive-admin" => Some(Self::ExecutiveAdmin),
139 "operator" => Some(Self::Operator),
140 "worker" => Some(Self::Worker),
141 "judge" => Some(Self::Judge),
142 "transition-judge" => Some(Self::TransitionJudge),
143 _ => None,
144 }
145 }
146}
147
148#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
150pub enum RoleValidationError {
151 #[error("unknown governed role name")]
152 UnknownName { name: String },
153 #[error(
154 "role name does not match governed branch: expected {expected_branch}, actual {actual_branch}"
155 )]
156 BranchMismatch {
157 name: String,
158 expected_branch: &'static str,
159 actual_branch: &'static str,
160 },
161}
162
163#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
165pub struct Role {
166 pub name: String,
167 pub branch: GovernmentBranch,
168}
169
170impl Role {
171 #[must_use]
172 pub fn governed(name: GovernedRoleName) -> Self {
173 Self {
174 name: name.as_str().to_owned(),
175 branch: name.branch(),
176 }
177 }
178
179 pub fn validate_governed(&self) -> Result<GovernedRoleName, RoleValidationError> {
187 let Some(governed_name) = GovernedRoleName::parse(&self.name) else {
188 return Err(RoleValidationError::UnknownName {
189 name: self.name.clone(),
190 });
191 };
192 let expected_branch = governed_name.branch();
193 if expected_branch != self.branch {
194 return Err(RoleValidationError::BranchMismatch {
195 name: self.name.clone(),
196 expected_branch: expected_branch.as_str(),
197 actual_branch: self.branch.as_str(),
198 });
199 }
200 Ok(governed_name)
201 }
202
203 pub fn try_new(
210 name: impl Into<String>,
211 branch: GovernmentBranch,
212 ) -> Result<Self, RoleValidationError> {
213 let role = Self {
214 name: name.into(),
215 branch,
216 };
217 role.validate_governed()?;
218 Ok(role)
219 }
220}
221
222pub const DAGDB_WRITEBACK_SCOPE: &str = "dag-db:writeback";
228
229#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
231pub enum BailmentState {
232 None,
234 Active {
236 bailor: Did,
237 bailee: Did,
238 scope: String,
239 },
240 Suspended { reason: String },
242 Terminated,
244}
245
246impl BailmentState {
247 pub fn is_active(&self) -> bool {
248 matches!(self, BailmentState::Active { .. })
249 }
250
251 pub fn authorizes_writeback(&self, agent_did: &str) -> bool {
252 matches!(
253 self,
254 BailmentState::Active { bailee, scope, .. }
255 if bailee.as_str() == agent_did && scope == DAGDB_WRITEBACK_SCOPE
256 )
257 }
258}
259
260#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
266pub struct ConsentRecord {
267 pub subject: Did,
268 pub granted_to: Did,
269 pub scope: String,
270 pub active: bool,
271}
272
273#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
279pub struct AuthorityChain {
280 pub links: Vec<AuthorityLink>,
281}
282
283impl AuthorityChain {
284 pub fn is_empty(&self) -> bool {
285 self.links.is_empty()
286 }
287
288 pub fn depth(&self) -> usize {
289 self.links.len()
290 }
291}
292
293#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
295pub struct AuthorityLink {
296 pub grantor: Did,
297 pub grantee: Did,
298 pub permissions: PermissionSet,
299 pub signature: Vec<u8>,
300 #[serde(default, skip_serializing_if = "Option::is_none")]
306 pub grantor_public_key: Option<Vec<u8>>,
307}
308
309pub type TrustedAuthorityKeys = BTreeMap<Did, Vec<Vec<u8>>>;
315
316pub type TrustedProvenanceKeys = BTreeMap<Did, Vec<Vec<u8>>>;
322
323#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
329pub struct QuorumEvidence {
330 pub threshold: u32,
331 pub votes: Vec<QuorumVote>,
332}
333
334impl QuorumEvidence {
335 pub fn is_met(&self) -> bool {
338 let Some(threshold) = usize::try_from(self.threshold).ok() else {
339 return false;
340 };
341 self.distinct_approved_voter_count() >= threshold
342 }
343
344 pub fn is_met_authentic(&self) -> bool {
349 let Some(threshold) = usize::try_from(self.threshold).ok() else {
350 return false;
351 };
352 self.distinct_authentic_approved_voter_count() >= threshold
353 }
354
355 pub fn synthetic_vote_count(&self) -> usize {
357 self.votes
358 .iter()
359 .filter(|v| v.provenance.as_ref().is_some_and(|p| p.is_synthetic()))
360 .count()
361 }
362
363 #[must_use]
365 pub fn duplicate_voters(&self) -> BTreeSet<Did> {
366 let mut seen = BTreeSet::new();
367 let mut duplicates = BTreeSet::new();
368 for vote in &self.votes {
369 if !seen.insert(vote.voter.clone()) {
370 duplicates.insert(vote.voter.clone());
371 }
372 }
373 duplicates
374 }
375
376 #[must_use]
378 pub fn distinct_approved_voter_count(&self) -> usize {
379 self.votes
380 .iter()
381 .filter(|vote| vote.approved)
382 .map(|vote| vote.voter.clone())
383 .collect::<BTreeSet<_>>()
384 .len()
385 }
386
387 #[must_use]
389 pub fn distinct_authentic_approved_voter_count(&self) -> usize {
390 self.votes
391 .iter()
392 .filter(|vote| {
393 vote.approved
394 && vote
395 .provenance
396 .as_ref()
397 .is_some_and(Provenance::is_authentic_human_quorum_voice)
398 })
399 .map(|vote| vote.voter.clone())
400 .collect::<BTreeSet<_>>()
401 .len()
402 }
403}
404
405#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
407pub struct QuorumVote {
408 pub voter: Did,
409 pub approved: bool,
410 pub signature: Vec<u8>,
411 #[serde(default, skip_serializing_if = "Option::is_none")]
416 pub provenance: Option<Provenance>,
417}
418
419#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
428pub enum VoiceKind {
429 Human,
431 Synthetic,
434 System,
436}
437
438#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
443pub enum IndependenceClaim {
444 Independent,
446 Coordinated,
448}
449
450#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
455pub enum ReviewOrder {
456 FirstOrder,
458 Derivative,
460}
461
462#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
468pub struct Provenance {
469 pub actor: Did,
470 pub timestamp: String,
471 pub action_hash: Vec<u8>,
472 pub signature: Vec<u8>,
473 #[serde(default, skip_serializing_if = "Option::is_none")]
479 pub public_key: Option<Vec<u8>>,
480 #[serde(default, skip_serializing_if = "Option::is_none")]
485 pub voice_kind: Option<VoiceKind>,
486 #[serde(default, skip_serializing_if = "Option::is_none")]
489 pub independence: Option<IndependenceClaim>,
490 #[serde(default, skip_serializing_if = "Option::is_none")]
492 pub review_order: Option<ReviewOrder>,
493}
494
495impl Provenance {
496 pub fn is_signed(&self) -> bool {
497 !self.signature.is_empty()
498 }
499
500 pub fn is_human_voice(&self) -> bool {
504 self.voice_kind == Some(VoiceKind::Human)
505 }
506
507 pub fn is_independent(&self) -> bool {
510 self.independence == Some(IndependenceClaim::Independent)
511 }
512
513 pub fn is_first_order_review(&self) -> bool {
516 self.review_order == Some(ReviewOrder::FirstOrder)
517 }
518
519 pub fn is_authentic_human_quorum_voice(&self) -> bool {
523 self.is_human_voice() && self.is_independent() && self.is_first_order_review()
524 }
525
526 pub fn is_synthetic(&self) -> bool {
528 self.voice_kind == Some(VoiceKind::Synthetic)
529 }
530}
531
532#[cfg(test)]
537mod tests {
538 use super::*;
539
540 fn did(s: &str) -> Did {
541 Did::new(s).expect("valid DID")
542 }
543
544 #[test]
545 fn permission_set_contains() {
546 let set = PermissionSet::new(vec![Permission::new("read"), Permission::new("write")]);
547 assert!(set.contains(&Permission::new("read")));
548 assert!(!set.contains(&Permission::new("admin")));
549 }
550
551 #[test]
552 fn permission_set_empty() {
553 let set = PermissionSet::default();
554 assert!(set.is_empty());
555 }
556
557 #[test]
558 fn bailment_state_is_active() {
559 let active = BailmentState::Active {
560 bailor: did("did:exo:bailor"),
561 bailee: did("did:exo:bailee"),
562 scope: "data".into(),
563 };
564 assert!(active.is_active());
565 assert!(!BailmentState::None.is_active());
566 assert!(!BailmentState::Terminated.is_active());
567 let suspended = BailmentState::Suspended {
568 reason: "audit".into(),
569 };
570 assert!(!suspended.is_active());
571 }
572
573 #[test]
574 fn bailment_state_authorizes_writeback_for_active_bailee_and_scope() {
575 let active = BailmentState::Active {
576 bailor: did("did:exo:bailor"),
577 bailee: did("did:exo:bailee"),
578 scope: DAGDB_WRITEBACK_SCOPE.into(),
579 };
580
581 assert!(active.authorizes_writeback("did:exo:bailee"));
582 }
583
584 #[test]
585 fn bailment_state_authorizes_writeback_rejects_wrong_bailee() {
586 let active = BailmentState::Active {
587 bailor: did("did:exo:bailor"),
588 bailee: did("did:exo:bailee"),
589 scope: DAGDB_WRITEBACK_SCOPE.into(),
590 };
591
592 assert!(!active.authorizes_writeback("did:exo:other"));
593 }
594
595 #[test]
596 fn bailment_state_authorizes_writeback_rejects_wrong_scope() {
597 let active = BailmentState::Active {
598 bailor: did("did:exo:bailor"),
599 bailee: did("did:exo:bailee"),
600 scope: "dag-db:read".into(),
601 };
602
603 assert!(!active.authorizes_writeback("did:exo:bailee"));
604 }
605
606 #[test]
607 fn bailment_state_authorizes_writeback_rejects_inactive_states() {
608 assert!(!BailmentState::None.authorizes_writeback("did:exo:bailee"));
609 assert!(!BailmentState::Terminated.authorizes_writeback("did:exo:bailee"));
610 assert!(
611 !BailmentState::Suspended {
612 reason: "audit".into(),
613 }
614 .authorizes_writeback("did:exo:bailee")
615 );
616 }
617
618 #[test]
619 fn authority_chain_empty() {
620 let chain = AuthorityChain::default();
621 assert!(chain.is_empty());
622 assert_eq!(chain.depth(), 0);
623 }
624
625 #[test]
626 fn authority_chain_depth() {
627 let chain = AuthorityChain {
628 links: vec![
629 AuthorityLink {
630 grantor: did("did:exo:root"),
631 grantee: did("did:exo:mid"),
632 permissions: PermissionSet::default(),
633 signature: vec![1],
634 grantor_public_key: None,
635 },
636 AuthorityLink {
637 grantor: did("did:exo:mid"),
638 grantee: did("did:exo:leaf"),
639 permissions: PermissionSet::default(),
640 signature: vec![2],
641 grantor_public_key: None,
642 },
643 ],
644 };
645 assert_eq!(chain.depth(), 2);
646 assert!(!chain.is_empty());
647 }
648
649 fn make_vote(voter: &str, approved: bool, sig: u8, voice: Option<VoiceKind>) -> QuorumVote {
650 QuorumVote {
651 voter: did(voter),
652 approved,
653 signature: vec![sig],
654 provenance: voice.map(|vk| Provenance {
655 actor: did(voter),
656 timestamp: "t".into(),
657 action_hash: vec![1],
658 signature: vec![sig],
659 public_key: None,
660 voice_kind: Some(vk),
661 independence: (vk == VoiceKind::Human).then_some(IndependenceClaim::Independent),
662 review_order: (vk == VoiceKind::Human).then_some(ReviewOrder::FirstOrder),
663 }),
664 }
665 }
666
667 #[test]
668 fn quorum_evidence_met() {
669 let ev = QuorumEvidence {
670 threshold: 2,
671 votes: vec![
672 QuorumVote {
673 voter: did("did:exo:v1"),
674 approved: true,
675 signature: vec![1],
676 provenance: None,
677 },
678 QuorumVote {
679 voter: did("did:exo:v2"),
680 approved: true,
681 signature: vec![2],
682 provenance: None,
683 },
684 QuorumVote {
685 voter: did("did:exo:v3"),
686 approved: false,
687 signature: vec![3],
688 provenance: None,
689 },
690 ],
691 };
692 assert!(ev.is_met());
693 }
694
695 #[test]
696 fn quorum_evidence_not_met() {
697 let ev = QuorumEvidence {
698 threshold: 3,
699 votes: vec![
700 QuorumVote {
701 voter: did("did:exo:v1"),
702 approved: true,
703 signature: vec![1],
704 provenance: None,
705 },
706 QuorumVote {
707 voter: did("did:exo:v2"),
708 approved: false,
709 signature: vec![2],
710 provenance: None,
711 },
712 ],
713 };
714 assert!(!ev.is_met());
715 }
716
717 #[test]
718 fn quorum_evidence_counts_distinct_voters_only() {
719 let ev = QuorumEvidence {
720 threshold: 2,
721 votes: vec![
722 QuorumVote {
723 voter: did("did:exo:v1"),
724 approved: true,
725 signature: vec![1],
726 provenance: None,
727 },
728 QuorumVote {
729 voter: did("did:exo:v1"),
730 approved: true,
731 signature: vec![2],
732 provenance: None,
733 },
734 ],
735 };
736 assert!(
737 !ev.is_met(),
738 "duplicate voter DIDs must not inflate raw quorum evidence"
739 );
740 }
741
742 #[test]
745 fn quorum_is_met_authentic_excludes_synthetic() {
746 let ev = QuorumEvidence {
748 threshold: 3,
749 votes: vec![
750 make_vote("did:exo:h1", true, 1, Some(VoiceKind::Human)),
751 make_vote("did:exo:h2", true, 2, Some(VoiceKind::Human)),
752 make_vote("did:exo:ai1", true, 3, Some(VoiceKind::Synthetic)),
753 ],
754 };
755 assert!(ev.is_met(), "raw count should pass (3 approvals)");
756 assert!(
757 !ev.is_met_authentic(),
758 "authentic count should fail (only 2 human)"
759 );
760 assert_eq!(ev.synthetic_vote_count(), 1);
761 }
762
763 #[test]
764 fn quorum_is_met_authentic_passes_all_human() {
765 let ev = QuorumEvidence {
766 threshold: 2,
767 votes: vec![
768 make_vote("did:exo:h1", true, 1, Some(VoiceKind::Human)),
769 make_vote("did:exo:h2", true, 2, Some(VoiceKind::Human)),
770 ],
771 };
772 assert!(ev.is_met_authentic());
773 assert_eq!(ev.synthetic_vote_count(), 0);
774 }
775
776 #[test]
777 fn quorum_is_met_authentic_counts_distinct_humans_only() {
778 let ev = QuorumEvidence {
779 threshold: 2,
780 votes: vec![
781 make_vote("did:exo:h1", true, 1, Some(VoiceKind::Human)),
782 make_vote("did:exo:h1", true, 2, Some(VoiceKind::Human)),
783 ],
784 };
785 assert!(
786 !ev.is_met_authentic(),
787 "duplicate human voter DIDs must not inflate authentic quorum evidence"
788 );
789 }
790
791 #[test]
792 fn quorum_is_met_authentic_rejects_legacy_votes_without_provenance() {
793 let ev = QuorumEvidence {
795 threshold: 2,
796 votes: vec![
797 QuorumVote {
798 voter: did("did:exo:v1"),
799 approved: true,
800 signature: vec![1],
801 provenance: None,
802 },
803 QuorumVote {
804 voter: did("did:exo:v2"),
805 approved: true,
806 signature: vec![2],
807 provenance: None,
808 },
809 ],
810 };
811 assert!(!ev.is_met_authentic());
812 }
813
814 #[test]
815 fn quorum_is_met_authentic_rejects_votes_without_human_provenance() {
816 let ev = QuorumEvidence {
817 threshold: 2,
818 votes: vec![
819 QuorumVote {
820 voter: did("did:exo:v1"),
821 approved: true,
822 signature: vec![1],
823 provenance: None,
824 },
825 make_vote("did:exo:system1", true, 2, Some(VoiceKind::System)),
826 ],
827 };
828
829 assert!(
830 !ev.is_met_authentic(),
831 "authentic quorum must not assume missing or system provenance is human"
832 );
833 }
834
835 #[test]
836 fn quorum_is_met_authentic_requires_independent_first_order_human_votes() {
837 let mut coordinated = make_vote("did:exo:h1", true, 1, Some(VoiceKind::Human));
838 coordinated
839 .provenance
840 .as_mut()
841 .expect("human provenance")
842 .independence = Some(IndependenceClaim::Coordinated);
843 coordinated
844 .provenance
845 .as_mut()
846 .expect("human provenance")
847 .review_order = Some(ReviewOrder::FirstOrder);
848
849 let mut derivative = make_vote("did:exo:h2", true, 2, Some(VoiceKind::Human));
850 derivative
851 .provenance
852 .as_mut()
853 .expect("human provenance")
854 .independence = Some(IndependenceClaim::Independent);
855 derivative
856 .provenance
857 .as_mut()
858 .expect("human provenance")
859 .review_order = Some(ReviewOrder::Derivative);
860
861 let ev = QuorumEvidence {
862 threshold: 2,
863 votes: vec![coordinated, derivative],
864 };
865
866 assert!(
867 !ev.is_met_authentic(),
868 "coordinated or derivative human claims must not count as authentic quorum"
869 );
870 }
871
872 #[test]
875 fn provenance_is_human_voice() {
876 let human_prov = Provenance {
877 actor: did("did:exo:h1"),
878 timestamp: "t".into(),
879 action_hash: vec![1],
880 signature: vec![1],
881 public_key: None,
882 voice_kind: Some(VoiceKind::Human),
883 independence: Some(IndependenceClaim::Independent),
884 review_order: Some(ReviewOrder::FirstOrder),
885 };
886 assert!(human_prov.is_human_voice());
887 assert!(human_prov.is_independent());
888 assert!(human_prov.is_first_order_review());
889 assert!(human_prov.is_authentic_human_quorum_voice());
890 assert!(!human_prov.is_synthetic());
891 }
892
893 #[test]
894 fn provenance_synthetic_not_human() {
895 let ai_prov = Provenance {
896 actor: did("did:exo:ai1"),
897 timestamp: "t".into(),
898 action_hash: vec![1],
899 signature: vec![1],
900 public_key: None,
901 voice_kind: Some(VoiceKind::Synthetic),
902 independence: None,
903 review_order: None,
904 };
905 assert!(!ai_prov.is_human_voice());
906 assert!(ai_prov.is_synthetic());
907 assert!(!ai_prov.is_independent());
908 }
909
910 #[test]
911 fn provenance_unspecified_voice_not_human() {
912 let prov = Provenance {
914 actor: did("did:exo:unknown"),
915 timestamp: "t".into(),
916 action_hash: vec![1],
917 signature: vec![1],
918 public_key: None,
919 voice_kind: None,
920 independence: None,
921 review_order: None,
922 };
923 assert!(!prov.is_human_voice());
924 assert!(!prov.is_synthetic());
925 assert!(!prov.is_independent());
926 }
927
928 #[test]
929 fn provenance_is_signed() {
930 let signed = Provenance {
931 actor: did("did:exo:actor"),
932 timestamp: "2025-01-01".into(),
933 action_hash: vec![1],
934 signature: vec![4, 5, 6],
935 public_key: None,
936 voice_kind: None,
937 independence: None,
938 review_order: None,
939 };
940 assert!(signed.is_signed());
941
942 let unsigned = Provenance {
943 actor: did("did:exo:actor"),
944 timestamp: "2025-01-01".into(),
945 action_hash: vec![1],
946 signature: vec![],
947 public_key: None,
948 voice_kind: None,
949 independence: None,
950 review_order: None,
951 };
952 assert!(!unsigned.is_signed());
953 }
954}