sentinel_proxy/agents/
decision.rs

1//! Agent decision types.
2
3use std::collections::HashMap;
4
5use sentinel_agent_protocol::{AgentResponse, AuditMetadata, Decision, HeaderOp};
6
7/// Agent decision combining all agent responses.
8#[derive(Debug, Clone)]
9pub struct AgentDecision {
10    /// Final decision action
11    pub action: AgentAction,
12    /// Header modifications for request
13    pub request_headers: Vec<HeaderOp>,
14    /// Header modifications for response
15    pub response_headers: Vec<HeaderOp>,
16    /// Audit metadata from all agents
17    pub audit: Vec<AuditMetadata>,
18    /// Routing metadata updates
19    pub routing_metadata: HashMap<String, String>,
20}
21
22/// Agent action types.
23#[derive(Debug, Clone)]
24pub enum AgentAction {
25    /// Allow request to proceed
26    Allow,
27    /// Block request
28    Block {
29        status: u16,
30        body: Option<String>,
31        headers: Option<HashMap<String, String>>,
32    },
33    /// Redirect request
34    Redirect { url: String, status: u16 },
35    /// Challenge client
36    Challenge {
37        challenge_type: String,
38        params: HashMap<String, String>,
39    },
40}
41
42impl AgentDecision {
43    /// Create default allow decision.
44    pub fn default_allow() -> Self {
45        Self {
46            action: AgentAction::Allow,
47            request_headers: Vec::new(),
48            response_headers: Vec::new(),
49            audit: Vec::new(),
50            routing_metadata: HashMap::new(),
51        }
52    }
53
54    /// Create block decision.
55    pub fn block(status: u16, message: &str) -> Self {
56        Self {
57            action: AgentAction::Block {
58                status,
59                body: Some(message.to_string()),
60                headers: None,
61            },
62            request_headers: Vec::new(),
63            response_headers: Vec::new(),
64            audit: Vec::new(),
65            routing_metadata: HashMap::new(),
66        }
67    }
68
69    /// Check if decision is to allow.
70    pub fn is_allow(&self) -> bool {
71        matches!(self.action, AgentAction::Allow)
72    }
73
74    /// Merge another decision into this one.
75    ///
76    /// If other decision is not allow, use it as the action.
77    /// Header modifications, audit metadata, and routing metadata are merged.
78    pub fn merge(&mut self, other: AgentDecision) {
79        // If other decision is not allow, use it
80        if !other.is_allow() {
81            self.action = other.action;
82        }
83
84        // Merge header modifications
85        self.request_headers.extend(other.request_headers);
86        self.response_headers.extend(other.response_headers);
87
88        // Merge audit metadata
89        self.audit.extend(other.audit);
90
91        // Merge routing metadata
92        self.routing_metadata.extend(other.routing_metadata);
93    }
94}
95
96impl From<AgentResponse> for AgentDecision {
97    fn from(response: AgentResponse) -> Self {
98        let action = match response.decision {
99            Decision::Allow => AgentAction::Allow,
100            Decision::Block {
101                status,
102                body,
103                headers,
104            } => AgentAction::Block {
105                status,
106                body,
107                headers,
108            },
109            Decision::Redirect { url, status } => AgentAction::Redirect { url, status },
110            Decision::Challenge {
111                challenge_type,
112                params,
113            } => AgentAction::Challenge {
114                challenge_type,
115                params,
116            },
117        };
118
119        Self {
120            action,
121            request_headers: response.request_headers,
122            response_headers: response.response_headers,
123            audit: vec![response.audit],
124            routing_metadata: response.routing_metadata,
125        }
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn test_agent_decision_merge() {
135        let mut decision1 = AgentDecision::default_allow();
136        decision1.request_headers.push(HeaderOp::Set {
137            name: "X-Test".to_string(),
138            value: "1".to_string(),
139        });
140
141        let decision2 = AgentDecision::block(403, "Forbidden");
142
143        decision1.merge(decision2);
144        assert!(!decision1.is_allow());
145    }
146}