agentkernel/policy/
client.rs1#![cfg(feature = "enterprise")]
7
8use anyhow::{Context as _, Result};
9use std::sync::Arc;
10use std::time::Duration;
11use tokio::sync::watch;
12
13use super::signing::PolicyBundle;
14
15pub struct PolicyClient {
17 server_url: String,
19 api_key: Option<String>,
21 http_client: reqwest::Client,
23}
24
25impl PolicyClient {
26 pub fn new(server_url: &str, api_key: Option<String>) -> Result<Self> {
32 let http_client = reqwest::Client::builder()
33 .timeout(Duration::from_secs(30))
34 .connect_timeout(Duration::from_secs(10))
35 .user_agent(format!("agentkernel/{}", env!("CARGO_PKG_VERSION")))
36 .build()
37 .context("Failed to build HTTP client")?;
38
39 Ok(Self {
40 server_url: server_url.trim_end_matches('/').to_string(),
41 api_key,
42 http_client,
43 })
44 }
45
46 pub async fn fetch_bundle(&self) -> Result<PolicyBundle> {
51 let url = format!("{}/v1/policies", self.server_url);
52
53 let mut request = self.http_client.get(&url);
54
55 if let Some(ref key) = self.api_key {
57 request = request.header("Authorization", format!("Bearer {}", key));
58 }
59
60 let response = request
61 .send()
62 .await
63 .context("Failed to connect to policy server")?;
64
65 if !response.status().is_success() {
66 let status = response.status();
67 let body = response
68 .text()
69 .await
70 .unwrap_or_else(|_| "<no body>".to_string());
71 anyhow::bail!("Policy server returned {}: {}", status, body);
72 }
73
74 let bundle: PolicyBundle = response
75 .json()
76 .await
77 .context("Failed to parse policy bundle response")?;
78
79 Ok(bundle)
80 }
81
82 pub fn poll(
95 self: Arc<Self>,
96 interval: Duration,
97 mut shutdown: watch::Receiver<bool>,
98 ) -> watch::Receiver<Option<PolicyBundle>> {
99 let (tx, rx) = watch::channel(None);
100
101 tokio::spawn(async move {
102 let mut ticker = tokio::time::interval(interval);
103 ticker.tick().await;
105
106 loop {
107 tokio::select! {
108 _ = ticker.tick() => {
109 match self.fetch_bundle().await {
110 Ok(bundle) => {
111 let _ = tx.send(Some(bundle));
112 }
113 Err(e) => {
114 eprintln!("[enterprise] Failed to fetch policy bundle: {}", e);
115 }
116 }
117 }
118 _ = shutdown.changed() => {
119 if *shutdown.borrow() {
120 break;
121 }
122 }
123 }
124 }
125 });
126
127 rx
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn test_client_creation() {
137 let client = PolicyClient::new("https://policy.example.com", None);
138 assert!(client.is_ok());
139 }
140
141 #[test]
142 fn test_client_creation_with_key() {
143 let client = PolicyClient::new(
144 "https://policy.example.com",
145 Some("test-api-key".to_string()),
146 );
147 assert!(client.is_ok());
148 }
149
150 #[test]
151 fn test_server_url_normalization() {
152 let client = PolicyClient::new("https://policy.example.com/", None).unwrap();
153 assert_eq!(client.server_url, "https://policy.example.com");
154 }
155}