Skip to main content

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}