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 const MAX_AUTHORITY_CHAIN_LINKS: usize = 5;
321
322pub type TrustedProvenanceKeys = BTreeMap<Did, Vec<Vec<u8>>>;
328
329#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
335pub struct QuorumEvidence {
336 pub threshold: u32,
337 pub votes: Vec<QuorumVote>,
338}
339
340impl QuorumEvidence {
341 pub fn is_met(&self) -> bool {
344 let Some(threshold) = usize::try_from(self.threshold).ok() else {
345 return false;
346 };
347 self.distinct_approved_voter_count() >= threshold
348 }
349
350 pub fn is_met_authentic(&self) -> bool {
355 let Some(threshold) = usize::try_from(self.threshold).ok() else {
356 return false;
357 };
358 self.distinct_authentic_approved_voter_count() >= threshold
359 }
360
361 pub fn synthetic_vote_count(&self) -> usize {
363 self.votes
364 .iter()
365 .filter(|v| v.provenance.as_ref().is_some_and(|p| p.is_synthetic()))
366 .count()
367 }
368
369 #[must_use]
371 pub fn duplicate_voters(&self) -> BTreeSet<Did> {
372 let mut seen = BTreeSet::new();
373 let mut duplicates = BTreeSet::new();
374 for vote in &self.votes {
375 if !seen.insert(vote.voter.clone()) {
376 duplicates.insert(vote.voter.clone());
377 }
378 }
379 duplicates
380 }
381
382 #[must_use]
384 pub fn distinct_approved_voter_count(&self) -> usize {
385 self.votes
386 .iter()
387 .filter(|vote| vote.approved)
388 .map(|vote| vote.voter.clone())
389 .collect::<BTreeSet<_>>()
390 .len()
391 }
392
393 #[must_use]
395 pub fn distinct_authentic_approved_voter_count(&self) -> usize {
396 self.votes
397 .iter()
398 .filter(|vote| {
399 vote.approved
400 && vote
401 .provenance
402 .as_ref()
403 .is_some_and(Provenance::is_authentic_human_quorum_voice)
404 })
405 .map(|vote| vote.voter.clone())
406 .collect::<BTreeSet<_>>()
407 .len()
408 }
409}
410
411#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
413pub struct QuorumVote {
414 pub voter: Did,
415 pub approved: bool,
416 pub signature: Vec<u8>,
417 #[serde(default, skip_serializing_if = "Option::is_none")]
422 pub provenance: Option<Provenance>,
423}
424
425#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
434pub enum VoiceKind {
435 Human,
437 Synthetic,
440 System,
442}
443
444#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
449pub enum IndependenceClaim {
450 Independent,
452 Coordinated,
454}
455
456#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
461pub enum ReviewOrder {
462 FirstOrder,
464 Derivative,
466}
467
468#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
474pub struct Provenance {
475 pub actor: Did,
476 pub timestamp: String,
477 pub action_hash: Vec<u8>,
478 pub signature: Vec<u8>,
479 #[serde(default, skip_serializing_if = "Option::is_none")]
485 pub public_key: Option<Vec<u8>>,
486 #[serde(default, skip_serializing_if = "Option::is_none")]
491 pub voice_kind: Option<VoiceKind>,
492 #[serde(default, skip_serializing_if = "Option::is_none")]
495 pub independence: Option<IndependenceClaim>,
496 #[serde(default, skip_serializing_if = "Option::is_none")]
498 pub review_order: Option<ReviewOrder>,
499}
500
501impl Provenance {
502 pub fn is_signed(&self) -> bool {
503 !self.signature.is_empty()
504 }
505
506 pub fn is_human_voice(&self) -> bool {
510 self.voice_kind == Some(VoiceKind::Human)
511 }
512
513 pub fn is_independent(&self) -> bool {
516 self.independence == Some(IndependenceClaim::Independent)
517 }
518
519 pub fn is_first_order_review(&self) -> bool {
522 self.review_order == Some(ReviewOrder::FirstOrder)
523 }
524
525 pub fn is_authentic_human_quorum_voice(&self) -> bool {
529 self.is_human_voice() && self.is_independent() && self.is_first_order_review()
530 }
531
532 pub fn is_synthetic(&self) -> bool {
534 self.voice_kind == Some(VoiceKind::Synthetic)
535 }
536}
537
538#[cfg(test)]
543mod tests {
544 use super::*;
545
546 fn did(s: &str) -> Did {
547 Did::new(s).expect("valid DID")
548 }
549
550 #[test]
551 fn permission_set_contains() {
552 let set = PermissionSet::new(vec![Permission::new("read"), Permission::new("write")]);
553 assert!(set.contains(&Permission::new("read")));
554 assert!(!set.contains(&Permission::new("admin")));
555 }
556
557 #[test]
558 fn permission_set_empty() {
559 let set = PermissionSet::default();
560 assert!(set.is_empty());
561 }
562
563 #[test]
564 fn bailment_state_is_active() {
565 let active = BailmentState::Active {
566 bailor: did("did:exo:bailor"),
567 bailee: did("did:exo:bailee"),
568 scope: "data".into(),
569 };
570 assert!(active.is_active());
571 assert!(!BailmentState::None.is_active());
572 assert!(!BailmentState::Terminated.is_active());
573 let suspended = BailmentState::Suspended {
574 reason: "audit".into(),
575 };
576 assert!(!suspended.is_active());
577 }
578
579 #[test]
580 fn bailment_state_authorizes_writeback_for_active_bailee_and_scope() {
581 let active = BailmentState::Active {
582 bailor: did("did:exo:bailor"),
583 bailee: did("did:exo:bailee"),
584 scope: DAGDB_WRITEBACK_SCOPE.into(),
585 };
586
587 assert!(active.authorizes_writeback("did:exo:bailee"));
588 }
589
590 #[test]
591 fn bailment_state_authorizes_writeback_rejects_wrong_bailee() {
592 let active = BailmentState::Active {
593 bailor: did("did:exo:bailor"),
594 bailee: did("did:exo:bailee"),
595 scope: DAGDB_WRITEBACK_SCOPE.into(),
596 };
597
598 assert!(!active.authorizes_writeback("did:exo:other"));
599 }
600
601 #[test]
602 fn bailment_state_authorizes_writeback_rejects_wrong_scope() {
603 let active = BailmentState::Active {
604 bailor: did("did:exo:bailor"),
605 bailee: did("did:exo:bailee"),
606 scope: "dag-db:read".into(),
607 };
608
609 assert!(!active.authorizes_writeback("did:exo:bailee"));
610 }
611
612 #[test]
613 fn bailment_state_authorizes_writeback_rejects_inactive_states() {
614 assert!(!BailmentState::None.authorizes_writeback("did:exo:bailee"));
615 assert!(!BailmentState::Terminated.authorizes_writeback("did:exo:bailee"));
616 assert!(
617 !BailmentState::Suspended {
618 reason: "audit".into(),
619 }
620 .authorizes_writeback("did:exo:bailee")
621 );
622 }
623
624 #[test]
625 fn authority_chain_empty() {
626 let chain = AuthorityChain::default();
627 assert!(chain.is_empty());
628 assert_eq!(chain.depth(), 0);
629 }
630
631 #[test]
632 fn authority_chain_depth() {
633 let chain = AuthorityChain {
634 links: vec![
635 AuthorityLink {
636 grantor: did("did:exo:root"),
637 grantee: did("did:exo:mid"),
638 permissions: PermissionSet::default(),
639 signature: vec![1],
640 grantor_public_key: None,
641 },
642 AuthorityLink {
643 grantor: did("did:exo:mid"),
644 grantee: did("did:exo:leaf"),
645 permissions: PermissionSet::default(),
646 signature: vec![2],
647 grantor_public_key: None,
648 },
649 ],
650 };
651 assert_eq!(chain.depth(), 2);
652 assert!(!chain.is_empty());
653 }
654
655 fn make_vote(voter: &str, approved: bool, sig: u8, voice: Option<VoiceKind>) -> QuorumVote {
656 QuorumVote {
657 voter: did(voter),
658 approved,
659 signature: vec![sig],
660 provenance: voice.map(|vk| Provenance {
661 actor: did(voter),
662 timestamp: "t".into(),
663 action_hash: vec![1],
664 signature: vec![sig],
665 public_key: None,
666 voice_kind: Some(vk),
667 independence: (vk == VoiceKind::Human).then_some(IndependenceClaim::Independent),
668 review_order: (vk == VoiceKind::Human).then_some(ReviewOrder::FirstOrder),
669 }),
670 }
671 }
672
673 #[test]
674 fn quorum_evidence_met() {
675 let ev = QuorumEvidence {
676 threshold: 2,
677 votes: vec![
678 QuorumVote {
679 voter: did("did:exo:v1"),
680 approved: true,
681 signature: vec![1],
682 provenance: None,
683 },
684 QuorumVote {
685 voter: did("did:exo:v2"),
686 approved: true,
687 signature: vec![2],
688 provenance: None,
689 },
690 QuorumVote {
691 voter: did("did:exo:v3"),
692 approved: false,
693 signature: vec![3],
694 provenance: None,
695 },
696 ],
697 };
698 assert!(ev.is_met());
699 }
700
701 #[test]
702 fn quorum_evidence_not_met() {
703 let ev = QuorumEvidence {
704 threshold: 3,
705 votes: vec![
706 QuorumVote {
707 voter: did("did:exo:v1"),
708 approved: true,
709 signature: vec![1],
710 provenance: None,
711 },
712 QuorumVote {
713 voter: did("did:exo:v2"),
714 approved: false,
715 signature: vec![2],
716 provenance: None,
717 },
718 ],
719 };
720 assert!(!ev.is_met());
721 }
722
723 #[test]
724 fn quorum_evidence_counts_distinct_voters_only() {
725 let ev = QuorumEvidence {
726 threshold: 2,
727 votes: vec![
728 QuorumVote {
729 voter: did("did:exo:v1"),
730 approved: true,
731 signature: vec![1],
732 provenance: None,
733 },
734 QuorumVote {
735 voter: did("did:exo:v1"),
736 approved: true,
737 signature: vec![2],
738 provenance: None,
739 },
740 ],
741 };
742 assert!(
743 !ev.is_met(),
744 "duplicate voter DIDs must not inflate raw quorum evidence"
745 );
746 }
747
748 #[test]
751 fn quorum_is_met_authentic_excludes_synthetic() {
752 let ev = QuorumEvidence {
754 threshold: 3,
755 votes: vec![
756 make_vote("did:exo:h1", true, 1, Some(VoiceKind::Human)),
757 make_vote("did:exo:h2", true, 2, Some(VoiceKind::Human)),
758 make_vote("did:exo:ai1", true, 3, Some(VoiceKind::Synthetic)),
759 ],
760 };
761 assert!(ev.is_met(), "raw count should pass (3 approvals)");
762 assert!(
763 !ev.is_met_authentic(),
764 "authentic count should fail (only 2 human)"
765 );
766 assert_eq!(ev.synthetic_vote_count(), 1);
767 }
768
769 #[test]
770 fn quorum_is_met_authentic_passes_all_human() {
771 let ev = QuorumEvidence {
772 threshold: 2,
773 votes: vec![
774 make_vote("did:exo:h1", true, 1, Some(VoiceKind::Human)),
775 make_vote("did:exo:h2", true, 2, Some(VoiceKind::Human)),
776 ],
777 };
778 assert!(ev.is_met_authentic());
779 assert_eq!(ev.synthetic_vote_count(), 0);
780 }
781
782 #[test]
783 fn quorum_is_met_authentic_counts_distinct_humans_only() {
784 let ev = QuorumEvidence {
785 threshold: 2,
786 votes: vec![
787 make_vote("did:exo:h1", true, 1, Some(VoiceKind::Human)),
788 make_vote("did:exo:h1", true, 2, Some(VoiceKind::Human)),
789 ],
790 };
791 assert!(
792 !ev.is_met_authentic(),
793 "duplicate human voter DIDs must not inflate authentic quorum evidence"
794 );
795 }
796
797 #[test]
798 fn quorum_is_met_authentic_rejects_legacy_votes_without_provenance() {
799 let ev = QuorumEvidence {
801 threshold: 2,
802 votes: vec![
803 QuorumVote {
804 voter: did("did:exo:v1"),
805 approved: true,
806 signature: vec![1],
807 provenance: None,
808 },
809 QuorumVote {
810 voter: did("did:exo:v2"),
811 approved: true,
812 signature: vec![2],
813 provenance: None,
814 },
815 ],
816 };
817 assert!(!ev.is_met_authentic());
818 }
819
820 #[test]
821 fn quorum_is_met_authentic_rejects_votes_without_human_provenance() {
822 let ev = QuorumEvidence {
823 threshold: 2,
824 votes: vec![
825 QuorumVote {
826 voter: did("did:exo:v1"),
827 approved: true,
828 signature: vec![1],
829 provenance: None,
830 },
831 make_vote("did:exo:system1", true, 2, Some(VoiceKind::System)),
832 ],
833 };
834
835 assert!(
836 !ev.is_met_authentic(),
837 "authentic quorum must not assume missing or system provenance is human"
838 );
839 }
840
841 #[test]
842 fn quorum_is_met_authentic_requires_independent_first_order_human_votes() {
843 let mut coordinated = make_vote("did:exo:h1", true, 1, Some(VoiceKind::Human));
844 coordinated
845 .provenance
846 .as_mut()
847 .expect("human provenance")
848 .independence = Some(IndependenceClaim::Coordinated);
849 coordinated
850 .provenance
851 .as_mut()
852 .expect("human provenance")
853 .review_order = Some(ReviewOrder::FirstOrder);
854
855 let mut derivative = make_vote("did:exo:h2", true, 2, Some(VoiceKind::Human));
856 derivative
857 .provenance
858 .as_mut()
859 .expect("human provenance")
860 .independence = Some(IndependenceClaim::Independent);
861 derivative
862 .provenance
863 .as_mut()
864 .expect("human provenance")
865 .review_order = Some(ReviewOrder::Derivative);
866
867 let ev = QuorumEvidence {
868 threshold: 2,
869 votes: vec![coordinated, derivative],
870 };
871
872 assert!(
873 !ev.is_met_authentic(),
874 "coordinated or derivative human claims must not count as authentic quorum"
875 );
876 }
877
878 #[test]
881 fn provenance_is_human_voice() {
882 let human_prov = Provenance {
883 actor: did("did:exo:h1"),
884 timestamp: "t".into(),
885 action_hash: vec![1],
886 signature: vec![1],
887 public_key: None,
888 voice_kind: Some(VoiceKind::Human),
889 independence: Some(IndependenceClaim::Independent),
890 review_order: Some(ReviewOrder::FirstOrder),
891 };
892 assert!(human_prov.is_human_voice());
893 assert!(human_prov.is_independent());
894 assert!(human_prov.is_first_order_review());
895 assert!(human_prov.is_authentic_human_quorum_voice());
896 assert!(!human_prov.is_synthetic());
897 }
898
899 #[test]
900 fn provenance_synthetic_not_human() {
901 let ai_prov = Provenance {
902 actor: did("did:exo:ai1"),
903 timestamp: "t".into(),
904 action_hash: vec![1],
905 signature: vec![1],
906 public_key: None,
907 voice_kind: Some(VoiceKind::Synthetic),
908 independence: None,
909 review_order: None,
910 };
911 assert!(!ai_prov.is_human_voice());
912 assert!(ai_prov.is_synthetic());
913 assert!(!ai_prov.is_independent());
914 }
915
916 #[test]
917 fn provenance_unspecified_voice_not_human() {
918 let prov = Provenance {
920 actor: did("did:exo:unknown"),
921 timestamp: "t".into(),
922 action_hash: vec![1],
923 signature: vec![1],
924 public_key: None,
925 voice_kind: None,
926 independence: None,
927 review_order: None,
928 };
929 assert!(!prov.is_human_voice());
930 assert!(!prov.is_synthetic());
931 assert!(!prov.is_independent());
932 }
933
934 #[test]
935 fn provenance_is_signed() {
936 let signed = Provenance {
937 actor: did("did:exo:actor"),
938 timestamp: "2025-01-01".into(),
939 action_hash: vec![1],
940 signature: vec![4, 5, 6],
941 public_key: None,
942 voice_kind: None,
943 independence: None,
944 review_order: None,
945 };
946 assert!(signed.is_signed());
947
948 let unsigned = Provenance {
949 actor: did("did:exo:actor"),
950 timestamp: "2025-01-01".into(),
951 action_hash: vec![1],
952 signature: vec![],
953 public_key: None,
954 voice_kind: None,
955 independence: None,
956 review_order: None,
957 };
958 assert!(!unsigned.is_signed());
959 }
960}