1use serde::{Deserialize, Serialize};
6
7use crate::crypto::PublicKey;
8use crate::error::{Error, Result};
9use crate::event::{EventId, ResourceId};
10
11use super::capability::{CapabilityId, CapabilityKind};
12use super::principal::PrincipalId;
13use super::session::SessionId;
14
15pub type DurationMs = i64;
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
20#[serde(tag = "type", rename_all = "snake_case")]
21pub enum EmergencyAction {
22 SuspendAgent {
24 agent: PublicKey,
26 reason: String,
28 duration: Option<DurationMs>,
30 scope: SuspensionScope,
32 },
33 RevokeAgent {
35 agent: PublicKey,
37 reason: String,
39 },
40 TerminateSession {
42 session_id: SessionId,
44 reason: String,
46 },
47 RevokeCapability {
49 capability_id: CapabilityId,
51 reason: String,
53 },
54 BlockResource {
56 resource: ResourceId,
58 blocked_actors: Vec<PublicKey>,
60 reason: String,
62 duration: Option<DurationMs>,
64 },
65 GlobalPause {
67 reason: String,
69 duration: DurationMs,
71 exceptions: Vec<PublicKey>,
73 },
74 RollbackActions {
76 agent: PublicKey,
78 since: i64,
80 reason: String,
82 },
83}
84
85impl EmergencyAction {
86 pub fn suspend_agent(
88 agent: PublicKey,
89 reason: impl Into<String>,
90 duration: Option<DurationMs>,
91 scope: SuspensionScope,
92 ) -> Self {
93 Self::SuspendAgent {
94 agent,
95 reason: reason.into(),
96 duration,
97 scope,
98 }
99 }
100
101 pub fn revoke_agent(agent: PublicKey, reason: impl Into<String>) -> Self {
103 Self::RevokeAgent {
104 agent,
105 reason: reason.into(),
106 }
107 }
108
109 pub fn terminate_session(session_id: SessionId, reason: impl Into<String>) -> Self {
111 Self::TerminateSession {
112 session_id,
113 reason: reason.into(),
114 }
115 }
116
117 pub fn revoke_capability(capability_id: CapabilityId, reason: impl Into<String>) -> Self {
119 Self::RevokeCapability {
120 capability_id,
121 reason: reason.into(),
122 }
123 }
124
125 pub fn block_resource(
127 resource: ResourceId,
128 blocked_actors: Vec<PublicKey>,
129 reason: impl Into<String>,
130 duration: Option<DurationMs>,
131 ) -> Self {
132 Self::BlockResource {
133 resource,
134 blocked_actors,
135 reason: reason.into(),
136 duration,
137 }
138 }
139
140 pub fn global_pause(
142 reason: impl Into<String>,
143 duration: DurationMs,
144 exceptions: Vec<PublicKey>,
145 ) -> Self {
146 Self::GlobalPause {
147 reason: reason.into(),
148 duration,
149 exceptions,
150 }
151 }
152
153 pub fn rollback_actions(agent: PublicKey, since: i64, reason: impl Into<String>) -> Self {
155 Self::RollbackActions {
156 agent,
157 since,
158 reason: reason.into(),
159 }
160 }
161
162 pub fn reason(&self) -> &str {
164 match self {
165 EmergencyAction::SuspendAgent { reason, .. } => reason,
166 EmergencyAction::RevokeAgent { reason, .. } => reason,
167 EmergencyAction::TerminateSession { reason, .. } => reason,
168 EmergencyAction::RevokeCapability { reason, .. } => reason,
169 EmergencyAction::BlockResource { reason, .. } => reason,
170 EmergencyAction::GlobalPause { reason, .. } => reason,
171 EmergencyAction::RollbackActions { reason, .. } => reason,
172 }
173 }
174
175 pub fn affects_agent(&self, agent: &PublicKey) -> bool {
177 match self {
178 EmergencyAction::SuspendAgent { agent: a, .. } => a == agent,
179 EmergencyAction::RevokeAgent { agent: a, .. } => a == agent,
180 EmergencyAction::BlockResource { blocked_actors, .. } => blocked_actors.contains(agent),
181 EmergencyAction::GlobalPause { exceptions, .. } => !exceptions.contains(agent),
182 EmergencyAction::RollbackActions { agent: a, .. } => a == agent,
183 _ => false,
184 }
185 }
186
187 pub fn is_permanent(&self) -> bool {
189 match self {
190 EmergencyAction::SuspendAgent { duration, .. } => duration.is_none(),
191 EmergencyAction::RevokeAgent { .. } => true,
192 EmergencyAction::TerminateSession { .. } => true,
193 EmergencyAction::RevokeCapability { .. } => true,
194 EmergencyAction::BlockResource { duration, .. } => duration.is_none(),
195 EmergencyAction::GlobalPause { .. } => false, EmergencyAction::RollbackActions { .. } => true,
197 }
198 }
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize)]
203#[serde(tag = "type", rename_all = "snake_case")]
204pub enum SuspensionScope {
205 Full,
207 Capabilities(Vec<CapabilityKind>),
209 Resources(Vec<ResourceId>),
211}
212
213impl Default for SuspensionScope {
214 fn default() -> Self {
215 Self::Full
216 }
217}
218
219impl SuspensionScope {
220 pub fn full() -> Self {
222 Self::Full
223 }
224
225 pub fn capabilities(capabilities: Vec<CapabilityKind>) -> Self {
227 Self::Capabilities(capabilities)
228 }
229
230 pub fn resources(resources: Vec<ResourceId>) -> Self {
232 Self::Resources(resources)
233 }
234
235 pub fn includes_capability(&self, capability: &CapabilityKind) -> bool {
237 match self {
238 SuspensionScope::Full => true,
239 SuspensionScope::Capabilities(caps) => caps.contains(capability),
240 SuspensionScope::Resources(_) => false,
241 }
242 }
243
244 pub fn includes_resource(&self, resource: &ResourceId) -> bool {
246 match self {
247 SuspensionScope::Full => true,
248 SuspensionScope::Capabilities(_) => false,
249 SuspensionScope::Resources(resources) => resources.contains(resource),
250 }
251 }
252}
253
254#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
256#[serde(rename_all = "snake_case")]
257pub enum EmergencyPriority {
258 Low,
260 Medium,
262 High,
264 Critical,
266}
267
268impl EmergencyPriority {
269 pub fn expected_response_ms(&self) -> i64 {
271 match self {
272 EmergencyPriority::Low => 60 * 60 * 1000, EmergencyPriority::Medium => 5 * 60 * 1000, EmergencyPriority::High => 60 * 1000, EmergencyPriority::Critical => 10 * 1000, }
277 }
278}
279
280impl std::fmt::Display for EmergencyPriority {
281 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
282 match self {
283 EmergencyPriority::Low => write!(f, "low"),
284 EmergencyPriority::Medium => write!(f, "medium"),
285 EmergencyPriority::High => write!(f, "high"),
286 EmergencyPriority::Critical => write!(f, "critical"),
287 }
288 }
289}
290
291#[derive(Debug, Clone, Serialize, Deserialize)]
293pub struct EmergencyEvent {
294 action: EmergencyAction,
296 initiator: PrincipalId,
298 priority: EmergencyPriority,
300 trigger_evidence: Vec<EventId>,
302 declared_at: i64,
304 expected_resolution: Option<i64>,
306 notify: Vec<PrincipalId>,
308}
309
310impl EmergencyEvent {
311 pub fn builder() -> EmergencyEventBuilder {
313 EmergencyEventBuilder::new()
314 }
315
316 pub fn action(&self) -> &EmergencyAction {
318 &self.action
319 }
320
321 pub fn initiator(&self) -> &PrincipalId {
323 &self.initiator
324 }
325
326 pub fn priority(&self) -> EmergencyPriority {
328 self.priority
329 }
330
331 pub fn trigger_evidence(&self) -> &[EventId] {
333 &self.trigger_evidence
334 }
335
336 pub fn declared_at(&self) -> i64 {
338 self.declared_at
339 }
340
341 pub fn expected_resolution(&self) -> Option<i64> {
343 self.expected_resolution
344 }
345
346 pub fn notify(&self) -> &[PrincipalId] {
348 &self.notify
349 }
350
351 pub fn is_critical(&self) -> bool {
353 self.priority == EmergencyPriority::Critical
354 }
355
356 pub fn is_overdue(&self) -> bool {
358 let now = chrono::Utc::now().timestamp_millis();
359 let deadline = self.declared_at + self.priority.expected_response_ms();
360 now > deadline
361 }
362}
363
364#[derive(Debug, Default)]
366pub struct EmergencyEventBuilder {
367 action: Option<EmergencyAction>,
368 initiator: Option<PrincipalId>,
369 priority: Option<EmergencyPriority>,
370 trigger_evidence: Vec<EventId>,
371 declared_at: Option<i64>,
372 expected_resolution: Option<i64>,
373 notify: Vec<PrincipalId>,
374}
375
376impl EmergencyEventBuilder {
377 pub fn new() -> Self {
379 Self::default()
380 }
381
382 pub fn action(mut self, action: EmergencyAction) -> Self {
384 self.action = Some(action);
385 self
386 }
387
388 pub fn initiator(mut self, initiator: PrincipalId) -> Self {
390 self.initiator = Some(initiator);
391 self
392 }
393
394 pub fn priority(mut self, priority: EmergencyPriority) -> Self {
396 self.priority = Some(priority);
397 self
398 }
399
400 pub fn trigger_evidence(mut self, evidence: EventId) -> Self {
402 self.trigger_evidence.push(evidence);
403 self
404 }
405
406 pub fn declared_at(mut self, timestamp: i64) -> Self {
408 self.declared_at = Some(timestamp);
409 self
410 }
411
412 pub fn declared_now(mut self) -> Self {
414 self.declared_at = Some(chrono::Utc::now().timestamp_millis());
415 self
416 }
417
418 pub fn expected_resolution(mut self, timestamp: i64) -> Self {
420 self.expected_resolution = Some(timestamp);
421 self
422 }
423
424 pub fn notify(mut self, principal: PrincipalId) -> Self {
426 self.notify.push(principal);
427 self
428 }
429
430 pub fn build(self) -> Result<EmergencyEvent> {
432 let action = self
433 .action
434 .ok_or_else(|| Error::invalid_input("action is required"))?;
435 let initiator = self
436 .initiator
437 .ok_or_else(|| Error::invalid_input("initiator is required"))?;
438 let priority = self.priority.unwrap_or(EmergencyPriority::High);
439 let declared_at = self
440 .declared_at
441 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
442
443 Ok(EmergencyEvent {
444 action,
445 initiator,
446 priority,
447 trigger_evidence: self.trigger_evidence,
448 declared_at,
449 expected_resolution: self.expected_resolution,
450 notify: self.notify,
451 })
452 }
453}
454
455#[derive(Debug, Clone, Serialize, Deserialize)]
457pub struct EmergencyResolution {
458 emergency_event_id: EventId,
460 resolution: Resolution,
462 resolver: PrincipalId,
464 resolved_at: i64,
466 post_mortem: Option<PostMortem>,
468}
469
470impl EmergencyResolution {
471 pub fn new(emergency_event_id: EventId, resolution: Resolution, resolver: PrincipalId) -> Self {
473 Self {
474 emergency_event_id,
475 resolution,
476 resolver,
477 resolved_at: chrono::Utc::now().timestamp_millis(),
478 post_mortem: None,
479 }
480 }
481
482 pub fn with_post_mortem(mut self, post_mortem: PostMortem) -> Self {
484 self.post_mortem = Some(post_mortem);
485 self
486 }
487
488 pub fn with_resolved_at(mut self, timestamp: i64) -> Self {
490 self.resolved_at = timestamp;
491 self
492 }
493
494 pub fn emergency_event_id(&self) -> EventId {
496 self.emergency_event_id
497 }
498
499 pub fn resolution(&self) -> &Resolution {
501 &self.resolution
502 }
503
504 pub fn resolver(&self) -> &PrincipalId {
506 &self.resolver
507 }
508
509 pub fn resolved_at(&self) -> i64 {
511 self.resolved_at
512 }
513
514 pub fn post_mortem(&self) -> Option<&PostMortem> {
516 self.post_mortem.as_ref()
517 }
518
519 pub fn is_false_alarm(&self) -> bool {
521 matches!(self.resolution, Resolution::FalseAlarm { .. })
522 }
523
524 pub fn has_active_restrictions(&self) -> bool {
526 matches!(self.resolution, Resolution::RestrictionsActive { .. })
527 }
528}
529
530#[derive(Debug, Clone, Serialize, Deserialize)]
532#[serde(tag = "type", rename_all = "snake_case")]
533pub enum Resolution {
534 FalseAlarm {
536 explanation: String,
538 },
539 Fixed {
541 fix_description: String,
543 },
544 AgentRemoved,
546 RestrictionsActive {
548 review_date: i64,
550 },
551 Escalated {
553 authority: String,
555 },
556}
557
558impl Resolution {
559 pub fn false_alarm(explanation: impl Into<String>) -> Self {
561 Self::FalseAlarm {
562 explanation: explanation.into(),
563 }
564 }
565
566 pub fn fixed(fix_description: impl Into<String>) -> Self {
568 Self::Fixed {
569 fix_description: fix_description.into(),
570 }
571 }
572
573 pub fn agent_removed() -> Self {
575 Self::AgentRemoved
576 }
577
578 pub fn restrictions_active(review_date: i64) -> Self {
580 Self::RestrictionsActive { review_date }
581 }
582
583 pub fn escalated(authority: impl Into<String>) -> Self {
585 Self::Escalated {
586 authority: authority.into(),
587 }
588 }
589}
590
591#[derive(Debug, Clone, Serialize, Deserialize)]
593pub struct PostMortem {
594 summary: String,
596 root_cause: String,
598 impact: String,
600 actions_taken: Vec<String>,
602 prevention: Vec<String>,
604 lessons: Vec<String>,
606}
607
608impl PostMortem {
609 pub fn new(
611 summary: impl Into<String>,
612 root_cause: impl Into<String>,
613 impact: impl Into<String>,
614 ) -> Self {
615 Self {
616 summary: summary.into(),
617 root_cause: root_cause.into(),
618 impact: impact.into(),
619 actions_taken: Vec::new(),
620 prevention: Vec::new(),
621 lessons: Vec::new(),
622 }
623 }
624
625 pub fn with_action_taken(mut self, action: impl Into<String>) -> Self {
627 self.actions_taken.push(action.into());
628 self
629 }
630
631 pub fn with_prevention(mut self, measure: impl Into<String>) -> Self {
633 self.prevention.push(measure.into());
634 self
635 }
636
637 pub fn with_lesson(mut self, lesson: impl Into<String>) -> Self {
639 self.lessons.push(lesson.into());
640 self
641 }
642
643 pub fn summary(&self) -> &str {
645 &self.summary
646 }
647
648 pub fn root_cause(&self) -> &str {
650 &self.root_cause
651 }
652
653 pub fn impact(&self) -> &str {
655 &self.impact
656 }
657
658 pub fn actions_taken(&self) -> &[String] {
660 &self.actions_taken
661 }
662
663 pub fn prevention(&self) -> &[String] {
665 &self.prevention
666 }
667
668 pub fn lessons(&self) -> &[String] {
670 &self.lessons
671 }
672}
673
674#[derive(Debug, Clone, Serialize, Deserialize)]
676#[serde(tag = "type", rename_all = "snake_case")]
677pub enum EmergencyTrigger {
678 RateLimitViolation {
680 factor: f64,
682 },
683 AuthorizationViolation {
685 attempts: u32,
687 },
688 AttestationInvalid,
690 SessionViolation,
692 AnomalyDetected {
694 anomaly_type: String,
696 score: f64,
698 },
699 HumanReport {
701 reporter: PrincipalId,
703 },
704 ThreatIntelligence {
706 source: String,
708 threat_id: String,
710 },
711}
712
713impl EmergencyTrigger {
714 pub fn rate_limit_violation(factor: f64) -> Self {
716 Self::RateLimitViolation { factor }
717 }
718
719 pub fn authorization_violation(attempts: u32) -> Self {
721 Self::AuthorizationViolation { attempts }
722 }
723
724 pub fn attestation_invalid() -> Self {
726 Self::AttestationInvalid
727 }
728
729 pub fn session_violation() -> Self {
731 Self::SessionViolation
732 }
733
734 pub fn anomaly_detected(anomaly_type: impl Into<String>, score: f64) -> Self {
736 Self::AnomalyDetected {
737 anomaly_type: anomaly_type.into(),
738 score,
739 }
740 }
741
742 pub fn human_report(reporter: PrincipalId) -> Self {
744 Self::HumanReport { reporter }
745 }
746
747 pub fn threat_intelligence(source: impl Into<String>, threat_id: impl Into<String>) -> Self {
749 Self::ThreatIntelligence {
750 source: source.into(),
751 threat_id: threat_id.into(),
752 }
753 }
754
755 pub fn recommended_priority(&self) -> EmergencyPriority {
757 match self {
758 EmergencyTrigger::RateLimitViolation { factor } => {
759 if *factor >= 10.0 {
760 EmergencyPriority::Critical
761 } else if *factor >= 5.0 {
762 EmergencyPriority::High
763 } else {
764 EmergencyPriority::Medium
765 }
766 }
767 EmergencyTrigger::AuthorizationViolation { attempts } => {
768 if *attempts >= 10 {
769 EmergencyPriority::Critical
770 } else if *attempts >= 5 {
771 EmergencyPriority::High
772 } else {
773 EmergencyPriority::Medium
774 }
775 }
776 EmergencyTrigger::AttestationInvalid => EmergencyPriority::High,
777 EmergencyTrigger::SessionViolation => EmergencyPriority::High,
778 EmergencyTrigger::AnomalyDetected { score, .. } => {
779 if *score >= 0.9 {
780 EmergencyPriority::Critical
781 } else if *score >= 0.7 {
782 EmergencyPriority::High
783 } else {
784 EmergencyPriority::Medium
785 }
786 }
787 EmergencyTrigger::HumanReport { .. } => EmergencyPriority::High,
788 EmergencyTrigger::ThreatIntelligence { .. } => EmergencyPriority::Critical,
789 }
790 }
791}
792
793#[derive(Debug, Clone)]
795pub struct SuspensionEntry {
796 pub agent: PublicKey,
798 pub scope: SuspensionScope,
800 pub suspended_at: i64,
802 pub expires_at: Option<i64>,
804 pub reason: String,
806}
807
808impl SuspensionEntry {
809 pub fn is_active(&self, now: i64) -> bool {
811 match self.expires_at {
812 None => true, Some(expires) => now < expires,
814 }
815 }
816}
817
818#[derive(Debug, Default)]
823pub struct SuspensionRegistry {
824 suspensions: std::collections::HashMap<PublicKey, Vec<SuspensionEntry>>,
826 global_pause: Option<GlobalPauseState>,
828}
829
830#[derive(Debug, Clone)]
832pub struct GlobalPauseState {
833 pub reason: String,
835 pub expires_at: i64,
837 pub exceptions: Vec<PublicKey>,
839}
840
841impl SuspensionRegistry {
842 pub fn new() -> Self {
844 Self::default()
845 }
846
847 pub fn suspend(
849 &mut self,
850 agent: PublicKey,
851 scope: SuspensionScope,
852 reason: String,
853 now: i64,
854 duration: Option<DurationMs>,
855 ) {
856 let expires_at = duration.map(|d| now.saturating_add(d));
857 let entry = SuspensionEntry {
858 agent: agent.clone(),
859 scope,
860 suspended_at: now,
861 expires_at,
862 reason,
863 };
864 self.suspensions.entry(agent).or_default().push(entry);
865 }
866
867 pub fn global_pause(
869 &mut self,
870 reason: String,
871 duration_ms: DurationMs,
872 exceptions: Vec<PublicKey>,
873 now: i64,
874 ) {
875 self.global_pause = Some(GlobalPauseState {
876 reason,
877 expires_at: now.saturating_add(duration_ms),
878 exceptions,
879 });
880 }
881
882 pub fn lift_suspension(&mut self, agent: &PublicKey) {
884 self.suspensions.remove(agent);
885 }
886
887 pub fn lift_global_pause(&mut self) {
889 self.global_pause = None;
890 }
891
892 pub fn check_agent(&self, agent: &PublicKey, now: i64) -> Result<()> {
896 if let Some(pause) = &self.global_pause {
898 if now < pause.expires_at && !pause.exceptions.contains(agent) {
899 return Err(Error::invalid_input(format!(
900 "global pause active: {}",
901 pause.reason
902 )));
903 }
904 }
905
906 if let Some(entries) = self.suspensions.get(agent) {
908 for entry in entries {
909 if entry.is_active(now) {
910 match &entry.scope {
911 SuspensionScope::Full => {
912 return Err(Error::invalid_input(format!(
913 "agent is fully suspended: {}",
914 entry.reason
915 )));
916 }
917 _ => {
918 }
921 }
922 }
923 }
924 }
925
926 Ok(())
927 }
928
929 pub fn check_capability(
931 &self,
932 agent: &PublicKey,
933 capability: &CapabilityKind,
934 now: i64,
935 ) -> Result<()> {
936 if let Some(entries) = self.suspensions.get(agent) {
937 for entry in entries {
938 if entry.is_active(now) && entry.scope.includes_capability(capability) {
939 return Err(Error::invalid_input(format!(
940 "capability suspended for agent: {}",
941 entry.reason
942 )));
943 }
944 }
945 }
946 Ok(())
947 }
948
949 pub fn check_resource(&self, agent: &PublicKey, resource: &ResourceId, now: i64) -> Result<()> {
951 if let Some(entries) = self.suspensions.get(agent) {
952 for entry in entries {
953 if entry.is_active(now) && entry.scope.includes_resource(resource) {
954 return Err(Error::invalid_input(format!(
955 "resource access blocked for agent: {}",
956 entry.reason
957 )));
958 }
959 }
960 }
961 Ok(())
962 }
963
964 pub fn prune_expired(&mut self, now: i64) {
966 for entries in self.suspensions.values_mut() {
967 entries.retain(|e| e.is_active(now));
968 }
969 self.suspensions.retain(|_, entries| !entries.is_empty());
970
971 if let Some(pause) = &self.global_pause {
972 if now >= pause.expires_at {
973 self.global_pause = None;
974 }
975 }
976 }
977}
978
979#[cfg(test)]
980mod tests {
981 use super::*;
982 use crate::crypto::{hash, SecretKey};
983 use crate::event::ResourceKind;
984
985 fn test_key() -> SecretKey {
986 SecretKey::generate()
987 }
988
989 fn test_event_id() -> EventId {
990 EventId(hash(b"test-event"))
991 }
992
993 fn test_principal() -> PrincipalId {
994 PrincipalId::user("admin@example.com").unwrap()
995 }
996
997 fn test_session_id() -> SessionId {
998 SessionId::random()
999 }
1000
1001 fn test_capability_id() -> CapabilityId {
1002 CapabilityId::generate()
1003 }
1004
1005 fn test_resource_id() -> ResourceId {
1006 ResourceId::new(ResourceKind::File, "/tmp/test.txt")
1007 }
1008
1009 #[test]
1012 fn suspend_agent_action() {
1013 let key = test_key();
1014 let action = EmergencyAction::suspend_agent(
1015 key.public_key(),
1016 "Suspicious behavior",
1017 Some(3600000),
1018 SuspensionScope::Full,
1019 );
1020 assert_eq!(action.reason(), "Suspicious behavior");
1021 assert!(action.affects_agent(&key.public_key()));
1022 assert!(!action.is_permanent());
1023 }
1024
1025 #[test]
1026 fn revoke_agent_action() {
1027 let key = test_key();
1028 let action = EmergencyAction::revoke_agent(key.public_key(), "Malicious activity");
1029 assert!(action.is_permanent());
1030 }
1031
1032 #[test]
1033 fn terminate_session_action() {
1034 let action = EmergencyAction::terminate_session(test_session_id(), "Session compromised");
1035 assert!(action.is_permanent());
1036 }
1037
1038 #[test]
1039 fn revoke_capability_action() {
1040 let action = EmergencyAction::revoke_capability(test_capability_id(), "Capability abused");
1041 assert!(action.is_permanent());
1042 }
1043
1044 #[test]
1045 fn block_resource_action() {
1046 let key = test_key();
1047 let action = EmergencyAction::block_resource(
1048 test_resource_id(),
1049 vec![key.public_key()],
1050 "Resource at risk",
1051 None,
1052 );
1053 assert!(action.is_permanent());
1054 assert!(action.affects_agent(&key.public_key()));
1055 }
1056
1057 #[test]
1058 fn global_pause_action() {
1059 let key = test_key();
1060 let other_key = test_key();
1061 let action = EmergencyAction::global_pause(
1062 "System maintenance",
1063 3600000,
1064 vec![key.public_key()], );
1066 assert!(!action.is_permanent());
1067 assert!(!action.affects_agent(&key.public_key())); assert!(action.affects_agent(&other_key.public_key())); }
1070
1071 #[test]
1072 fn rollback_actions_action() {
1073 let key = test_key();
1074 let action = EmergencyAction::rollback_actions(key.public_key(), 1000, "Undo damage");
1075 assert!(action.is_permanent());
1076 }
1077
1078 #[test]
1081 fn suspension_scope_full() {
1082 let scope = SuspensionScope::full();
1083 assert!(scope.includes_capability(&CapabilityKind::Read));
1084 assert!(scope.includes_resource(&test_resource_id()));
1085 }
1086
1087 #[test]
1088 fn suspension_scope_capabilities() {
1089 let scope =
1090 SuspensionScope::capabilities(vec![CapabilityKind::Read, CapabilityKind::Write]);
1091 assert!(scope.includes_capability(&CapabilityKind::Read));
1092 assert!(!scope.includes_capability(&CapabilityKind::Execute));
1093 assert!(!scope.includes_resource(&test_resource_id()));
1094 }
1095
1096 #[test]
1097 fn suspension_scope_resources() {
1098 let resource = test_resource_id();
1099 let scope = SuspensionScope::resources(vec![resource.clone()]);
1100 assert!(scope.includes_resource(&resource));
1101 assert!(!scope.includes_capability(&CapabilityKind::Read));
1102 }
1103
1104 #[test]
1107 fn priority_ordering() {
1108 assert!(EmergencyPriority::Low < EmergencyPriority::Medium);
1109 assert!(EmergencyPriority::Medium < EmergencyPriority::High);
1110 assert!(EmergencyPriority::High < EmergencyPriority::Critical);
1111 }
1112
1113 #[test]
1114 fn priority_response_times() {
1115 assert!(
1116 EmergencyPriority::Critical.expected_response_ms()
1117 < EmergencyPriority::High.expected_response_ms()
1118 );
1119 assert!(
1120 EmergencyPriority::High.expected_response_ms()
1121 < EmergencyPriority::Medium.expected_response_ms()
1122 );
1123 assert!(
1124 EmergencyPriority::Medium.expected_response_ms()
1125 < EmergencyPriority::Low.expected_response_ms()
1126 );
1127 }
1128
1129 #[test]
1132 fn emergency_event_build() {
1133 let key = test_key();
1134 let event = EmergencyEvent::builder()
1135 .action(EmergencyAction::suspend_agent(
1136 key.public_key(),
1137 "Test",
1138 None,
1139 SuspensionScope::Full,
1140 ))
1141 .initiator(test_principal())
1142 .priority(EmergencyPriority::High)
1143 .declared_now()
1144 .build()
1145 .unwrap();
1146
1147 assert_eq!(event.priority(), EmergencyPriority::High);
1148 assert!(!event.is_critical());
1149 }
1150
1151 #[test]
1152 fn emergency_event_critical() {
1153 let key = test_key();
1154 let event = EmergencyEvent::builder()
1155 .action(EmergencyAction::revoke_agent(key.public_key(), "Malicious"))
1156 .initiator(test_principal())
1157 .priority(EmergencyPriority::Critical)
1158 .declared_now()
1159 .build()
1160 .unwrap();
1161
1162 assert!(event.is_critical());
1163 }
1164
1165 #[test]
1166 fn emergency_event_requires_action() {
1167 let result = EmergencyEvent::builder()
1168 .initiator(test_principal())
1169 .build();
1170 assert!(result.is_err());
1171 }
1172
1173 #[test]
1174 fn emergency_event_requires_initiator() {
1175 let key = test_key();
1176 let result = EmergencyEvent::builder()
1177 .action(EmergencyAction::revoke_agent(key.public_key(), "Test"))
1178 .build();
1179 assert!(result.is_err());
1180 }
1181
1182 #[test]
1185 fn resolution_false_alarm() {
1186 let resolution = EmergencyResolution::new(
1187 test_event_id(),
1188 Resolution::false_alarm("Misconfigured alert"),
1189 test_principal(),
1190 );
1191 assert!(resolution.is_false_alarm());
1192 assert!(!resolution.has_active_restrictions());
1193 }
1194
1195 #[test]
1196 fn resolution_fixed() {
1197 let resolution = EmergencyResolution::new(
1198 test_event_id(),
1199 Resolution::fixed("Patched vulnerability"),
1200 test_principal(),
1201 );
1202 assert!(!resolution.is_false_alarm());
1203 }
1204
1205 #[test]
1206 fn resolution_with_post_mortem() {
1207 let post_mortem = PostMortem::new(
1208 "Agent exceeded rate limits",
1209 "Misconfigured retry logic",
1210 "Minor service degradation",
1211 )
1212 .with_action_taken("Disabled agent")
1213 .with_prevention("Add rate limiting at client level")
1214 .with_lesson("Monitor retry patterns");
1215
1216 let resolution = EmergencyResolution::new(
1217 test_event_id(),
1218 Resolution::fixed("Fixed retry logic"),
1219 test_principal(),
1220 )
1221 .with_post_mortem(post_mortem);
1222
1223 assert!(resolution.post_mortem().is_some());
1224 let pm = resolution.post_mortem().unwrap();
1225 assert_eq!(pm.actions_taken().len(), 1);
1226 assert_eq!(pm.prevention().len(), 1);
1227 assert_eq!(pm.lessons().len(), 1);
1228 }
1229
1230 #[test]
1231 fn resolution_restrictions_active() {
1232 let review_date = chrono::Utc::now().timestamp_millis() + 86400000; let resolution = EmergencyResolution::new(
1234 test_event_id(),
1235 Resolution::restrictions_active(review_date),
1236 test_principal(),
1237 );
1238 assert!(resolution.has_active_restrictions());
1239 }
1240
1241 #[test]
1244 fn trigger_rate_limit_priority() {
1245 let low = EmergencyTrigger::rate_limit_violation(2.0);
1246 assert_eq!(low.recommended_priority(), EmergencyPriority::Medium);
1247
1248 let high = EmergencyTrigger::rate_limit_violation(5.0);
1249 assert_eq!(high.recommended_priority(), EmergencyPriority::High);
1250
1251 let critical = EmergencyTrigger::rate_limit_violation(10.0);
1252 assert_eq!(critical.recommended_priority(), EmergencyPriority::Critical);
1253 }
1254
1255 #[test]
1256 fn trigger_authorization_violation_priority() {
1257 let low = EmergencyTrigger::authorization_violation(2);
1258 assert_eq!(low.recommended_priority(), EmergencyPriority::Medium);
1259
1260 let high = EmergencyTrigger::authorization_violation(5);
1261 assert_eq!(high.recommended_priority(), EmergencyPriority::High);
1262
1263 let critical = EmergencyTrigger::authorization_violation(10);
1264 assert_eq!(critical.recommended_priority(), EmergencyPriority::Critical);
1265 }
1266
1267 #[test]
1268 fn trigger_anomaly_priority() {
1269 let medium = EmergencyTrigger::anomaly_detected("unusual_pattern", 0.5);
1270 assert_eq!(medium.recommended_priority(), EmergencyPriority::Medium);
1271
1272 let high = EmergencyTrigger::anomaly_detected("unusual_pattern", 0.7);
1273 assert_eq!(high.recommended_priority(), EmergencyPriority::High);
1274
1275 let critical = EmergencyTrigger::anomaly_detected("unusual_pattern", 0.9);
1276 assert_eq!(critical.recommended_priority(), EmergencyPriority::Critical);
1277 }
1278
1279 #[test]
1280 fn trigger_threat_intelligence_always_critical() {
1281 let trigger = EmergencyTrigger::threat_intelligence("threat-feed", "CVE-2024-1234");
1282 assert_eq!(trigger.recommended_priority(), EmergencyPriority::Critical);
1283 }
1284
1285 #[test]
1286 fn trigger_human_report() {
1287 let trigger = EmergencyTrigger::human_report(test_principal());
1288 assert_eq!(trigger.recommended_priority(), EmergencyPriority::High);
1289 }
1290
1291 #[test]
1294 fn emergency_event_build_error_is_crate_error() {
1295 let result = EmergencyEvent::builder()
1296 .initiator(test_principal())
1297 .build();
1298
1299 let err: crate::error::Error = result.unwrap_err();
1301 assert!(err.to_string().contains("action"));
1302 }
1303}