1use serde::{Deserialize, Serialize};
7use std::time::Duration;
8
9use crate::crypto::{Hash, PublicKey, Sig};
10use crate::error::{Error, Result};
11use crate::event::{EventId, ResourceId};
12
13use super::capability::{CapabilityId, ResourceScope};
14use super::causality::CausalContext;
15use super::principal::PrincipalId;
16use super::reasoning::ReasoningTrace;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
20pub struct ApprovalRequestId(pub [u8; 16]);
21
22impl ApprovalRequestId {
23 pub fn generate() -> Self {
25 use rand::RngCore;
26 let mut bytes = [0u8; 16];
27 rand::thread_rng().fill_bytes(&mut bytes);
28 Self(bytes)
29 }
30
31 pub fn from_bytes(bytes: [u8; 16]) -> Self {
33 Self(bytes)
34 }
35
36 pub fn as_bytes(&self) -> &[u8; 16] {
38 &self.0
39 }
40
41 pub fn to_hex(&self) -> String {
43 hex::encode(self.0)
44 }
45
46 pub fn from_hex(s: &str) -> Result<Self> {
48 let bytes = hex::decode(s).map_err(|_| Error::invalid_input("invalid hex"))?;
49 if bytes.len() != 16 {
50 return Err(Error::invalid_input("approval request ID must be 16 bytes"));
51 }
52 let mut arr = [0u8; 16];
53 arr.copy_from_slice(&bytes);
54 Ok(Self(arr))
55 }
56}
57
58impl std::fmt::Display for ApprovalRequestId {
59 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60 write!(f, "{}", self.to_hex())
61 }
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
66#[serde(rename_all = "snake_case")]
67pub enum Severity {
68 Low,
70 Medium,
72 High,
74 Critical,
76}
77
78impl Severity {
79 pub fn requires_approval(&self) -> bool {
81 matches!(self, Severity::High | Severity::Critical)
82 }
83
84 pub fn level(&self) -> u8 {
86 match self {
87 Severity::Low => 1,
88 Severity::Medium => 2,
89 Severity::High => 3,
90 Severity::Critical => 4,
91 }
92 }
93}
94
95impl std::fmt::Display for Severity {
96 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97 match self {
98 Severity::Low => write!(f, "low"),
99 Severity::Medium => write!(f, "medium"),
100 Severity::High => write!(f, "high"),
101 Severity::Critical => write!(f, "critical"),
102 }
103 }
104}
105
106#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
108pub struct Cost {
109 pub amount: u64,
111 pub currency: String,
113}
114
115impl Cost {
116 pub fn new(amount: u64, currency: impl Into<String>) -> Self {
118 Self {
119 amount,
120 currency: currency.into(),
121 }
122 }
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct ImpactAssessment {
128 severity: Severity,
130 affected_resources: Vec<ResourceId>,
132 estimated_cost: Option<Cost>,
134 risks: Vec<String>,
136}
137
138impl ImpactAssessment {
139 pub fn new(severity: Severity) -> Self {
141 Self {
142 severity,
143 affected_resources: Vec::new(),
144 estimated_cost: None,
145 risks: Vec::new(),
146 }
147 }
148
149 pub fn low() -> Self {
151 Self::new(Severity::Low)
152 }
153
154 pub fn medium() -> Self {
156 Self::new(Severity::Medium)
157 }
158
159 pub fn high() -> Self {
161 Self::new(Severity::High)
162 }
163
164 pub fn critical() -> Self {
166 Self::new(Severity::Critical)
167 }
168
169 pub fn with_resource(mut self, resource: ResourceId) -> Self {
171 self.affected_resources.push(resource);
172 self
173 }
174
175 pub fn with_resources(mut self, resources: Vec<ResourceId>) -> Self {
177 self.affected_resources = resources;
178 self
179 }
180
181 pub fn with_cost(mut self, cost: Cost) -> Self {
183 self.estimated_cost = Some(cost);
184 self
185 }
186
187 pub fn with_risk(mut self, risk: impl Into<String>) -> Self {
189 self.risks.push(risk.into());
190 self
191 }
192
193 pub fn severity(&self) -> Severity {
195 self.severity
196 }
197
198 pub fn affected_resources(&self) -> &[ResourceId] {
200 &self.affected_resources
201 }
202
203 pub fn estimated_cost(&self) -> Option<&Cost> {
205 self.estimated_cost.as_ref()
206 }
207
208 pub fn risks(&self) -> &[String] {
210 &self.risks
211 }
212
213 pub fn requires_approval(&self) -> bool {
215 self.severity.requires_approval()
216 }
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize)]
221pub struct ProposedAction {
222 action_type: String,
224 resource: ResourceId,
226 parameters: serde_json::Value,
228 reasoning: String,
230 impact: ImpactAssessment,
232 reversible: bool,
234}
235
236impl ProposedAction {
237 pub fn builder() -> ProposedActionBuilder {
239 ProposedActionBuilder::new()
240 }
241
242 pub fn action_type(&self) -> &str {
244 &self.action_type
245 }
246
247 pub fn resource(&self) -> &ResourceId {
249 &self.resource
250 }
251
252 pub fn parameters(&self) -> &serde_json::Value {
254 &self.parameters
255 }
256
257 pub fn reasoning(&self) -> &str {
259 &self.reasoning
260 }
261
262 pub fn impact(&self) -> &ImpactAssessment {
264 &self.impact
265 }
266
267 pub fn is_reversible(&self) -> bool {
269 self.reversible
270 }
271}
272
273#[derive(Debug, Default)]
275pub struct ProposedActionBuilder {
276 action_type: Option<String>,
277 resource: Option<ResourceId>,
278 parameters: serde_json::Value,
279 reasoning: Option<String>,
280 impact: Option<ImpactAssessment>,
281 reversible: bool,
282}
283
284impl ProposedActionBuilder {
285 pub fn new() -> Self {
287 Self {
288 parameters: serde_json::Value::Null,
289 ..Default::default()
290 }
291 }
292
293 pub fn action_type(mut self, action_type: impl Into<String>) -> Self {
295 self.action_type = Some(action_type.into());
296 self
297 }
298
299 pub fn resource(mut self, resource: ResourceId) -> Self {
301 self.resource = Some(resource);
302 self
303 }
304
305 pub fn parameters(mut self, parameters: serde_json::Value) -> Self {
307 self.parameters = parameters;
308 self
309 }
310
311 pub fn reasoning(mut self, reasoning: impl Into<String>) -> Self {
313 self.reasoning = Some(reasoning.into());
314 self
315 }
316
317 pub fn impact(mut self, impact: ImpactAssessment) -> Self {
319 self.impact = Some(impact);
320 self
321 }
322
323 pub fn reversible(mut self, reversible: bool) -> Self {
325 self.reversible = reversible;
326 self
327 }
328
329 pub fn build(self) -> Result<ProposedAction> {
331 let action_type = self
332 .action_type
333 .ok_or_else(|| Error::invalid_input("action_type is required"))?;
334
335 let resource = self
336 .resource
337 .ok_or_else(|| Error::invalid_input("resource is required"))?;
338
339 let reasoning = self
340 .reasoning
341 .ok_or_else(|| Error::invalid_input("reasoning is required"))?;
342
343 let impact = self.impact.unwrap_or_else(ImpactAssessment::low);
344
345 Ok(ProposedAction {
346 action_type,
347 resource,
348 parameters: self.parameters,
349 reasoning,
350 impact,
351 reversible: self.reversible,
352 })
353 }
354}
355
356#[derive(Debug, Clone, Serialize, Deserialize)]
358pub struct EscalationPolicy {
359 escalate_after_ms: u64,
361 escalate_to: Vec<PrincipalId>,
363 max_escalations: u32,
365}
366
367impl EscalationPolicy {
368 pub fn new(escalate_after: Duration, escalate_to: Vec<PrincipalId>) -> Self {
370 Self {
371 escalate_after_ms: escalate_after.as_millis() as u64,
372 escalate_to,
373 max_escalations: 3,
374 }
375 }
376
377 pub fn with_max_escalations(mut self, max: u32) -> Self {
379 self.max_escalations = max;
380 self
381 }
382
383 pub fn escalate_after(&self) -> Duration {
385 Duration::from_millis(self.escalate_after_ms)
386 }
387
388 pub fn escalate_to(&self) -> &[PrincipalId] {
390 &self.escalate_to
391 }
392
393 pub fn max_escalations(&self) -> u32 {
395 self.max_escalations
396 }
397}
398
399#[derive(Debug, Clone, Serialize, Deserialize)]
401pub struct ApprovalPolicy {
402 required_approvals: u32,
404 any_can_reject: bool,
406 auto_approve_on_timeout: bool,
408 escalation: Option<EscalationPolicy>,
410}
411
412impl ApprovalPolicy {
413 pub fn single_approver() -> Self {
415 Self {
416 required_approvals: 1,
417 any_can_reject: true,
418 auto_approve_on_timeout: false,
419 escalation: None,
420 }
421 }
422
423 pub fn multi_approver(required: u32) -> Self {
425 Self {
426 required_approvals: required,
427 any_can_reject: true,
428 auto_approve_on_timeout: false,
429 escalation: None,
430 }
431 }
432
433 pub fn with_any_can_reject(mut self, can_reject: bool) -> Self {
435 self.any_can_reject = can_reject;
436 self
437 }
438
439 pub fn with_auto_approve_on_timeout(mut self, auto_approve: bool) -> Self {
441 self.auto_approve_on_timeout = auto_approve;
442 self
443 }
444
445 pub fn with_escalation(mut self, policy: EscalationPolicy) -> Self {
447 self.escalation = Some(policy);
448 self
449 }
450
451 pub fn required_approvals(&self) -> u32 {
453 self.required_approvals
454 }
455
456 pub fn any_can_reject(&self) -> bool {
458 self.any_can_reject
459 }
460
461 pub fn auto_approve_on_timeout(&self) -> bool {
463 self.auto_approve_on_timeout
464 }
465
466 pub fn escalation(&self) -> Option<&EscalationPolicy> {
468 self.escalation.as_ref()
469 }
470}
471
472impl Default for ApprovalPolicy {
473 fn default() -> Self {
474 Self::single_approver()
475 }
476}
477
478#[derive(Debug, Clone, Default, Serialize, Deserialize)]
480pub struct ActionModifications {
481 pub parameters: Option<serde_json::Value>,
483 pub constraints: Vec<String>,
485 pub scope: Option<ResourceScope>,
487 pub instructions: Option<String>,
489}
490
491impl ActionModifications {
492 pub fn new() -> Self {
494 Self::default()
495 }
496
497 pub fn with_parameters(mut self, params: serde_json::Value) -> Self {
499 self.parameters = Some(params);
500 self
501 }
502
503 pub fn with_constraint(mut self, constraint: impl Into<String>) -> Self {
505 self.constraints.push(constraint.into());
506 self
507 }
508
509 pub fn with_scope(mut self, scope: ResourceScope) -> Self {
511 self.scope = Some(scope);
512 self
513 }
514
515 pub fn with_instructions(mut self, instructions: impl Into<String>) -> Self {
517 self.instructions = Some(instructions.into());
518 self
519 }
520
521 pub fn has_modifications(&self) -> bool {
523 self.parameters.is_some()
524 || !self.constraints.is_empty()
525 || self.scope.is_some()
526 || self.instructions.is_some()
527 }
528}
529
530#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
532#[serde(tag = "type", rename_all = "snake_case")]
533pub enum CancellationActor {
534 Principal(PrincipalId),
536 Agent(PublicKey),
538 System,
540}
541
542#[derive(Debug, Clone, Serialize, Deserialize)]
544#[serde(tag = "status", rename_all = "snake_case")]
545pub enum ApprovalStatus {
546 Pending,
548 Approved {
550 approver: PrincipalId,
551 approved_at: i64,
552 modifications: Option<ActionModifications>,
553 },
554 Rejected {
556 rejector: PrincipalId,
557 rejected_at: i64,
558 reason: String,
559 },
560 Expired,
562 Escalated {
564 escalated_to: Vec<PrincipalId>,
565 escalated_at: i64,
566 escalation_level: u32,
567 },
568 Cancelled {
570 cancelled_by: CancellationActor,
571 reason: String,
572 },
573}
574
575impl ApprovalStatus {
576 pub fn is_pending(&self) -> bool {
578 matches!(self, ApprovalStatus::Pending)
579 }
580
581 pub fn is_approved(&self) -> bool {
583 matches!(self, ApprovalStatus::Approved { .. })
584 }
585
586 pub fn is_rejected(&self) -> bool {
588 matches!(self, ApprovalStatus::Rejected { .. })
589 }
590
591 pub fn is_expired(&self) -> bool {
593 matches!(self, ApprovalStatus::Expired)
594 }
595
596 pub fn is_escalated(&self) -> bool {
598 matches!(self, ApprovalStatus::Escalated { .. })
599 }
600
601 pub fn is_cancelled(&self) -> bool {
603 matches!(self, ApprovalStatus::Cancelled { .. })
604 }
605
606 pub fn is_resolved(&self) -> bool {
608 !self.is_pending() && !self.is_escalated()
609 }
610
611 pub fn modifications(&self) -> Option<&ActionModifications> {
613 match self {
614 ApprovalStatus::Approved { modifications, .. } => modifications.as_ref(),
615 _ => None,
616 }
617 }
618}
619
620#[derive(Debug, Clone, Serialize, Deserialize)]
622pub struct ApprovalContext {
623 pub causal_context: CausalContext,
625 pub agent_attestation_hash: Hash,
627 pub capability_id: CapabilityId,
629 pub similar_actions: Vec<EventId>,
631 pub reasoning_trace: Option<ReasoningTrace>,
633}
634
635impl ApprovalContext {
636 pub fn new(
638 causal_context: CausalContext,
639 agent_attestation_hash: Hash,
640 capability_id: CapabilityId,
641 ) -> Self {
642 Self {
643 causal_context,
644 agent_attestation_hash,
645 capability_id,
646 similar_actions: Vec::new(),
647 reasoning_trace: None,
648 }
649 }
650
651 pub fn with_similar_actions(mut self, actions: Vec<EventId>) -> Self {
653 self.similar_actions = actions;
654 self
655 }
656
657 pub fn with_reasoning_trace(mut self, trace: ReasoningTrace) -> Self {
659 self.reasoning_trace = Some(trace);
660 self
661 }
662}
663
664#[derive(Debug, Clone, Serialize, Deserialize)]
666pub struct ApprovalRequest {
667 id: ApprovalRequestId,
669 proposed_action: ProposedAction,
671 requestor: PublicKey,
673 approvers: Vec<PrincipalId>,
675 policy: ApprovalPolicy,
677 created_at: i64,
679 expires_at: i64,
681 status: ApprovalStatus,
683 context: ApprovalContext,
685 escalation_level: u32,
687 collected_approvals: Vec<(PrincipalId, i64)>,
689}
690
691impl ApprovalRequest {
692 pub fn new(
694 proposed_action: ProposedAction,
695 requestor: PublicKey,
696 approvers: Vec<PrincipalId>,
697 policy: ApprovalPolicy,
698 timeout: Duration,
699 context: ApprovalContext,
700 ) -> Self {
701 let now = chrono::Utc::now().timestamp_millis();
702 Self {
703 id: ApprovalRequestId::generate(),
704 proposed_action,
705 requestor,
706 approvers,
707 policy,
708 created_at: now,
709 expires_at: now + timeout.as_millis() as i64,
710 status: ApprovalStatus::Pending,
711 context,
712 escalation_level: 0,
713 collected_approvals: Vec::new(),
714 }
715 }
716
717 pub fn id(&self) -> ApprovalRequestId {
719 self.id
720 }
721
722 pub fn proposed_action(&self) -> &ProposedAction {
724 &self.proposed_action
725 }
726
727 pub fn requestor(&self) -> &PublicKey {
729 &self.requestor
730 }
731
732 pub fn approvers(&self) -> &[PrincipalId] {
734 &self.approvers
735 }
736
737 pub fn policy(&self) -> &ApprovalPolicy {
739 &self.policy
740 }
741
742 pub fn created_at(&self) -> i64 {
744 self.created_at
745 }
746
747 pub fn expires_at(&self) -> i64 {
749 self.expires_at
750 }
751
752 pub fn status(&self) -> &ApprovalStatus {
754 &self.status
755 }
756
757 pub fn context(&self) -> &ApprovalContext {
759 &self.context
760 }
761
762 pub fn is_expired(&self) -> bool {
764 let now = chrono::Utc::now().timestamp_millis();
765 now >= self.expires_at
766 }
767
768 pub fn is_approved(&self) -> bool {
770 self.status.is_approved()
771 || self.collected_approvals.len() >= self.policy.required_approvals as usize
772 }
773
774 pub fn can_approve(&self, principal: &PrincipalId) -> bool {
776 self.approvers.contains(principal)
777 && !self.collected_approvals.iter().any(|(p, _)| p == principal)
778 }
779
780 pub fn apply_response(&mut self, response: &ApprovalResponse) -> Result<()> {
782 if response.request_id() != self.id {
784 return Err(Error::invalid_input("Response request_id does not match"));
785 }
786
787 if self.status.is_resolved() {
789 return Err(Error::invalid_input("Request is already resolved"));
790 }
791
792 if self.is_expired() {
794 self.status = ApprovalStatus::Expired;
795 return Err(Error::invalid_input("Request has expired"));
796 }
797
798 if !self.can_approve(response.responder()) {
800 return Err(Error::invalid_input(
801 "Responder is not a valid approver for this request",
802 ));
803 }
804
805 match response.decision() {
806 ApprovalDecision::Approve => {
807 self.collected_approvals
808 .push((response.responder().clone(), response.responded_at()));
809
810 if self.collected_approvals.len() >= self.policy.required_approvals as usize {
811 self.status = ApprovalStatus::Approved {
812 approver: response.responder().clone(),
813 approved_at: response.responded_at(),
814 modifications: None,
815 };
816 }
817 }
818 ApprovalDecision::ApproveWithModifications(mods) => {
819 self.collected_approvals
820 .push((response.responder().clone(), response.responded_at()));
821
822 if self.collected_approvals.len() >= self.policy.required_approvals as usize {
823 self.status = ApprovalStatus::Approved {
824 approver: response.responder().clone(),
825 approved_at: response.responded_at(),
826 modifications: Some(mods.clone()),
827 };
828 }
829 }
830 ApprovalDecision::Reject { reason } => {
831 if self.policy.any_can_reject {
832 self.status = ApprovalStatus::Rejected {
833 rejector: response.responder().clone(),
834 rejected_at: response.responded_at(),
835 reason: reason.clone(),
836 };
837 }
838 }
839 ApprovalDecision::RequestInfo { .. } => {
840 }
842 ApprovalDecision::Defer { .. } => {
843 }
845 }
846
847 Ok(())
848 }
849
850 pub fn escalate(&mut self) -> Result<()> {
852 let policy = self
853 .policy
854 .escalation
855 .as_ref()
856 .ok_or_else(|| Error::invalid_input("No escalation policy configured"))?;
857
858 if self.escalation_level >= policy.max_escalations {
859 return Err(Error::invalid_input("Maximum escalations reached"));
860 }
861
862 self.escalation_level += 1;
863 let now = chrono::Utc::now().timestamp_millis();
864
865 for target in &policy.escalate_to {
867 if !self.approvers.contains(target) {
868 self.approvers.push(target.clone());
869 }
870 }
871
872 self.status = ApprovalStatus::Escalated {
873 escalated_to: policy.escalate_to.clone(),
874 escalated_at: now,
875 escalation_level: self.escalation_level,
876 };
877
878 self.expires_at = now + policy.escalate_after_ms as i64;
880
881 Ok(())
882 }
883
884 pub fn needs_escalation(&self) -> bool {
886 if !self.status.is_pending() {
887 return false;
888 }
889
890 let policy = match &self.policy.escalation {
891 Some(p) => p,
892 None => return false,
893 };
894
895 if self.escalation_level >= policy.max_escalations {
896 return false;
897 }
898
899 let now = chrono::Utc::now().timestamp_millis();
900 let escalate_at = self.created_at + policy.escalate_after_ms as i64;
901
902 now >= escalate_at
903 }
904
905 pub fn cancel(&mut self, actor: CancellationActor, reason: impl Into<String>) {
907 self.status = ApprovalStatus::Cancelled {
908 cancelled_by: actor,
909 reason: reason.into(),
910 };
911 }
912
913 pub fn mark_expired(&mut self) {
915 if self.status.is_pending() {
916 self.status = ApprovalStatus::Expired;
917 }
918 }
919}
920
921#[derive(Debug, Clone, Serialize, Deserialize)]
923#[serde(tag = "type", rename_all = "snake_case")]
924pub enum ApprovalDecision {
925 Approve,
927 ApproveWithModifications(ActionModifications),
929 Reject { reason: String },
931 RequestInfo { questions: Vec<String> },
933 Defer { defer_to: PrincipalId },
935}
936
937impl ApprovalDecision {
938 pub fn approve() -> Self {
940 Self::Approve
941 }
942
943 pub fn approve_with_modifications(mods: ActionModifications) -> Self {
945 Self::ApproveWithModifications(mods)
946 }
947
948 pub fn reject(reason: impl Into<String>) -> Self {
950 Self::Reject {
951 reason: reason.into(),
952 }
953 }
954
955 pub fn request_info(questions: Vec<String>) -> Self {
957 Self::RequestInfo { questions }
958 }
959
960 pub fn defer(defer_to: PrincipalId) -> Self {
962 Self::Defer { defer_to }
963 }
964
965 pub fn is_approval(&self) -> bool {
967 matches!(
968 self,
969 ApprovalDecision::Approve | ApprovalDecision::ApproveWithModifications(_)
970 )
971 }
972
973 pub fn is_rejection(&self) -> bool {
975 matches!(self, ApprovalDecision::Reject { .. })
976 }
977}
978
979#[derive(Debug, Clone, Serialize, Deserialize)]
981pub struct ApprovalResponse {
982 request_id: ApprovalRequestId,
984 responder: PrincipalId,
986 decision: ApprovalDecision,
988 responded_at: i64,
990 signature: Sig,
992}
993
994impl ApprovalResponse {
995 pub fn new(
997 request_id: ApprovalRequestId,
998 responder: PrincipalId,
999 decision: ApprovalDecision,
1000 ) -> Self {
1001 Self {
1002 request_id,
1003 responder,
1004 decision,
1005 responded_at: chrono::Utc::now().timestamp_millis(),
1006 signature: Sig::empty(),
1007 }
1008 }
1009
1010 pub fn request_id(&self) -> ApprovalRequestId {
1012 self.request_id
1013 }
1014
1015 pub fn responder(&self) -> &PrincipalId {
1017 &self.responder
1018 }
1019
1020 pub fn decision(&self) -> &ApprovalDecision {
1022 &self.decision
1023 }
1024
1025 pub fn responded_at(&self) -> i64 {
1027 self.responded_at
1028 }
1029
1030 pub fn signature(&self) -> &Sig {
1032 &self.signature
1033 }
1034
1035 pub fn sign(mut self, secret_key: &crate::crypto::SecretKey) -> Self {
1037 let bytes = self.canonical_bytes();
1038 self.signature = secret_key.sign(&bytes);
1039 self
1040 }
1041
1042 pub fn canonical_bytes(&self) -> Vec<u8> {
1044 let mut data = Vec::new();
1045 data.extend_from_slice(&self.request_id.0);
1046 let responder_json = serde_json::to_vec(&self.responder).unwrap_or_default();
1047 data.extend_from_slice(&responder_json);
1048 let decision_json = serde_json::to_vec(&self.decision).unwrap_or_default();
1049 data.extend_from_slice(&decision_json);
1050 data.extend_from_slice(&self.responded_at.to_le_bytes());
1051 data
1052 }
1053
1054 pub fn verify_signature(&self, public_key: &PublicKey) -> Result<()> {
1056 let bytes = self.canonical_bytes();
1057 public_key
1058 .verify(&bytes, &self.signature)
1059 .map_err(|_| Error::invalid_input("Response signature verification failed"))
1060 }
1061}
1062
1063#[cfg(test)]
1064mod tests {
1065 use super::*;
1066 use crate::crypto::{hash, SecretKey};
1067 use crate::event::ResourceKind;
1068
1069 fn test_principal() -> PrincipalId {
1070 PrincipalId::user("alice").unwrap()
1071 }
1072
1073 fn test_approver() -> PrincipalId {
1074 PrincipalId::user("bob").unwrap()
1075 }
1076
1077 fn test_resource() -> ResourceId {
1078 ResourceId::new(ResourceKind::Repository, "org/repo")
1079 }
1080
1081 fn test_context() -> ApprovalContext {
1082 let session_id = super::super::session::SessionId::random();
1083 let event_id = EventId(hash(b"event"));
1084 let causal = CausalContext::root(event_id, session_id, test_principal());
1085
1086 ApprovalContext::new(causal, hash(b"attestation"), CapabilityId::generate())
1087 }
1088
1089 fn test_proposed_action() -> ProposedAction {
1090 ProposedAction::builder()
1091 .action_type("delete_repository")
1092 .resource(test_resource())
1093 .reasoning("User requested deletion")
1094 .impact(ImpactAssessment::high())
1095 .reversible(false)
1096 .build()
1097 .unwrap()
1098 }
1099
1100 #[test]
1103 fn approval_request_id_generates_unique() {
1104 let id1 = ApprovalRequestId::generate();
1105 let id2 = ApprovalRequestId::generate();
1106 assert_ne!(id1, id2);
1107 }
1108
1109 #[test]
1110 fn approval_request_id_hex_roundtrip() {
1111 let id = ApprovalRequestId::generate();
1112 let hex = id.to_hex();
1113 let restored = ApprovalRequestId::from_hex(&hex).unwrap();
1114 assert_eq!(id, restored);
1115 }
1116
1117 #[test]
1120 fn severity_requires_approval_for_high_and_critical() {
1121 assert!(!Severity::Low.requires_approval());
1122 assert!(!Severity::Medium.requires_approval());
1123 assert!(Severity::High.requires_approval());
1124 assert!(Severity::Critical.requires_approval());
1125 }
1126
1127 #[test]
1128 fn severity_levels_ordered() {
1129 assert!(Severity::Low.level() < Severity::Medium.level());
1130 assert!(Severity::Medium.level() < Severity::High.level());
1131 assert!(Severity::High.level() < Severity::Critical.level());
1132 }
1133
1134 #[test]
1137 fn impact_assessment_requires_approval() {
1138 let low = ImpactAssessment::low();
1139 assert!(!low.requires_approval());
1140
1141 let high = ImpactAssessment::high();
1142 assert!(high.requires_approval());
1143 }
1144
1145 #[test]
1146 fn impact_assessment_with_resources_and_cost() {
1147 let impact = ImpactAssessment::medium()
1148 .with_resource(test_resource())
1149 .with_cost(Cost::new(1000, "USD"))
1150 .with_risk("Data may be lost");
1151
1152 assert_eq!(impact.affected_resources().len(), 1);
1153 assert!(impact.estimated_cost().is_some());
1154 assert_eq!(impact.risks().len(), 1);
1155 }
1156
1157 #[test]
1160 fn proposed_action_builder_requires_fields() {
1161 let result = ProposedAction::builder().build();
1162 assert!(result.is_err());
1163
1164 let result = ProposedAction::builder()
1165 .action_type("test")
1166 .resource(test_resource())
1167 .build();
1168 assert!(result.is_err()); let result = ProposedAction::builder()
1171 .action_type("test")
1172 .resource(test_resource())
1173 .reasoning("test reason")
1174 .build();
1175 assert!(result.is_ok());
1176 }
1177
1178 #[test]
1181 fn approval_policy_single_approver() {
1182 let policy = ApprovalPolicy::single_approver();
1183 assert_eq!(policy.required_approvals(), 1);
1184 assert!(policy.any_can_reject());
1185 assert!(!policy.auto_approve_on_timeout());
1186 }
1187
1188 #[test]
1189 fn approval_policy_multi_approver() {
1190 let policy = ApprovalPolicy::multi_approver(3);
1191 assert_eq!(policy.required_approvals(), 3);
1192 }
1193
1194 #[test]
1197 fn approval_request_sets_expiry() {
1198 let agent_key = SecretKey::generate();
1199 let req = ApprovalRequest::new(
1200 test_proposed_action(),
1201 agent_key.public_key(),
1202 vec![test_approver()],
1203 ApprovalPolicy::single_approver(),
1204 Duration::from_secs(300),
1205 test_context(),
1206 );
1207
1208 assert!(req.expires_at() > req.created_at());
1209 assert_eq!(req.expires_at() - req.created_at(), 300 * 1000);
1210 }
1211
1212 #[test]
1213 fn approval_request_status_initially_pending() {
1214 let agent_key = SecretKey::generate();
1215 let req = ApprovalRequest::new(
1216 test_proposed_action(),
1217 agent_key.public_key(),
1218 vec![test_approver()],
1219 ApprovalPolicy::single_approver(),
1220 Duration::from_secs(300),
1221 test_context(),
1222 );
1223
1224 assert!(req.status().is_pending());
1225 }
1226
1227 #[test]
1228 fn approval_request_includes_context() {
1229 let agent_key = SecretKey::generate();
1230 let context = test_context();
1231 let req = ApprovalRequest::new(
1232 test_proposed_action(),
1233 agent_key.public_key(),
1234 vec![test_approver()],
1235 ApprovalPolicy::single_approver(),
1236 Duration::from_secs(300),
1237 context.clone(),
1238 );
1239
1240 assert_eq!(
1241 req.context().capability_id.as_bytes(),
1242 context.capability_id.as_bytes()
1243 );
1244 }
1245
1246 #[test]
1247 fn expired_request_cannot_be_approved() {
1248 let agent_key = SecretKey::generate();
1249 let mut req = ApprovalRequest::new(
1250 test_proposed_action(),
1251 agent_key.public_key(),
1252 vec![test_approver()],
1253 ApprovalPolicy::single_approver(),
1254 Duration::from_secs(0), test_context(),
1256 );
1257
1258 std::thread::sleep(std::time::Duration::from_millis(10));
1260
1261 let response =
1262 ApprovalResponse::new(req.id(), test_approver(), ApprovalDecision::approve());
1263
1264 let result = req.apply_response(&response);
1265 assert!(result.is_err());
1266 assert!(req.status().is_expired());
1267 }
1268
1269 #[test]
1270 fn policy_required_approvals_must_be_met() {
1271 let agent_key = SecretKey::generate();
1272 let approver1 = PrincipalId::user("approver1").unwrap();
1273 let approver2 = PrincipalId::user("approver2").unwrap();
1274
1275 let mut req = ApprovalRequest::new(
1276 test_proposed_action(),
1277 agent_key.public_key(),
1278 vec![approver1.clone(), approver2.clone()],
1279 ApprovalPolicy::multi_approver(2),
1280 Duration::from_secs(300),
1281 test_context(),
1282 );
1283
1284 let response1 = ApprovalResponse::new(req.id(), approver1, ApprovalDecision::approve());
1286 req.apply_response(&response1).unwrap();
1287 assert!(!req.is_approved());
1288
1289 let response2 = ApprovalResponse::new(req.id(), approver2, ApprovalDecision::approve());
1291 req.apply_response(&response2).unwrap();
1292 assert!(req.is_approved());
1293 }
1294
1295 #[test]
1296 fn policy_any_can_reject() {
1297 let agent_key = SecretKey::generate();
1298 let approver1 = PrincipalId::user("approver1").unwrap();
1299 let approver2 = PrincipalId::user("approver2").unwrap();
1300
1301 let mut req = ApprovalRequest::new(
1302 test_proposed_action(),
1303 agent_key.public_key(),
1304 vec![approver1.clone(), approver2],
1305 ApprovalPolicy::multi_approver(2).with_any_can_reject(true),
1306 Duration::from_secs(300),
1307 test_context(),
1308 );
1309
1310 let response =
1312 ApprovalResponse::new(req.id(), approver1, ApprovalDecision::reject("Not allowed"));
1313 req.apply_response(&response).unwrap();
1314 assert!(req.status().is_rejected());
1315 }
1316
1317 #[test]
1320 fn response_must_reference_existing_request() {
1321 let agent_key = SecretKey::generate();
1322 let mut req = ApprovalRequest::new(
1323 test_proposed_action(),
1324 agent_key.public_key(),
1325 vec![test_approver()],
1326 ApprovalPolicy::single_approver(),
1327 Duration::from_secs(300),
1328 test_context(),
1329 );
1330
1331 let wrong_id = ApprovalRequestId::generate();
1333 let response =
1334 ApprovalResponse::new(wrong_id, test_approver(), ApprovalDecision::approve());
1335
1336 let result = req.apply_response(&response);
1337 assert!(result.is_err());
1338 }
1339
1340 #[test]
1341 fn response_must_be_from_valid_approver() {
1342 let agent_key = SecretKey::generate();
1343 let mut req = ApprovalRequest::new(
1344 test_proposed_action(),
1345 agent_key.public_key(),
1346 vec![test_approver()], ApprovalPolicy::single_approver(),
1348 Duration::from_secs(300),
1349 test_context(),
1350 );
1351
1352 let non_approver = PrincipalId::user("charlie").unwrap();
1354 let response = ApprovalResponse::new(req.id(), non_approver, ApprovalDecision::approve());
1355
1356 let result = req.apply_response(&response);
1357 assert!(result.is_err());
1358 }
1359
1360 #[test]
1361 fn response_signature_must_verify() {
1362 let approver_key = SecretKey::generate();
1363 let req_id = ApprovalRequestId::generate();
1364
1365 let response = ApprovalResponse::new(req_id, test_approver(), ApprovalDecision::approve())
1366 .sign(&approver_key);
1367
1368 assert!(response
1370 .verify_signature(&approver_key.public_key())
1371 .is_ok());
1372
1373 let wrong_key = SecretKey::generate();
1375 assert!(response.verify_signature(&wrong_key.public_key()).is_err());
1376 }
1377
1378 #[test]
1379 fn approve_with_modifications_recorded() {
1380 let agent_key = SecretKey::generate();
1381 let mut req = ApprovalRequest::new(
1382 test_proposed_action(),
1383 agent_key.public_key(),
1384 vec![test_approver()],
1385 ApprovalPolicy::single_approver(),
1386 Duration::from_secs(300),
1387 test_context(),
1388 );
1389
1390 let mods = ActionModifications::new()
1391 .with_parameters(serde_json::json!({"limit": 100}))
1392 .with_constraint("Must complete within 1 hour");
1393
1394 let response = ApprovalResponse::new(
1395 req.id(),
1396 test_approver(),
1397 ApprovalDecision::approve_with_modifications(mods),
1398 );
1399
1400 req.apply_response(&response).unwrap();
1401
1402 assert!(req.status().is_approved());
1403 let modifications = req.status().modifications().unwrap();
1404 assert!(modifications.parameters.is_some());
1405 assert_eq!(modifications.constraints.len(), 1);
1406 }
1407
1408 #[test]
1411 fn escalation_adds_escalation_targets() {
1412 let agent_key = SecretKey::generate();
1413 let supervisor = PrincipalId::user("supervisor").unwrap();
1414
1415 let policy = ApprovalPolicy::single_approver().with_escalation(EscalationPolicy::new(
1416 Duration::from_secs(60),
1417 vec![supervisor.clone()],
1418 ));
1419
1420 let mut req = ApprovalRequest::new(
1421 test_proposed_action(),
1422 agent_key.public_key(),
1423 vec![test_approver()],
1424 policy,
1425 Duration::from_secs(300),
1426 test_context(),
1427 );
1428
1429 req.escalate().unwrap();
1430
1431 assert!(req.status().is_escalated());
1432 assert!(req.approvers().contains(&supervisor));
1433 }
1434
1435 #[test]
1436 fn max_escalations_respected() {
1437 let agent_key = SecretKey::generate();
1438 let supervisor = PrincipalId::user("supervisor").unwrap();
1439
1440 let policy = ApprovalPolicy::single_approver().with_escalation(
1441 EscalationPolicy::new(Duration::from_secs(60), vec![supervisor])
1442 .with_max_escalations(2),
1443 );
1444
1445 let mut req = ApprovalRequest::new(
1446 test_proposed_action(),
1447 agent_key.public_key(),
1448 vec![test_approver()],
1449 policy,
1450 Duration::from_secs(300),
1451 test_context(),
1452 );
1453
1454 assert!(req.escalate().is_ok());
1456
1457 req.status = ApprovalStatus::Pending; assert!(req.escalate().is_ok());
1460
1461 req.status = ApprovalStatus::Pending;
1463 assert!(req.escalate().is_err());
1464 }
1465
1466 #[test]
1469 fn action_modifications_has_modifications() {
1470 let empty = ActionModifications::new();
1471 assert!(!empty.has_modifications());
1472
1473 let with_params = ActionModifications::new().with_parameters(serde_json::json!({}));
1474 assert!(with_params.has_modifications());
1475
1476 let with_constraint = ActionModifications::new().with_constraint("test");
1477 assert!(with_constraint.has_modifications());
1478 }
1479
1480 #[test]
1483 fn approval_decision_is_approval() {
1484 assert!(ApprovalDecision::approve().is_approval());
1485 assert!(
1486 ApprovalDecision::approve_with_modifications(ActionModifications::new()).is_approval()
1487 );
1488 assert!(!ApprovalDecision::reject("no").is_approval());
1489 }
1490
1491 #[test]
1492 fn approval_decision_is_rejection() {
1493 assert!(!ApprovalDecision::approve().is_rejection());
1494 assert!(ApprovalDecision::reject("no").is_rejection());
1495 }
1496
1497 #[test]
1500 fn approval_response_accessed_through_methods() {
1501 let req_id = ApprovalRequestId::generate();
1502 let principal = test_principal();
1503
1504 let response =
1505 ApprovalResponse::new(req_id, principal.clone(), ApprovalDecision::approve());
1506
1507 assert_eq!(response.request_id(), req_id);
1508 assert_eq!(response.responder(), &principal);
1509 assert!(response.decision().is_approval());
1510 assert!(response.responded_at() > 0);
1511 }
1512
1513 #[test]
1514 fn approval_response_signature_accessor() {
1515 let key = SecretKey::generate();
1516 let req_id = ApprovalRequestId::generate();
1517 let principal = test_principal();
1518
1519 let response = ApprovalResponse::new(req_id, principal, ApprovalDecision::approve());
1520 let signed = response.sign(&key);
1521
1522 assert!(!signed.signature().is_empty());
1524 }
1525
1526 #[test]
1527 fn approval_response_rejection_via_accessor() {
1528 let req_id = ApprovalRequestId::generate();
1529 let principal = test_principal();
1530
1531 let response = ApprovalResponse::new(
1532 req_id,
1533 principal,
1534 ApprovalDecision::reject("policy violation"),
1535 );
1536
1537 assert!(response.decision().is_rejection());
1538 assert!(!response.decision().is_approval());
1539 }
1540}