Skip to main content

moloch_core/agent/
hitl.rs

1//! Human-in-the-Loop (HITL) protocol types.
2//!
3//! The HITL protocol ensures human oversight of agent actions. It answers:
4//! "Did a human approve this?" and "Can a human intervene?"
5
6use 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/// Unique approval request identifier.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
20pub struct ApprovalRequestId(pub [u8; 16]);
21
22impl ApprovalRequestId {
23    /// Generate a new random approval request ID.
24    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    /// Create from bytes.
32    pub fn from_bytes(bytes: [u8; 16]) -> Self {
33        Self(bytes)
34    }
35
36    /// Get the bytes.
37    pub fn as_bytes(&self) -> &[u8; 16] {
38        &self.0
39    }
40
41    /// Convert to hex string.
42    pub fn to_hex(&self) -> String {
43        hex::encode(self.0)
44    }
45
46    /// Parse from hex string.
47    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/// Severity level for impact assessment.
65#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
66#[serde(rename_all = "snake_case")]
67pub enum Severity {
68    /// Informational, easily reversible.
69    Low,
70    /// Moderate impact, reversible with effort.
71    Medium,
72    /// Significant impact, difficult to reverse.
73    High,
74    /// Irreversible or high-stakes.
75    Critical,
76}
77
78impl Severity {
79    /// Check if this severity requires approval.
80    pub fn requires_approval(&self) -> bool {
81        matches!(self, Severity::High | Severity::Critical)
82    }
83
84    /// Get the numeric level (for comparison).
85    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/// Cost representation for impact assessment.
107#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
108pub struct Cost {
109    /// Amount in smallest unit (e.g., cents).
110    pub amount: u64,
111    /// Currency code (e.g., "USD").
112    pub currency: String,
113}
114
115impl Cost {
116    /// Create a new cost.
117    pub fn new(amount: u64, currency: impl Into<String>) -> Self {
118        Self {
119            amount,
120            currency: currency.into(),
121        }
122    }
123}
124
125/// Assessment of action impact.
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct ImpactAssessment {
128    /// Severity level.
129    severity: Severity,
130    /// Affected resources.
131    affected_resources: Vec<ResourceId>,
132    /// Estimated cost (if applicable).
133    estimated_cost: Option<Cost>,
134    /// Risk factors.
135    risks: Vec<String>,
136}
137
138impl ImpactAssessment {
139    /// Create a new impact assessment.
140    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    /// Create a low severity assessment.
150    pub fn low() -> Self {
151        Self::new(Severity::Low)
152    }
153
154    /// Create a medium severity assessment.
155    pub fn medium() -> Self {
156        Self::new(Severity::Medium)
157    }
158
159    /// Create a high severity assessment.
160    pub fn high() -> Self {
161        Self::new(Severity::High)
162    }
163
164    /// Create a critical severity assessment.
165    pub fn critical() -> Self {
166        Self::new(Severity::Critical)
167    }
168
169    /// Add an affected resource.
170    pub fn with_resource(mut self, resource: ResourceId) -> Self {
171        self.affected_resources.push(resource);
172        self
173    }
174
175    /// Add affected resources.
176    pub fn with_resources(mut self, resources: Vec<ResourceId>) -> Self {
177        self.affected_resources = resources;
178        self
179    }
180
181    /// Set estimated cost.
182    pub fn with_cost(mut self, cost: Cost) -> Self {
183        self.estimated_cost = Some(cost);
184        self
185    }
186
187    /// Add a risk factor.
188    pub fn with_risk(mut self, risk: impl Into<String>) -> Self {
189        self.risks.push(risk.into());
190        self
191    }
192
193    /// Get the severity.
194    pub fn severity(&self) -> Severity {
195        self.severity
196    }
197
198    /// Get the affected resources.
199    pub fn affected_resources(&self) -> &[ResourceId] {
200        &self.affected_resources
201    }
202
203    /// Get the estimated cost.
204    pub fn estimated_cost(&self) -> Option<&Cost> {
205        self.estimated_cost.as_ref()
206    }
207
208    /// Get the risks.
209    pub fn risks(&self) -> &[String] {
210        &self.risks
211    }
212
213    /// Check if this assessment requires approval.
214    pub fn requires_approval(&self) -> bool {
215        self.severity.requires_approval()
216    }
217}
218
219/// The action being proposed for approval.
220#[derive(Debug, Clone, Serialize, Deserialize)]
221pub struct ProposedAction {
222    /// What the agent wants to do.
223    action_type: String,
224    /// Target resource.
225    resource: ResourceId,
226    /// Action parameters.
227    parameters: serde_json::Value,
228    /// Why the agent wants to do this.
229    reasoning: String,
230    /// Estimated impact.
231    impact: ImpactAssessment,
232    /// Can this action be undone?
233    reversible: bool,
234}
235
236impl ProposedAction {
237    /// Create a new proposed action builder.
238    pub fn builder() -> ProposedActionBuilder {
239        ProposedActionBuilder::new()
240    }
241
242    /// Get the action type.
243    pub fn action_type(&self) -> &str {
244        &self.action_type
245    }
246
247    /// Get the target resource.
248    pub fn resource(&self) -> &ResourceId {
249        &self.resource
250    }
251
252    /// Get the parameters.
253    pub fn parameters(&self) -> &serde_json::Value {
254        &self.parameters
255    }
256
257    /// Get the reasoning.
258    pub fn reasoning(&self) -> &str {
259        &self.reasoning
260    }
261
262    /// Get the impact assessment.
263    pub fn impact(&self) -> &ImpactAssessment {
264        &self.impact
265    }
266
267    /// Check if the action is reversible.
268    pub fn is_reversible(&self) -> bool {
269        self.reversible
270    }
271}
272
273/// Builder for ProposedAction.
274#[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    /// Create a new builder.
286    pub fn new() -> Self {
287        Self {
288            parameters: serde_json::Value::Null,
289            ..Default::default()
290        }
291    }
292
293    /// Set the action type.
294    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    /// Set the target resource.
300    pub fn resource(mut self, resource: ResourceId) -> Self {
301        self.resource = Some(resource);
302        self
303    }
304
305    /// Set the parameters.
306    pub fn parameters(mut self, parameters: serde_json::Value) -> Self {
307        self.parameters = parameters;
308        self
309    }
310
311    /// Set the reasoning.
312    pub fn reasoning(mut self, reasoning: impl Into<String>) -> Self {
313        self.reasoning = Some(reasoning.into());
314        self
315    }
316
317    /// Set the impact assessment.
318    pub fn impact(mut self, impact: ImpactAssessment) -> Self {
319        self.impact = Some(impact);
320        self
321    }
322
323    /// Set whether the action is reversible.
324    pub fn reversible(mut self, reversible: bool) -> Self {
325        self.reversible = reversible;
326        self
327    }
328
329    /// Build the proposed action.
330    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/// Escalation policy configuration.
357#[derive(Debug, Clone, Serialize, Deserialize)]
358pub struct EscalationPolicy {
359    /// Escalate after this duration (milliseconds).
360    escalate_after_ms: u64,
361    /// Who to escalate to.
362    escalate_to: Vec<PrincipalId>,
363    /// Maximum escalation levels.
364    max_escalations: u32,
365}
366
367impl EscalationPolicy {
368    /// Create a new escalation policy.
369    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    /// Set maximum escalation levels.
378    pub fn with_max_escalations(mut self, max: u32) -> Self {
379        self.max_escalations = max;
380        self
381    }
382
383    /// Get escalation timeout.
384    pub fn escalate_after(&self) -> Duration {
385        Duration::from_millis(self.escalate_after_ms)
386    }
387
388    /// Get escalation targets.
389    pub fn escalate_to(&self) -> &[PrincipalId] {
390        &self.escalate_to
391    }
392
393    /// Get maximum escalations.
394    pub fn max_escalations(&self) -> u32 {
395        self.max_escalations
396    }
397}
398
399/// How approval decisions are made.
400#[derive(Debug, Clone, Serialize, Deserialize)]
401pub struct ApprovalPolicy {
402    /// How many approvals needed.
403    required_approvals: u32,
404    /// Whether any approver can reject.
405    any_can_reject: bool,
406    /// Auto-approve after timeout (dangerous, use carefully).
407    auto_approve_on_timeout: bool,
408    /// Escalation path if no response.
409    escalation: Option<EscalationPolicy>,
410}
411
412impl ApprovalPolicy {
413    /// Create a new approval policy requiring one approval.
414    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    /// Create a policy requiring multiple approvals.
424    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    /// Set whether any approver can reject.
434    pub fn with_any_can_reject(mut self, can_reject: bool) -> Self {
435        self.any_can_reject = can_reject;
436        self
437    }
438
439    /// Enable auto-approve on timeout (use with extreme caution).
440    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    /// Set escalation policy.
446    pub fn with_escalation(mut self, policy: EscalationPolicy) -> Self {
447        self.escalation = Some(policy);
448        self
449    }
450
451    /// Get required approvals count.
452    pub fn required_approvals(&self) -> u32 {
453        self.required_approvals
454    }
455
456    /// Check if any approver can reject.
457    pub fn any_can_reject(&self) -> bool {
458        self.any_can_reject
459    }
460
461    /// Check if auto-approve on timeout is enabled.
462    pub fn auto_approve_on_timeout(&self) -> bool {
463        self.auto_approve_on_timeout
464    }
465
466    /// Get escalation policy.
467    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/// Modifications to the proposed action.
479#[derive(Debug, Clone, Default, Serialize, Deserialize)]
480pub struct ActionModifications {
481    /// Modified parameters.
482    pub parameters: Option<serde_json::Value>,
483    /// Additional constraints.
484    pub constraints: Vec<String>,
485    /// Modified scope.
486    pub scope: Option<ResourceScope>,
487    /// Human-provided instructions.
488    pub instructions: Option<String>,
489}
490
491impl ActionModifications {
492    /// Create empty modifications.
493    pub fn new() -> Self {
494        Self::default()
495    }
496
497    /// Set modified parameters.
498    pub fn with_parameters(mut self, params: serde_json::Value) -> Self {
499        self.parameters = Some(params);
500        self
501    }
502
503    /// Add a constraint.
504    pub fn with_constraint(mut self, constraint: impl Into<String>) -> Self {
505        self.constraints.push(constraint.into());
506        self
507    }
508
509    /// Set modified scope.
510    pub fn with_scope(mut self, scope: ResourceScope) -> Self {
511        self.scope = Some(scope);
512        self
513    }
514
515    /// Set instructions.
516    pub fn with_instructions(mut self, instructions: impl Into<String>) -> Self {
517        self.instructions = Some(instructions.into());
518        self
519    }
520
521    /// Check if there are any modifications.
522    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/// Actor identifier for cancellation tracking.
531#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
532#[serde(tag = "type", rename_all = "snake_case")]
533pub enum CancellationActor {
534    /// Human principal.
535    Principal(PrincipalId),
536    /// Agent.
537    Agent(PublicKey),
538    /// System.
539    System,
540}
541
542/// Current status of an approval request.
543#[derive(Debug, Clone, Serialize, Deserialize)]
544#[serde(tag = "status", rename_all = "snake_case")]
545pub enum ApprovalStatus {
546    /// Awaiting response.
547    Pending,
548    /// Approved by a human.
549    Approved {
550        approver: PrincipalId,
551        approved_at: i64,
552        modifications: Option<ActionModifications>,
553    },
554    /// Rejected by a human.
555    Rejected {
556        rejector: PrincipalId,
557        rejected_at: i64,
558        reason: String,
559    },
560    /// Request expired without response.
561    Expired,
562    /// Escalated to higher authority.
563    Escalated {
564        escalated_to: Vec<PrincipalId>,
565        escalated_at: i64,
566        escalation_level: u32,
567    },
568    /// Cancelled by requestor or system.
569    Cancelled {
570        cancelled_by: CancellationActor,
571        reason: String,
572    },
573}
574
575impl ApprovalStatus {
576    /// Check if the request is pending.
577    pub fn is_pending(&self) -> bool {
578        matches!(self, ApprovalStatus::Pending)
579    }
580
581    /// Check if the request is approved.
582    pub fn is_approved(&self) -> bool {
583        matches!(self, ApprovalStatus::Approved { .. })
584    }
585
586    /// Check if the request is rejected.
587    pub fn is_rejected(&self) -> bool {
588        matches!(self, ApprovalStatus::Rejected { .. })
589    }
590
591    /// Check if the request is expired.
592    pub fn is_expired(&self) -> bool {
593        matches!(self, ApprovalStatus::Expired)
594    }
595
596    /// Check if the request is escalated.
597    pub fn is_escalated(&self) -> bool {
598        matches!(self, ApprovalStatus::Escalated { .. })
599    }
600
601    /// Check if the request is cancelled.
602    pub fn is_cancelled(&self) -> bool {
603        matches!(self, ApprovalStatus::Cancelled { .. })
604    }
605
606    /// Check if the request is resolved (no longer pending).
607    pub fn is_resolved(&self) -> bool {
608        !self.is_pending() && !self.is_escalated()
609    }
610
611    /// Get modifications if approved with modifications.
612    pub fn modifications(&self) -> Option<&ActionModifications> {
613        match self {
614            ApprovalStatus::Approved { modifications, .. } => modifications.as_ref(),
615            _ => None,
616        }
617    }
618}
619
620/// Context provided to approvers.
621#[derive(Debug, Clone, Serialize, Deserialize)]
622pub struct ApprovalContext {
623    /// Causal chain leading to this request.
624    pub causal_context: CausalContext,
625    /// Agent's attestation hash.
626    pub agent_attestation_hash: Hash,
627    /// Capability being invoked.
628    pub capability_id: CapabilityId,
629    /// Similar past actions for reference.
630    pub similar_actions: Vec<EventId>,
631    /// Agent's full reasoning trace.
632    pub reasoning_trace: Option<ReasoningTrace>,
633}
634
635impl ApprovalContext {
636    /// Create a new approval context.
637    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    /// Add similar past actions.
652    pub fn with_similar_actions(mut self, actions: Vec<EventId>) -> Self {
653        self.similar_actions = actions;
654        self
655    }
656
657    /// Add reasoning trace.
658    pub fn with_reasoning_trace(mut self, trace: ReasoningTrace) -> Self {
659        self.reasoning_trace = Some(trace);
660        self
661    }
662}
663
664/// Request for human approval of an agent action.
665#[derive(Debug, Clone, Serialize, Deserialize)]
666pub struct ApprovalRequest {
667    /// Unique request identifier.
668    id: ApprovalRequestId,
669    /// The proposed action awaiting approval.
670    proposed_action: ProposedAction,
671    /// Agent requesting approval.
672    requestor: PublicKey,
673    /// Human(s) who can approve.
674    approvers: Vec<PrincipalId>,
675    /// Approval policy.
676    policy: ApprovalPolicy,
677    /// When the request was created (Unix timestamp ms).
678    created_at: i64,
679    /// When the request expires (Unix timestamp ms).
680    expires_at: i64,
681    /// Current status.
682    status: ApprovalStatus,
683    /// Context for the approver.
684    context: ApprovalContext,
685    /// Current escalation level.
686    escalation_level: u32,
687    /// Collected approvals (for multi-approval policies).
688    collected_approvals: Vec<(PrincipalId, i64)>,
689}
690
691impl ApprovalRequest {
692    /// Create a new approval request.
693    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    /// Get the request ID.
718    pub fn id(&self) -> ApprovalRequestId {
719        self.id
720    }
721
722    /// Get the proposed action.
723    pub fn proposed_action(&self) -> &ProposedAction {
724        &self.proposed_action
725    }
726
727    /// Get the requestor.
728    pub fn requestor(&self) -> &PublicKey {
729        &self.requestor
730    }
731
732    /// Get the approvers.
733    pub fn approvers(&self) -> &[PrincipalId] {
734        &self.approvers
735    }
736
737    /// Get the policy.
738    pub fn policy(&self) -> &ApprovalPolicy {
739        &self.policy
740    }
741
742    /// Get when the request was created.
743    pub fn created_at(&self) -> i64 {
744        self.created_at
745    }
746
747    /// Get when the request expires.
748    pub fn expires_at(&self) -> i64 {
749        self.expires_at
750    }
751
752    /// Get the current status.
753    pub fn status(&self) -> &ApprovalStatus {
754        &self.status
755    }
756
757    /// Get the context.
758    pub fn context(&self) -> &ApprovalContext {
759        &self.context
760    }
761
762    /// Check if the request has expired.
763    pub fn is_expired(&self) -> bool {
764        let now = chrono::Utc::now().timestamp_millis();
765        now >= self.expires_at
766    }
767
768    /// Check if the request is approved (met required approvals).
769    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    /// Check if a principal can approve this request.
775    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    /// Apply a response to this request.
781    pub fn apply_response(&mut self, response: &ApprovalResponse) -> Result<()> {
782        // Verify request ID matches
783        if response.request_id() != self.id {
784            return Err(Error::invalid_input("Response request_id does not match"));
785        }
786
787        // Check if already resolved
788        if self.status.is_resolved() {
789            return Err(Error::invalid_input("Request is already resolved"));
790        }
791
792        // Check if expired
793        if self.is_expired() {
794            self.status = ApprovalStatus::Expired;
795            return Err(Error::invalid_input("Request has expired"));
796        }
797
798        // Verify responder is an approver
799        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                // Keep pending, but record the request for info
841            }
842            ApprovalDecision::Defer { .. } => {
843                // Keep pending, deferral handled separately
844            }
845        }
846
847        Ok(())
848    }
849
850    /// Escalate the request.
851    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        // Add escalation targets to approvers
866        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        // Extend expiry
879        self.expires_at = now + policy.escalate_after_ms as i64;
880
881        Ok(())
882    }
883
884    /// Check if escalation is needed.
885    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    /// Cancel the request.
906    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    /// Mark as expired.
914    pub fn mark_expired(&mut self) {
915        if self.status.is_pending() {
916            self.status = ApprovalStatus::Expired;
917        }
918    }
919}
920
921/// Decision made by an approver.
922#[derive(Debug, Clone, Serialize, Deserialize)]
923#[serde(tag = "type", rename_all = "snake_case")]
924pub enum ApprovalDecision {
925    /// Approve as requested.
926    Approve,
927    /// Approve with modifications.
928    ApproveWithModifications(ActionModifications),
929    /// Reject the action.
930    Reject { reason: String },
931    /// Request more information.
932    RequestInfo { questions: Vec<String> },
933    /// Defer to another approver.
934    Defer { defer_to: PrincipalId },
935}
936
937impl ApprovalDecision {
938    /// Create an approval.
939    pub fn approve() -> Self {
940        Self::Approve
941    }
942
943    /// Create an approval with modifications.
944    pub fn approve_with_modifications(mods: ActionModifications) -> Self {
945        Self::ApproveWithModifications(mods)
946    }
947
948    /// Create a rejection.
949    pub fn reject(reason: impl Into<String>) -> Self {
950        Self::Reject {
951            reason: reason.into(),
952        }
953    }
954
955    /// Create an info request.
956    pub fn request_info(questions: Vec<String>) -> Self {
957        Self::RequestInfo { questions }
958    }
959
960    /// Create a deferral.
961    pub fn defer(defer_to: PrincipalId) -> Self {
962        Self::Defer { defer_to }
963    }
964
965    /// Check if this is an approval (with or without modifications).
966    pub fn is_approval(&self) -> bool {
967        matches!(
968            self,
969            ApprovalDecision::Approve | ApprovalDecision::ApproveWithModifications(_)
970        )
971    }
972
973    /// Check if this is a rejection.
974    pub fn is_rejection(&self) -> bool {
975        matches!(self, ApprovalDecision::Reject { .. })
976    }
977}
978
979/// Human response to an approval request.
980#[derive(Debug, Clone, Serialize, Deserialize)]
981pub struct ApprovalResponse {
982    /// The request being responded to.
983    request_id: ApprovalRequestId,
984    /// The human responding.
985    responder: PrincipalId,
986    /// The decision.
987    decision: ApprovalDecision,
988    /// When the response was made (Unix timestamp ms).
989    responded_at: i64,
990    /// Signature proving human involvement.
991    signature: Sig,
992}
993
994impl ApprovalResponse {
995    /// Create a new approval response.
996    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    /// Get the request ID this response is for.
1011    pub fn request_id(&self) -> ApprovalRequestId {
1012        self.request_id
1013    }
1014
1015    /// Get the responder principal.
1016    pub fn responder(&self) -> &PrincipalId {
1017        &self.responder
1018    }
1019
1020    /// Get the approval decision.
1021    pub fn decision(&self) -> &ApprovalDecision {
1022        &self.decision
1023    }
1024
1025    /// Get when the response was made (Unix timestamp ms).
1026    pub fn responded_at(&self) -> i64 {
1027        self.responded_at
1028    }
1029
1030    /// Get the response signature.
1031    pub fn signature(&self) -> &Sig {
1032        &self.signature
1033    }
1034
1035    /// Sign the response.
1036    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    /// Compute canonical bytes for signing.
1043    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    /// Verify the signature.
1055    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    // === ApprovalRequestId Tests ===
1101
1102    #[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    // === Severity Tests ===
1118
1119    #[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    // === ImpactAssessment Tests ===
1135
1136    #[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    // === ProposedAction Tests ===
1158
1159    #[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()); // Missing reasoning
1169
1170        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    // === ApprovalPolicy Tests ===
1179
1180    #[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    // === ApprovalRequest Tests ===
1195
1196    #[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), // Immediate expiry
1255            test_context(),
1256        );
1257
1258        // Wait a tiny bit to ensure expiry
1259        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        // First approval
1285        let response1 = ApprovalResponse::new(req.id(), approver1, ApprovalDecision::approve());
1286        req.apply_response(&response1).unwrap();
1287        assert!(!req.is_approved());
1288
1289        // Second approval
1290        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        // Single rejection should reject the whole request
1311        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    // === ApprovalResponse Tests ===
1318
1319    #[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        // Response with wrong request ID
1332        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()], // Only bob can approve
1347            ApprovalPolicy::single_approver(),
1348            Duration::from_secs(300),
1349            test_context(),
1350        );
1351
1352        // Response from non-approver
1353        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        // Verify with correct key
1369        assert!(response
1370            .verify_signature(&approver_key.public_key())
1371            .is_ok());
1372
1373        // Verify with wrong key should fail
1374        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    // === Escalation Tests ===
1409
1410    #[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        // First escalation
1455        assert!(req.escalate().is_ok());
1456
1457        // Second escalation
1458        req.status = ApprovalStatus::Pending; // Reset for testing
1459        assert!(req.escalate().is_ok());
1460
1461        // Third escalation should fail
1462        req.status = ApprovalStatus::Pending;
1463        assert!(req.escalate().is_err());
1464    }
1465
1466    // === ActionModifications Tests ===
1467
1468    #[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    // === ApprovalDecision Tests ===
1481
1482    #[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    // === ApprovalResponse Encapsulation Tests (Finding 3.1) ===
1498
1499    #[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        // Signature should not be empty after signing
1523        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}