Skip to main content

moloch_core/agent/
emergency.rs

1//! Emergency controls for rapid intervention when agent behavior is problematic.
2//!
3//! Emergency controls answer: "How do we stop this?"
4
5use 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
15/// Duration in milliseconds.
16pub type DurationMs = i64;
17
18/// An emergency control action.
19#[derive(Debug, Clone, Serialize, Deserialize)]
20#[serde(tag = "type", rename_all = "snake_case")]
21pub enum EmergencyAction {
22    /// Immediately suspend an agent.
23    SuspendAgent {
24        /// Agent to suspend.
25        agent: PublicKey,
26        /// Reason for suspension.
27        reason: String,
28        /// Duration of suspension (None = indefinite).
29        duration: Option<DurationMs>,
30        /// Scope of suspension.
31        scope: SuspensionScope,
32    },
33    /// Permanently revoke agent credentials.
34    RevokeAgent {
35        /// Agent to revoke.
36        agent: PublicKey,
37        /// Reason for revocation.
38        reason: String,
39    },
40    /// Kill an active session.
41    TerminateSession {
42        /// Session to terminate.
43        session_id: SessionId,
44        /// Reason for termination.
45        reason: String,
46    },
47    /// Revoke a specific capability.
48    RevokeCapability {
49        /// Capability to revoke.
50        capability_id: CapabilityId,
51        /// Reason for revocation.
52        reason: String,
53    },
54    /// Block access to a resource.
55    BlockResource {
56        /// Resource to block.
57        resource: ResourceId,
58        /// Actors blocked from the resource.
59        blocked_actors: Vec<PublicKey>,
60        /// Reason for blocking.
61        reason: String,
62        /// Duration of block (None = indefinite).
63        duration: Option<DurationMs>,
64    },
65    /// Global pause on all agent actions.
66    GlobalPause {
67        /// Reason for global pause.
68        reason: String,
69        /// Duration of pause.
70        duration: DurationMs,
71        /// Agents exempt from pause.
72        exceptions: Vec<PublicKey>,
73    },
74    /// Rollback actions from an agent.
75    RollbackActions {
76        /// Agent whose actions to rollback.
77        agent: PublicKey,
78        /// Rollback all actions since this time (Unix timestamp ms).
79        since: i64,
80        /// Reason for rollback.
81        reason: String,
82    },
83}
84
85impl EmergencyAction {
86    /// Create a suspend agent action.
87    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    /// Create a revoke agent action.
102    pub fn revoke_agent(agent: PublicKey, reason: impl Into<String>) -> Self {
103        Self::RevokeAgent {
104            agent,
105            reason: reason.into(),
106        }
107    }
108
109    /// Create a terminate session action.
110    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    /// Create a revoke capability action.
118    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    /// Create a block resource action.
126    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    /// Create a global pause action.
141    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    /// Create a rollback actions action.
154    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    /// Get the reason for this emergency action.
163    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    /// Check if this action affects a specific agent.
176    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    /// Check if this is a permanent action (no duration/indefinite).
188    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, // Always has duration
196            EmergencyAction::RollbackActions { .. } => true,
197        }
198    }
199}
200
201/// Scope of a suspension.
202#[derive(Debug, Clone, Serialize, Deserialize)]
203#[serde(tag = "type", rename_all = "snake_case")]
204pub enum SuspensionScope {
205    /// All actions suspended.
206    Full,
207    /// Only specific capabilities suspended.
208    Capabilities(Vec<CapabilityKind>),
209    /// Only specific resources blocked.
210    Resources(Vec<ResourceId>),
211}
212
213impl Default for SuspensionScope {
214    fn default() -> Self {
215        Self::Full
216    }
217}
218
219impl SuspensionScope {
220    /// Create a full suspension.
221    pub fn full() -> Self {
222        Self::Full
223    }
224
225    /// Create a capability-limited suspension.
226    pub fn capabilities(capabilities: Vec<CapabilityKind>) -> Self {
227        Self::Capabilities(capabilities)
228    }
229
230    /// Create a resource-limited suspension.
231    pub fn resources(resources: Vec<ResourceId>) -> Self {
232        Self::Resources(resources)
233    }
234
235    /// Check if this scope includes a capability.
236    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    /// Check if this scope includes a resource.
245    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/// Priority level of an emergency.
255#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
256#[serde(rename_all = "snake_case")]
257pub enum EmergencyPriority {
258    /// Respond within hours.
259    Low,
260    /// Respond within minutes.
261    Medium,
262    /// Respond immediately.
263    High,
264    /// Stop everything now.
265    Critical,
266}
267
268impl EmergencyPriority {
269    /// Get the expected response time in milliseconds.
270    pub fn expected_response_ms(&self) -> i64 {
271        match self {
272            EmergencyPriority::Low => 60 * 60 * 1000,   // 1 hour
273            EmergencyPriority::Medium => 5 * 60 * 1000, // 5 minutes
274            EmergencyPriority::High => 60 * 1000,       // 1 minute
275            EmergencyPriority::Critical => 10 * 1000,   // 10 seconds
276        }
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/// Event recording an emergency action.
292#[derive(Debug, Clone, Serialize, Deserialize)]
293pub struct EmergencyEvent {
294    /// The emergency action taken.
295    action: EmergencyAction,
296    /// Who initiated the emergency action.
297    initiator: PrincipalId,
298    /// Priority level of the emergency.
299    priority: EmergencyPriority,
300    /// Evidence triggering the emergency.
301    trigger_evidence: Vec<EventId>,
302    /// When the emergency was declared (Unix timestamp ms).
303    declared_at: i64,
304    /// Expected resolution time (Unix timestamp ms).
305    expected_resolution: Option<i64>,
306    /// Notification list.
307    notify: Vec<PrincipalId>,
308}
309
310impl EmergencyEvent {
311    /// Create a new emergency event builder.
312    pub fn builder() -> EmergencyEventBuilder {
313        EmergencyEventBuilder::new()
314    }
315
316    /// Get the action.
317    pub fn action(&self) -> &EmergencyAction {
318        &self.action
319    }
320
321    /// Get the initiator.
322    pub fn initiator(&self) -> &PrincipalId {
323        &self.initiator
324    }
325
326    /// Get the priority.
327    pub fn priority(&self) -> EmergencyPriority {
328        self.priority
329    }
330
331    /// Get the trigger evidence.
332    pub fn trigger_evidence(&self) -> &[EventId] {
333        &self.trigger_evidence
334    }
335
336    /// Get the declaration time.
337    pub fn declared_at(&self) -> i64 {
338        self.declared_at
339    }
340
341    /// Get the expected resolution time.
342    pub fn expected_resolution(&self) -> Option<i64> {
343        self.expected_resolution
344    }
345
346    /// Get the notification list.
347    pub fn notify(&self) -> &[PrincipalId] {
348        &self.notify
349    }
350
351    /// Check if this emergency requires immediate response.
352    pub fn is_critical(&self) -> bool {
353        self.priority == EmergencyPriority::Critical
354    }
355
356    /// Check if the expected response time has passed.
357    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/// Builder for EmergencyEvent.
365#[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    /// Create a new builder.
378    pub fn new() -> Self {
379        Self::default()
380    }
381
382    /// Set the action.
383    pub fn action(mut self, action: EmergencyAction) -> Self {
384        self.action = Some(action);
385        self
386    }
387
388    /// Set the initiator.
389    pub fn initiator(mut self, initiator: PrincipalId) -> Self {
390        self.initiator = Some(initiator);
391        self
392    }
393
394    /// Set the priority.
395    pub fn priority(mut self, priority: EmergencyPriority) -> Self {
396        self.priority = Some(priority);
397        self
398    }
399
400    /// Add trigger evidence.
401    pub fn trigger_evidence(mut self, evidence: EventId) -> Self {
402        self.trigger_evidence.push(evidence);
403        self
404    }
405
406    /// Set the declaration time.
407    pub fn declared_at(mut self, timestamp: i64) -> Self {
408        self.declared_at = Some(timestamp);
409        self
410    }
411
412    /// Set declared to now.
413    pub fn declared_now(mut self) -> Self {
414        self.declared_at = Some(chrono::Utc::now().timestamp_millis());
415        self
416    }
417
418    /// Set the expected resolution time.
419    pub fn expected_resolution(mut self, timestamp: i64) -> Self {
420        self.expected_resolution = Some(timestamp);
421        self
422    }
423
424    /// Add a principal to notify.
425    pub fn notify(mut self, principal: PrincipalId) -> Self {
426        self.notify.push(principal);
427        self
428    }
429
430    /// Build the emergency event.
431    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/// Resolution of an emergency.
456#[derive(Debug, Clone, Serialize, Deserialize)]
457pub struct EmergencyResolution {
458    /// The emergency event being resolved.
459    emergency_event_id: EventId,
460    /// Resolution action.
461    resolution: Resolution,
462    /// Who resolved it.
463    resolver: PrincipalId,
464    /// When it was resolved (Unix timestamp ms).
465    resolved_at: i64,
466    /// Post-mortem analysis.
467    post_mortem: Option<PostMortem>,
468}
469
470impl EmergencyResolution {
471    /// Create a new resolution.
472    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    /// Add a post-mortem.
483    pub fn with_post_mortem(mut self, post_mortem: PostMortem) -> Self {
484        self.post_mortem = Some(post_mortem);
485        self
486    }
487
488    /// Set the resolution time.
489    pub fn with_resolved_at(mut self, timestamp: i64) -> Self {
490        self.resolved_at = timestamp;
491        self
492    }
493
494    /// Get the emergency event ID.
495    pub fn emergency_event_id(&self) -> EventId {
496        self.emergency_event_id
497    }
498
499    /// Get the resolution.
500    pub fn resolution(&self) -> &Resolution {
501        &self.resolution
502    }
503
504    /// Get the resolver.
505    pub fn resolver(&self) -> &PrincipalId {
506        &self.resolver
507    }
508
509    /// Get the resolution time.
510    pub fn resolved_at(&self) -> i64 {
511        self.resolved_at
512    }
513
514    /// Get the post-mortem.
515    pub fn post_mortem(&self) -> Option<&PostMortem> {
516        self.post_mortem.as_ref()
517    }
518
519    /// Check if this resolution indicates the emergency was a false alarm.
520    pub fn is_false_alarm(&self) -> bool {
521        matches!(self.resolution, Resolution::FalseAlarm { .. })
522    }
523
524    /// Check if restrictions are still active.
525    pub fn has_active_restrictions(&self) -> bool {
526        matches!(self.resolution, Resolution::RestrictionsActive { .. })
527    }
528}
529
530/// Resolution action for an emergency.
531#[derive(Debug, Clone, Serialize, Deserialize)]
532#[serde(tag = "type", rename_all = "snake_case")]
533pub enum Resolution {
534    /// Emergency was false alarm.
535    FalseAlarm {
536        /// Explanation of why it was a false alarm.
537        explanation: String,
538    },
539    /// Issue was fixed.
540    Fixed {
541        /// Description of the fix.
542        fix_description: String,
543    },
544    /// Agent was permanently removed.
545    AgentRemoved,
546    /// Restrictions remain in place.
547    RestrictionsActive {
548        /// When restrictions will be reviewed (Unix timestamp ms).
549        review_date: i64,
550    },
551    /// Escalated to external authority.
552    Escalated {
553        /// Authority to which it was escalated.
554        authority: String,
555    },
556}
557
558impl Resolution {
559    /// Create a false alarm resolution.
560    pub fn false_alarm(explanation: impl Into<String>) -> Self {
561        Self::FalseAlarm {
562            explanation: explanation.into(),
563        }
564    }
565
566    /// Create a fixed resolution.
567    pub fn fixed(fix_description: impl Into<String>) -> Self {
568        Self::Fixed {
569            fix_description: fix_description.into(),
570        }
571    }
572
573    /// Create an agent removed resolution.
574    pub fn agent_removed() -> Self {
575        Self::AgentRemoved
576    }
577
578    /// Create a restrictions active resolution.
579    pub fn restrictions_active(review_date: i64) -> Self {
580        Self::RestrictionsActive { review_date }
581    }
582
583    /// Create an escalated resolution.
584    pub fn escalated(authority: impl Into<String>) -> Self {
585        Self::Escalated {
586            authority: authority.into(),
587        }
588    }
589}
590
591/// Post-mortem analysis of an emergency.
592#[derive(Debug, Clone, Serialize, Deserialize)]
593pub struct PostMortem {
594    /// What happened.
595    summary: String,
596    /// Root cause.
597    root_cause: String,
598    /// Impact assessment.
599    impact: String,
600    /// Actions taken.
601    actions_taken: Vec<String>,
602    /// Preventive measures.
603    prevention: Vec<String>,
604    /// Lessons learned.
605    lessons: Vec<String>,
606}
607
608impl PostMortem {
609    /// Create a new post-mortem.
610    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    /// Add an action taken.
626    pub fn with_action_taken(mut self, action: impl Into<String>) -> Self {
627        self.actions_taken.push(action.into());
628        self
629    }
630
631    /// Add a preventive measure.
632    pub fn with_prevention(mut self, measure: impl Into<String>) -> Self {
633        self.prevention.push(measure.into());
634        self
635    }
636
637    /// Add a lesson learned.
638    pub fn with_lesson(mut self, lesson: impl Into<String>) -> Self {
639        self.lessons.push(lesson.into());
640        self
641    }
642
643    /// Get the summary.
644    pub fn summary(&self) -> &str {
645        &self.summary
646    }
647
648    /// Get the root cause.
649    pub fn root_cause(&self) -> &str {
650        &self.root_cause
651    }
652
653    /// Get the impact.
654    pub fn impact(&self) -> &str {
655        &self.impact
656    }
657
658    /// Get the actions taken.
659    pub fn actions_taken(&self) -> &[String] {
660        &self.actions_taken
661    }
662
663    /// Get the preventive measures.
664    pub fn prevention(&self) -> &[String] {
665        &self.prevention
666    }
667
668    /// Get the lessons learned.
669    pub fn lessons(&self) -> &[String] {
670        &self.lessons
671    }
672}
673
674/// Trigger for automatic emergency actions.
675#[derive(Debug, Clone, Serialize, Deserialize)]
676#[serde(tag = "type", rename_all = "snake_case")]
677pub enum EmergencyTrigger {
678    /// Agent exceeded rate limits excessively.
679    RateLimitViolation {
680        /// Factor by which rate limit was exceeded.
681        factor: f64,
682    },
683    /// Agent attempted unauthorized action.
684    AuthorizationViolation {
685        /// Number of unauthorized attempts.
686        attempts: u32,
687    },
688    /// Agent's attestation expired or revoked.
689    AttestationInvalid,
690    /// Agent acting outside session bounds.
691    SessionViolation,
692    /// Anomalous behavior detected.
693    AnomalyDetected {
694        /// Type of anomaly.
695        anomaly_type: String,
696        /// Anomaly score (higher = more anomalous).
697        score: f64,
698    },
699    /// Human reported issue.
700    HumanReport {
701        /// Principal who reported the issue.
702        reporter: PrincipalId,
703    },
704    /// External threat intelligence.
705    ThreatIntelligence {
706        /// Source of the intelligence.
707        source: String,
708        /// Threat identifier.
709        threat_id: String,
710    },
711}
712
713impl EmergencyTrigger {
714    /// Create a rate limit violation trigger.
715    pub fn rate_limit_violation(factor: f64) -> Self {
716        Self::RateLimitViolation { factor }
717    }
718
719    /// Create an authorization violation trigger.
720    pub fn authorization_violation(attempts: u32) -> Self {
721        Self::AuthorizationViolation { attempts }
722    }
723
724    /// Create an attestation invalid trigger.
725    pub fn attestation_invalid() -> Self {
726        Self::AttestationInvalid
727    }
728
729    /// Create a session violation trigger.
730    pub fn session_violation() -> Self {
731        Self::SessionViolation
732    }
733
734    /// Create an anomaly detected trigger.
735    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    /// Create a human report trigger.
743    pub fn human_report(reporter: PrincipalId) -> Self {
744        Self::HumanReport { reporter }
745    }
746
747    /// Create a threat intelligence trigger.
748    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    /// Get the recommended priority for this trigger.
756    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/// Entry in the suspension registry.
794#[derive(Debug, Clone)]
795pub struct SuspensionEntry {
796    /// The agent that is suspended.
797    pub agent: PublicKey,
798    /// Scope of the suspension.
799    pub scope: SuspensionScope,
800    /// When the suspension was declared (Unix ms).
801    pub suspended_at: i64,
802    /// When the suspension expires (None = indefinite).
803    pub expires_at: Option<i64>,
804    /// Reason for the suspension.
805    pub reason: String,
806}
807
808impl SuspensionEntry {
809    /// Check if this suspension is still active at the given time.
810    pub fn is_active(&self, now: i64) -> bool {
811        match self.expires_at {
812            None => true, // Indefinite
813            Some(expires) => now < expires,
814        }
815    }
816}
817
818/// Registry tracking suspended agents for runtime enforcement (G-9.1, INV-EMERG-1).
819///
820/// Nodes MUST check this registry before accepting events from agents.
821/// Events from suspended agents MUST be rejected.
822#[derive(Debug, Default)]
823pub struct SuspensionRegistry {
824    /// Active suspensions indexed by agent public key.
825    suspensions: std::collections::HashMap<PublicKey, Vec<SuspensionEntry>>,
826    /// Globally paused: if Some, only exception agents can act.
827    global_pause: Option<GlobalPauseState>,
828}
829
830/// State of a global pause.
831#[derive(Debug, Clone)]
832pub struct GlobalPauseState {
833    /// Reason for the pause.
834    pub reason: String,
835    /// When the pause expires.
836    pub expires_at: i64,
837    /// Agents exempt from the pause.
838    pub exceptions: Vec<PublicKey>,
839}
840
841impl SuspensionRegistry {
842    /// Create an empty registry.
843    pub fn new() -> Self {
844        Self::default()
845    }
846
847    /// Record a suspension.
848    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    /// Set a global pause.
868    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    /// Lift a specific agent suspension.
883    pub fn lift_suspension(&mut self, agent: &PublicKey) {
884        self.suspensions.remove(agent);
885    }
886
887    /// Lift the global pause.
888    pub fn lift_global_pause(&mut self) {
889        self.global_pause = None;
890    }
891
892    /// Check if an agent is allowed to act at the given time (Rule 9.3.3).
893    ///
894    /// Returns Ok(()) if allowed, Err with reason if suspended.
895    pub fn check_agent(&self, agent: &PublicKey, now: i64) -> Result<()> {
896        // Check global pause first (INV-EMERG-3)
897        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        // Check per-agent suspensions (INV-EMERG-1)
907        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                            // Partial suspensions are checked in permits()
919                            // but a Full suspension blocks everything
920                        }
921                    }
922                }
923            }
924        }
925
926        Ok(())
927    }
928
929    /// Check if an agent's use of a specific capability is suspended.
930    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    /// Check if an agent's access to a specific resource is blocked.
950    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    /// Prune expired entries.
965    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    // === EmergencyAction Tests ===
1010
1011    #[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()], // key is exempt
1065        );
1066        assert!(!action.is_permanent());
1067        assert!(!action.affects_agent(&key.public_key())); // exempt
1068        assert!(action.affects_agent(&other_key.public_key())); // not exempt
1069    }
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    // === SuspensionScope Tests ===
1079
1080    #[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    // === EmergencyPriority Tests ===
1105
1106    #[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    // === EmergencyEvent Tests ===
1130
1131    #[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    // === EmergencyResolution Tests ===
1183
1184    #[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; // Tomorrow
1233        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    // === EmergencyTrigger Tests ===
1242
1243    #[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    // === Builder Error Type Consistency Tests (Finding 5.1) ===
1292
1293    #[test]
1294    fn emergency_event_build_error_is_crate_error() {
1295        let result = EmergencyEvent::builder()
1296            .initiator(test_principal())
1297            .build();
1298
1299        // Should return crate::error::Error, not &'static str
1300        let err: crate::error::Error = result.unwrap_err();
1301        assert!(err.to_string().contains("action"));
1302    }
1303}