sentinel_proxy/agents/
decision.rs

1//! Agent decision types.
2
3use std::collections::HashMap;
4
5use sentinel_agent_protocol::{AgentResponse, AuditMetadata, BodyMutation, 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    /// Whether agent needs more data to make final decision (streaming mode)
21    pub needs_more: bool,
22    /// Mutation for request body chunk (streaming mode)
23    pub request_body_mutation: Option<BodyMutation>,
24    /// Mutation for response body chunk (streaming mode)
25    pub response_body_mutation: Option<BodyMutation>,
26}
27
28/// Agent action types.
29#[derive(Debug, Clone)]
30pub enum AgentAction {
31    /// Allow request to proceed
32    Allow,
33    /// Block request
34    Block {
35        status: u16,
36        body: Option<String>,
37        headers: Option<HashMap<String, String>>,
38    },
39    /// Redirect request
40    Redirect { url: String, status: u16 },
41    /// Challenge client
42    Challenge {
43        challenge_type: String,
44        params: HashMap<String, String>,
45    },
46}
47
48impl AgentDecision {
49    /// Create default allow decision.
50    pub fn default_allow() -> Self {
51        Self {
52            action: AgentAction::Allow,
53            request_headers: Vec::new(),
54            response_headers: Vec::new(),
55            audit: Vec::new(),
56            routing_metadata: HashMap::new(),
57            needs_more: false,
58            request_body_mutation: None,
59            response_body_mutation: None,
60        }
61    }
62
63    /// Create block decision.
64    pub fn block(status: u16, message: &str) -> Self {
65        Self {
66            action: AgentAction::Block {
67                status,
68                body: Some(message.to_string()),
69                headers: None,
70            },
71            request_headers: Vec::new(),
72            response_headers: Vec::new(),
73            audit: Vec::new(),
74            routing_metadata: HashMap::new(),
75            needs_more: false,
76            request_body_mutation: None,
77            response_body_mutation: None,
78        }
79    }
80
81    /// Check if decision is to allow.
82    pub fn is_allow(&self) -> bool {
83        matches!(self.action, AgentAction::Allow)
84    }
85
86    /// Merge another decision into this one.
87    ///
88    /// If other decision is not allow, use it as the action.
89    /// Header modifications, audit metadata, and routing metadata are merged.
90    pub fn merge(&mut self, other: AgentDecision) {
91        // If other decision is not allow, use it
92        if !other.is_allow() {
93            self.action = other.action;
94        }
95
96        // Merge header modifications
97        self.request_headers.extend(other.request_headers);
98        self.response_headers.extend(other.response_headers);
99
100        // Merge audit metadata
101        self.audit.extend(other.audit);
102
103        // Merge routing metadata
104        self.routing_metadata.extend(other.routing_metadata);
105
106        // Streaming: if any agent needs more, we need more
107        if other.needs_more {
108            self.needs_more = true;
109        }
110
111        // Body mutations: last one wins
112        if other.request_body_mutation.is_some() {
113            self.request_body_mutation = other.request_body_mutation;
114        }
115        if other.response_body_mutation.is_some() {
116            self.response_body_mutation = other.response_body_mutation;
117        }
118    }
119}
120
121impl From<AgentResponse> for AgentDecision {
122    fn from(response: AgentResponse) -> Self {
123        let action = match response.decision {
124            Decision::Allow => AgentAction::Allow,
125            Decision::Block {
126                status,
127                body,
128                headers,
129            } => AgentAction::Block {
130                status,
131                body,
132                headers,
133            },
134            Decision::Redirect { url, status } => AgentAction::Redirect { url, status },
135            Decision::Challenge {
136                challenge_type,
137                params,
138            } => AgentAction::Challenge {
139                challenge_type,
140                params,
141            },
142        };
143
144        Self {
145            action,
146            request_headers: response.request_headers,
147            response_headers: response.response_headers,
148            audit: vec![response.audit],
149            routing_metadata: response.routing_metadata,
150            needs_more: response.needs_more,
151            request_body_mutation: response.request_body_mutation,
152            response_body_mutation: response.response_body_mutation,
153        }
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[test]
162    fn test_agent_decision_merge() {
163        let mut decision1 = AgentDecision::default_allow();
164        decision1.request_headers.push(HeaderOp::Set {
165            name: "X-Test".to_string(),
166            value: "1".to_string(),
167        });
168
169        let decision2 = AgentDecision::block(403, "Forbidden");
170
171        decision1.merge(decision2);
172        assert!(!decision1.is_allow());
173    }
174}