sentinel_agent_protocol/v2/
capabilities.rs

1//! Agent capability negotiation for Protocol v2.
2
3use serde::{Deserialize, Serialize};
4use crate::EventType;
5
6/// Agent capabilities declared during handshake.
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct AgentCapabilities {
9    pub protocol_version: u32,
10    pub agent_id: String,
11    pub name: String,
12    pub version: String,
13    pub supported_events: Vec<EventType>,
14    #[serde(default)]
15    pub features: AgentFeatures,
16    #[serde(default)]
17    pub limits: AgentLimits,
18    #[serde(default)]
19    pub health: HealthConfig,
20}
21
22impl AgentCapabilities {
23    pub fn new(agent_id: impl Into<String>, name: impl Into<String>, version: impl Into<String>) -> Self {
24        Self {
25            protocol_version: super::PROTOCOL_VERSION_2,
26            agent_id: agent_id.into(),
27            name: name.into(),
28            version: version.into(),
29            supported_events: vec![EventType::RequestHeaders],
30            features: AgentFeatures::default(),
31            limits: AgentLimits::default(),
32            health: HealthConfig::default(),
33        }
34    }
35
36    pub fn supports_event(&self, event_type: EventType) -> bool {
37        self.supported_events.contains(&event_type)
38    }
39
40    pub fn with_event(mut self, event_type: EventType) -> Self {
41        if !self.supported_events.contains(&event_type) {
42            self.supported_events.push(event_type);
43        }
44        self
45    }
46
47    pub fn with_features(mut self, features: AgentFeatures) -> Self {
48        self.features = features;
49        self
50    }
51
52    pub fn with_limits(mut self, limits: AgentLimits) -> Self {
53        self.limits = limits;
54        self
55    }
56}
57
58/// Features this agent supports.
59#[derive(Debug, Clone, Default, Serialize, Deserialize)]
60pub struct AgentFeatures {
61    #[serde(default)]
62    pub streaming_body: bool,
63    #[serde(default)]
64    pub websocket: bool,
65    #[serde(default)]
66    pub guardrails: bool,
67    #[serde(default)]
68    pub config_push: bool,
69    #[serde(default)]
70    pub metrics_export: bool,
71    #[serde(default)]
72    pub concurrent_requests: u32,
73    #[serde(default)]
74    pub cancellation: bool,
75    #[serde(default)]
76    pub flow_control: bool,
77    #[serde(default)]
78    pub health_reporting: bool,
79}
80
81impl AgentFeatures {
82    pub fn simple() -> Self { Self::default() }
83    pub fn full() -> Self {
84        Self {
85            streaming_body: true,
86            websocket: true,
87            guardrails: true,
88            config_push: true,
89            metrics_export: true,
90            concurrent_requests: 100,
91            cancellation: true,
92            flow_control: true,
93            health_reporting: true,
94        }
95    }
96}
97
98/// Resource limits.
99#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct AgentLimits {
101    pub max_body_size: usize,
102    pub max_concurrency: u32,
103    pub preferred_chunk_size: usize,
104    pub max_memory: Option<usize>,
105    pub max_processing_time_ms: Option<u64>,
106}
107
108impl Default for AgentLimits {
109    fn default() -> Self {
110        Self {
111            max_body_size: 10 * 1024 * 1024,
112            max_concurrency: 100,
113            preferred_chunk_size: 64 * 1024,
114            max_memory: None,
115            max_processing_time_ms: Some(5000),
116        }
117    }
118}
119
120/// Health check configuration.
121#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct HealthConfig {
123    pub report_interval_ms: u32,
124    pub include_load_metrics: bool,
125    pub include_resource_metrics: bool,
126}
127
128impl Default for HealthConfig {
129    fn default() -> Self {
130        Self {
131            report_interval_ms: 10_000,
132            include_load_metrics: true,
133            include_resource_metrics: false,
134        }
135    }
136}
137
138/// Handshake request from proxy to agent.
139#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct HandshakeRequest {
141    pub supported_versions: Vec<u32>,
142    pub proxy_id: String,
143    pub proxy_version: String,
144    pub config: serde_json::Value,
145}
146
147impl HandshakeRequest {
148    pub fn new(proxy_id: impl Into<String>, proxy_version: impl Into<String>) -> Self {
149        Self {
150            supported_versions: vec![super::PROTOCOL_VERSION_2, 1],
151            proxy_id: proxy_id.into(),
152            proxy_version: proxy_version.into(),
153            config: serde_json::Value::Null,
154        }
155    }
156
157    pub fn max_version(&self) -> u32 {
158        self.supported_versions.first().copied().unwrap_or(1)
159    }
160}
161
162/// Handshake response from agent to proxy.
163#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct HandshakeResponse {
165    pub protocol_version: u32,
166    pub capabilities: AgentCapabilities,
167    pub success: bool,
168    pub error: Option<String>,
169}
170
171impl HandshakeResponse {
172    pub fn success(capabilities: AgentCapabilities) -> Self {
173        Self {
174            protocol_version: capabilities.protocol_version,
175            capabilities,
176            success: true,
177            error: None,
178        }
179    }
180
181    pub fn failure(error: impl Into<String>) -> Self {
182        Self {
183            protocol_version: 0,
184            capabilities: AgentCapabilities::new("", "", ""),
185            success: false,
186            error: Some(error.into()),
187        }
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    #[test]
196    fn test_capabilities_builder() {
197        let caps = AgentCapabilities::new("test-agent", "Test Agent", "1.0.0")
198            .with_event(EventType::RequestHeaders)
199            .with_features(AgentFeatures::full());
200
201        assert_eq!(caps.agent_id, "test-agent");
202        assert!(caps.supports_event(EventType::RequestHeaders));
203        assert!(caps.features.streaming_body);
204    }
205
206    #[test]
207    fn test_handshake() {
208        let request = HandshakeRequest::new("proxy-1", "0.2.5");
209        assert_eq!(request.max_version(), 2);
210
211        let caps = AgentCapabilities::new("agent-1", "My Agent", "1.0.0");
212        let response = HandshakeResponse::success(caps);
213        assert!(response.success);
214    }
215
216    #[test]
217    fn test_features() {
218        let simple = AgentFeatures::simple();
219        assert!(!simple.streaming_body);
220
221        let full = AgentFeatures::full();
222        assert!(full.streaming_body);
223    }
224}