1use bytes::Bytes;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10pub const PROTOCOL_VERSION: u32 = 2;
12
13pub const MAX_MESSAGE_SIZE: usize = 10 * 1024 * 1024;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
18#[serde(rename_all = "snake_case")]
19pub enum EventType {
20 Configure,
22 RequestHeaders,
24 RequestBodyChunk,
26 ResponseHeaders,
28 ResponseBodyChunk,
30 RequestComplete,
32 WebSocketFrame,
34 GuardrailInspect,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
40#[serde(rename_all = "snake_case")]
41pub enum Decision {
42 #[default]
44 Allow,
45 Block {
47 status: u16,
49 body: Option<String>,
51 headers: Option<HashMap<String, String>>,
53 },
54 Redirect {
56 url: String,
58 status: u16,
60 },
61 Challenge {
63 challenge_type: String,
65 params: HashMap<String, String>,
67 },
68}
69
70#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
72#[serde(rename_all = "snake_case")]
73pub enum HeaderOp {
74 Set { name: String, value: String },
76 Add { name: String, value: String },
78 Remove { name: String },
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
93pub struct BodyMutation {
94 pub data: Option<String>,
100
101 #[serde(default)]
105 pub chunk_index: u32,
106}
107
108impl BodyMutation {
109 pub fn pass_through(chunk_index: u32) -> Self {
111 Self {
112 data: None,
113 chunk_index,
114 }
115 }
116
117 pub fn drop_chunk(chunk_index: u32) -> Self {
119 Self {
120 data: Some(String::new()),
121 chunk_index,
122 }
123 }
124
125 pub fn replace(chunk_index: u32, data: String) -> Self {
127 Self {
128 data: Some(data),
129 chunk_index,
130 }
131 }
132
133 pub fn is_pass_through(&self) -> bool {
135 self.data.is_none()
136 }
137
138 pub fn is_drop(&self) -> bool {
140 matches!(&self.data, Some(d) if d.is_empty())
141 }
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct RequestMetadata {
147 pub correlation_id: String,
149 pub request_id: String,
151 pub client_ip: String,
153 pub client_port: u16,
155 pub server_name: Option<String>,
157 pub protocol: String,
159 pub tls_version: Option<String>,
161 pub tls_cipher: Option<String>,
163 pub route_id: Option<String>,
165 pub upstream_id: Option<String>,
167 pub timestamp: String,
169 #[serde(skip_serializing_if = "Option::is_none")]
176 pub traceparent: Option<String>,
177}
178
179#[derive(Debug, Clone, Serialize, Deserialize)]
181pub struct RequestHeadersEvent {
182 pub metadata: RequestMetadata,
184 pub method: String,
186 pub uri: String,
188 pub headers: HashMap<String, Vec<String>>,
190}
191
192#[derive(Debug, Clone, Serialize, Deserialize)]
194pub struct RequestBodyChunkEvent {
195 pub correlation_id: String,
197 pub data: String,
199 pub is_last: bool,
201 pub total_size: Option<usize>,
203 #[serde(default)]
207 pub chunk_index: u32,
208 #[serde(default)]
210 pub bytes_received: usize,
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct ResponseHeadersEvent {
216 pub correlation_id: String,
218 pub status: u16,
220 pub headers: HashMap<String, Vec<String>>,
222}
223
224#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct ResponseBodyChunkEvent {
227 pub correlation_id: String,
229 pub data: String,
231 pub is_last: bool,
233 pub total_size: Option<usize>,
235 #[serde(default)]
237 pub chunk_index: u32,
238 #[serde(default)]
240 pub bytes_sent: usize,
241}
242
243#[derive(Debug, Clone)]
257pub struct BinaryRequestBodyChunkEvent {
258 pub correlation_id: String,
260 pub data: Bytes,
262 pub is_last: bool,
264 pub total_size: Option<usize>,
266 pub chunk_index: u32,
268 pub bytes_received: usize,
270}
271
272#[derive(Debug, Clone)]
282pub struct BinaryResponseBodyChunkEvent {
283 pub correlation_id: String,
285 pub data: Bytes,
287 pub is_last: bool,
289 pub total_size: Option<usize>,
291 pub chunk_index: u32,
293 pub bytes_sent: usize,
295}
296
297impl BinaryRequestBodyChunkEvent {
298 pub fn new(
300 correlation_id: impl Into<String>,
301 data: impl Into<Bytes>,
302 chunk_index: u32,
303 is_last: bool,
304 ) -> Self {
305 let data = data.into();
306 Self {
307 correlation_id: correlation_id.into(),
308 bytes_received: data.len(),
309 data,
310 is_last,
311 total_size: None,
312 chunk_index,
313 }
314 }
315
316 pub fn with_total_size(mut self, size: usize) -> Self {
318 self.total_size = Some(size);
319 self
320 }
321
322 pub fn with_bytes_received(mut self, bytes: usize) -> Self {
324 self.bytes_received = bytes;
325 self
326 }
327}
328
329impl BinaryResponseBodyChunkEvent {
330 pub fn new(
332 correlation_id: impl Into<String>,
333 data: impl Into<Bytes>,
334 chunk_index: u32,
335 is_last: bool,
336 ) -> Self {
337 let data = data.into();
338 Self {
339 correlation_id: correlation_id.into(),
340 bytes_sent: data.len(),
341 data,
342 is_last,
343 total_size: None,
344 chunk_index,
345 }
346 }
347
348 pub fn with_total_size(mut self, size: usize) -> Self {
350 self.total_size = Some(size);
351 self
352 }
353
354 pub fn with_bytes_sent(mut self, bytes: usize) -> Self {
356 self.bytes_sent = bytes;
357 self
358 }
359}
360
361impl From<BinaryRequestBodyChunkEvent> for RequestBodyChunkEvent {
366 fn from(event: BinaryRequestBodyChunkEvent) -> Self {
368 use base64::{engine::general_purpose::STANDARD, Engine as _};
369 Self {
370 correlation_id: event.correlation_id,
371 data: STANDARD.encode(&event.data),
372 is_last: event.is_last,
373 total_size: event.total_size,
374 chunk_index: event.chunk_index,
375 bytes_received: event.bytes_received,
376 }
377 }
378}
379
380impl From<&RequestBodyChunkEvent> for BinaryRequestBodyChunkEvent {
381 fn from(event: &RequestBodyChunkEvent) -> Self {
385 use base64::{engine::general_purpose::STANDARD, Engine as _};
386 let data = STANDARD
387 .decode(&event.data)
388 .map(Bytes::from)
389 .unwrap_or_else(|_| Bytes::copy_from_slice(event.data.as_bytes()));
390 Self {
391 correlation_id: event.correlation_id.clone(),
392 data,
393 is_last: event.is_last,
394 total_size: event.total_size,
395 chunk_index: event.chunk_index,
396 bytes_received: event.bytes_received,
397 }
398 }
399}
400
401impl From<BinaryResponseBodyChunkEvent> for ResponseBodyChunkEvent {
402 fn from(event: BinaryResponseBodyChunkEvent) -> Self {
404 use base64::{engine::general_purpose::STANDARD, Engine as _};
405 Self {
406 correlation_id: event.correlation_id,
407 data: STANDARD.encode(&event.data),
408 is_last: event.is_last,
409 total_size: event.total_size,
410 chunk_index: event.chunk_index,
411 bytes_sent: event.bytes_sent,
412 }
413 }
414}
415
416impl From<&ResponseBodyChunkEvent> for BinaryResponseBodyChunkEvent {
417 fn from(event: &ResponseBodyChunkEvent) -> Self {
421 use base64::{engine::general_purpose::STANDARD, Engine as _};
422 let data = STANDARD
423 .decode(&event.data)
424 .map(Bytes::from)
425 .unwrap_or_else(|_| Bytes::copy_from_slice(event.data.as_bytes()));
426 Self {
427 correlation_id: event.correlation_id.clone(),
428 data,
429 is_last: event.is_last,
430 total_size: event.total_size,
431 chunk_index: event.chunk_index,
432 bytes_sent: event.bytes_sent,
433 }
434 }
435}
436
437#[derive(Debug, Clone, Serialize, Deserialize)]
439pub struct RequestCompleteEvent {
440 pub correlation_id: String,
442 pub status: u16,
444 pub duration_ms: u64,
446 pub request_body_size: usize,
448 pub response_body_size: usize,
450 pub upstream_attempts: u32,
452 pub error: Option<String>,
454}
455
456#[derive(Debug, Clone, Serialize, Deserialize)]
465pub struct WebSocketFrameEvent {
466 pub correlation_id: String,
468 pub opcode: String,
470 pub data: String,
472 pub client_to_server: bool,
474 pub frame_index: u64,
476 pub fin: bool,
478 pub route_id: Option<String>,
480 pub client_ip: String,
482}
483
484#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
486#[serde(rename_all = "snake_case")]
487pub enum WebSocketOpcode {
488 Continuation,
490 Text,
492 Binary,
494 Close,
496 Ping,
498 Pong,
500}
501
502impl WebSocketOpcode {
503 pub fn as_str(&self) -> &'static str {
505 match self {
506 Self::Continuation => "continuation",
507 Self::Text => "text",
508 Self::Binary => "binary",
509 Self::Close => "close",
510 Self::Ping => "ping",
511 Self::Pong => "pong",
512 }
513 }
514
515 pub fn from_u8(value: u8) -> Option<Self> {
517 match value {
518 0x0 => Some(Self::Continuation),
519 0x1 => Some(Self::Text),
520 0x2 => Some(Self::Binary),
521 0x8 => Some(Self::Close),
522 0x9 => Some(Self::Ping),
523 0xA => Some(Self::Pong),
524 _ => None,
525 }
526 }
527
528 pub fn as_u8(&self) -> u8 {
530 match self {
531 Self::Continuation => 0x0,
532 Self::Text => 0x1,
533 Self::Binary => 0x2,
534 Self::Close => 0x8,
535 Self::Ping => 0x9,
536 Self::Pong => 0xA,
537 }
538 }
539}
540
541#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
545#[serde(rename_all = "snake_case")]
546pub enum WebSocketDecision {
547 #[default]
549 Allow,
550 Drop,
552 Close {
554 code: u16,
556 reason: String,
558 },
559}
560
561#[derive(Debug, Clone, Serialize, Deserialize)]
563pub struct AgentResponse {
564 pub version: u32,
566 pub decision: Decision,
568 #[serde(default)]
570 pub request_headers: Vec<HeaderOp>,
571 #[serde(default)]
573 pub response_headers: Vec<HeaderOp>,
574 #[serde(default)]
576 pub routing_metadata: HashMap<String, String>,
577 #[serde(default)]
579 pub audit: AuditMetadata,
580
581 #[serde(default)]
592 pub needs_more: bool,
593
594 #[serde(default)]
599 pub request_body_mutation: Option<BodyMutation>,
600
601 #[serde(default)]
606 pub response_body_mutation: Option<BodyMutation>,
607
608 #[serde(default)]
612 pub websocket_decision: Option<WebSocketDecision>,
613}
614
615impl AgentResponse {
616 pub fn default_allow() -> Self {
618 Self {
619 version: PROTOCOL_VERSION,
620 decision: Decision::Allow,
621 request_headers: vec![],
622 response_headers: vec![],
623 routing_metadata: HashMap::new(),
624 audit: AuditMetadata::default(),
625 needs_more: false,
626 request_body_mutation: None,
627 response_body_mutation: None,
628 websocket_decision: None,
629 }
630 }
631
632 pub fn block(status: u16, body: Option<String>) -> Self {
634 Self {
635 version: PROTOCOL_VERSION,
636 decision: Decision::Block {
637 status,
638 body,
639 headers: None,
640 },
641 request_headers: vec![],
642 response_headers: vec![],
643 routing_metadata: HashMap::new(),
644 audit: AuditMetadata::default(),
645 needs_more: false,
646 request_body_mutation: None,
647 response_body_mutation: None,
648 websocket_decision: None,
649 }
650 }
651
652 pub fn redirect(url: String, status: u16) -> Self {
654 Self {
655 version: PROTOCOL_VERSION,
656 decision: Decision::Redirect { url, status },
657 request_headers: vec![],
658 response_headers: vec![],
659 routing_metadata: HashMap::new(),
660 audit: AuditMetadata::default(),
661 needs_more: false,
662 request_body_mutation: None,
663 response_body_mutation: None,
664 websocket_decision: None,
665 }
666 }
667
668 pub fn needs_more_data() -> Self {
670 Self {
671 version: PROTOCOL_VERSION,
672 decision: Decision::Allow,
673 request_headers: vec![],
674 response_headers: vec![],
675 routing_metadata: HashMap::new(),
676 audit: AuditMetadata::default(),
677 needs_more: true,
678 request_body_mutation: None,
679 response_body_mutation: None,
680 websocket_decision: None,
681 }
682 }
683
684 pub fn websocket_allow() -> Self {
686 Self {
687 websocket_decision: Some(WebSocketDecision::Allow),
688 ..Self::default_allow()
689 }
690 }
691
692 pub fn websocket_drop() -> Self {
694 Self {
695 websocket_decision: Some(WebSocketDecision::Drop),
696 ..Self::default_allow()
697 }
698 }
699
700 pub fn websocket_close(code: u16, reason: String) -> Self {
702 Self {
703 websocket_decision: Some(WebSocketDecision::Close { code, reason }),
704 ..Self::default_allow()
705 }
706 }
707
708 pub fn with_websocket_decision(mut self, decision: WebSocketDecision) -> Self {
710 self.websocket_decision = Some(decision);
711 self
712 }
713
714 pub fn with_request_body_mutation(mut self, mutation: BodyMutation) -> Self {
716 self.request_body_mutation = Some(mutation);
717 self
718 }
719
720 pub fn with_response_body_mutation(mut self, mutation: BodyMutation) -> Self {
722 self.response_body_mutation = Some(mutation);
723 self
724 }
725
726 pub fn set_needs_more(mut self, needs_more: bool) -> Self {
728 self.needs_more = needs_more;
729 self
730 }
731
732 pub fn add_request_header(mut self, op: HeaderOp) -> Self {
734 self.request_headers.push(op);
735 self
736 }
737
738 pub fn add_response_header(mut self, op: HeaderOp) -> Self {
740 self.response_headers.push(op);
741 self
742 }
743
744 pub fn with_audit(mut self, audit: AuditMetadata) -> Self {
746 self.audit = audit;
747 self
748 }
749}
750
751#[derive(Debug, Clone, Default, Serialize, Deserialize)]
753pub struct AuditMetadata {
754 #[serde(default)]
756 pub tags: Vec<String>,
757 #[serde(default)]
759 pub rule_ids: Vec<String>,
760 pub confidence: Option<f32>,
762 #[serde(default)]
764 pub reason_codes: Vec<String>,
765 #[serde(default)]
767 pub custom: HashMap<String, serde_json::Value>,
768}
769
770#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
776#[serde(rename_all = "snake_case")]
777pub enum GuardrailInspectionType {
778 PromptInjection,
780 PiiDetection,
782}
783
784#[derive(Debug, Clone, Serialize, Deserialize)]
789pub struct GuardrailInspectEvent {
790 pub correlation_id: String,
792 pub inspection_type: GuardrailInspectionType,
794 pub content: String,
796 #[serde(skip_serializing_if = "Option::is_none")]
798 pub model: Option<String>,
799 #[serde(default)]
802 pub categories: Vec<String>,
803 #[serde(skip_serializing_if = "Option::is_none")]
805 pub route_id: Option<String>,
806 #[serde(default)]
808 pub metadata: HashMap<String, String>,
809}
810
811#[derive(Debug, Clone, Serialize, Deserialize)]
813pub struct GuardrailResponse {
814 pub detected: bool,
816 #[serde(default)]
818 pub confidence: f64,
819 #[serde(default)]
821 pub detections: Vec<GuardrailDetection>,
822 #[serde(skip_serializing_if = "Option::is_none")]
824 pub redacted_content: Option<String>,
825}
826
827impl Default for GuardrailResponse {
828 fn default() -> Self {
829 Self {
830 detected: false,
831 confidence: 0.0,
832 detections: Vec::new(),
833 redacted_content: None,
834 }
835 }
836}
837
838impl GuardrailResponse {
839 pub fn clean() -> Self {
841 Self::default()
842 }
843
844 pub fn with_detection(detection: GuardrailDetection) -> Self {
846 Self {
847 detected: true,
848 confidence: detection.confidence.unwrap_or(1.0),
849 detections: vec![detection],
850 redacted_content: None,
851 }
852 }
853
854 pub fn add_detection(&mut self, detection: GuardrailDetection) {
856 self.detected = true;
857 if let Some(conf) = detection.confidence {
858 self.confidence = self.confidence.max(conf);
859 }
860 self.detections.push(detection);
861 }
862}
863
864#[derive(Debug, Clone, Serialize, Deserialize)]
866pub struct GuardrailDetection {
867 pub category: String,
869 pub description: String,
871 #[serde(default)]
873 pub severity: DetectionSeverity,
874 #[serde(skip_serializing_if = "Option::is_none")]
876 pub confidence: Option<f64>,
877 #[serde(skip_serializing_if = "Option::is_none")]
879 pub span: Option<TextSpan>,
880}
881
882impl GuardrailDetection {
883 pub fn new(category: impl Into<String>, description: impl Into<String>) -> Self {
885 Self {
886 category: category.into(),
887 description: description.into(),
888 severity: DetectionSeverity::Medium,
889 confidence: None,
890 span: None,
891 }
892 }
893
894 pub fn with_severity(mut self, severity: DetectionSeverity) -> Self {
896 self.severity = severity;
897 self
898 }
899
900 pub fn with_confidence(mut self, confidence: f64) -> Self {
902 self.confidence = Some(confidence);
903 self
904 }
905
906 pub fn with_span(mut self, start: usize, end: usize) -> Self {
908 self.span = Some(TextSpan { start, end });
909 self
910 }
911}
912
913#[derive(Debug, Clone, Serialize, Deserialize)]
915pub struct TextSpan {
916 pub start: usize,
918 pub end: usize,
920}
921
922#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
924#[serde(rename_all = "lowercase")]
925pub enum DetectionSeverity {
926 Low,
928 #[default]
930 Medium,
931 High,
933 Critical,
935}