zentinel_agent_sdk/lib.rs
1//! Zentinel Agent SDK - Build proxy agents with less boilerplate.
2//!
3//! This crate provides a high-level SDK for building Zentinel proxy agents.
4//! It wraps the low-level protocol with ergonomic types and handles common
5//! patterns like CLI parsing, logging setup, and graceful shutdown.
6//!
7//! # Quick Start
8//!
9//! ```ignore
10//! use zentinel_agent_sdk::prelude::*;
11//!
12//! struct MyAgent;
13//!
14//! #[async_trait]
15//! impl Agent for MyAgent {
16//! async fn on_request(&self, request: &Request) -> Decision {
17//! if request.path_starts_with("/admin") && request.header("x-admin-token").is_none() {
18//! Decision::deny().with_body("Admin access required")
19//! } else {
20//! Decision::allow()
21//! }
22//! }
23//! }
24//!
25//! #[tokio::main]
26//! async fn main() {
27//! AgentRunner::new(MyAgent)
28//! .with_name("my-agent")
29//! .run()
30//! .await
31//! .unwrap();
32//! }
33//! ```
34//!
35//! # V2 Protocol Support
36//!
37//! The SDK supports the v2 agent protocol with gRPC and UDS transports:
38//!
39//! ```ignore
40//! use zentinel_agent_sdk::v2::{AgentRunnerV2, TransportConfig};
41//!
42//! AgentRunnerV2::new(MyAgent)
43//! .with_name("my-agent")
44//! .with_uds("/tmp/my-agent.sock")
45//! .run()
46//! .await?;
47//! ```
48//!
49//! # Features
50//!
51//! - **Simplified types**: `Request`, `Response`, and `Decision` provide ergonomic APIs
52//! - **Fluent decision builder**: Chain methods to build complex responses
53//! - **Configuration handling**: Receive config from proxy's KDL file
54//! - **CLI support**: Built-in argument parsing with clap (optional)
55//! - **Logging**: Automatic tracing setup
56//! - **V2 protocol**: Support for gRPC and UDS transports
57//!
58//! # Crate Features
59//!
60//! - `cli` (default): Enable CLI argument parsing with clap
61//! - `macros` (default): Enable derive macros
62//! - `v2` (default): Enable v2 protocol support with AgentRunnerV2
63
64mod agent;
65mod decision;
66mod request;
67mod response;
68mod runner;
69
70#[cfg(feature = "v2")]
71pub mod v2;
72
73pub use agent::{Agent, AgentHandler, ConfigurableAgent, ConfigurableAgentExt};
74pub use decision::{decisions, Decision};
75pub use request::Request;
76pub use response::Response;
77pub use runner::{AgentRunner, RunnerConfig};
78
79// Re-export commonly used items from dependencies
80pub use async_trait::async_trait;
81pub use serde;
82pub use serde_json;
83pub use tokio;
84pub use tracing;
85
86// Re-export protocol types that users might need
87pub use zentinel_agent_protocol::{
88 AgentResponse, ConfigureEvent, Decision as ProtocolDecision, DetectionSeverity,
89 GuardrailDetection, GuardrailInspectEvent, GuardrailInspectionType, GuardrailResponse,
90 HeaderOp, RequestHeadersEvent, RequestMetadata, ResponseHeadersEvent, TextSpan,
91};
92
93/// Prelude module for convenient imports.
94///
95/// ```ignore
96/// use zentinel_agent_sdk::prelude::*;
97/// ```
98pub mod prelude {
99 pub use crate::agent::{Agent, ConfigurableAgent, ConfigurableAgentExt};
100 pub use crate::decision::{decisions, Decision};
101 pub use crate::request::Request;
102 pub use crate::response::Response;
103 pub use crate::runner::{AgentRunner, RunnerConfig};
104 pub use async_trait::async_trait;
105}
106
107/// Testing utilities for agent development.
108#[cfg(feature = "testing")]
109pub mod testing;
110
111#[cfg(test)]
112mod tests {
113 use super::prelude::*;
114
115 struct ExampleAgent;
116
117 #[async_trait]
118 impl Agent for ExampleAgent {
119 fn name(&self) -> &str {
120 "example"
121 }
122
123 async fn on_request(&self, request: &Request) -> Decision {
124 // Check for blocked paths
125 if request.path_starts_with("/blocked") {
126 return Decision::deny().with_body("Access denied");
127 }
128
129 // Check for required header
130 if request.path_starts_with("/api") {
131 if request.header("x-api-key").is_none() {
132 return Decision::unauthorized()
133 .with_body("API key required")
134 .with_tag("missing-api-key");
135 }
136 }
137
138 // Add request context
139 Decision::allow()
140 .add_request_header("X-Processed-By", "example-agent")
141 .add_request_header("X-Client-IP", request.client_ip())
142 }
143
144 async fn on_response(&self, _request: &Request, response: &Response) -> Decision {
145 // Add security headers to HTML responses
146 if response.is_html() {
147 Decision::allow()
148 .add_response_header("X-Content-Type-Options", "nosniff")
149 .add_response_header("X-Frame-Options", "DENY")
150 } else {
151 Decision::allow()
152 }
153 }
154 }
155
156 #[test]
157 fn test_prelude_imports() {
158 // Verify prelude provides necessary types
159 let _decision: Decision = Decision::allow();
160 }
161}