1use reqwest::blocking::Client;
5use std::time::Duration;
6
7pub mod client;
8pub mod types_gen;
9pub use types_gen::*;
10
11#[derive(Debug)]
13pub struct HelmApiError {
14 pub status: u16,
15 pub message: String,
16 pub reason_code: ReasonCode,
17}
18
19impl std::fmt::Display for HelmApiError {
20 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21 write!(
22 f,
23 "HELM API {}: {} ({:?})",
24 self.status, self.message, self.reason_code
25 )
26 }
27}
28
29impl std::error::Error for HelmApiError {}
30
31pub struct HelmClient {
33 base_url: String,
34 client: Client,
35}
36
37impl HelmClient {
38 pub fn new(base_url: &str) -> Self {
40 Self {
41 base_url: base_url.trim_end_matches('/').to_string(),
42 client: Client::builder()
43 .timeout(Duration::from_secs(30))
44 .build()
45 .expect("failed to build HTTP client"),
46 }
47 }
48
49 fn url(&self, path: &str) -> String {
50 format!("{}{}", self.base_url, path)
51 }
52
53 fn check(
54 &self,
55 resp: reqwest::blocking::Response,
56 ) -> Result<reqwest::blocking::Response, HelmApiError> {
57 if resp.status().is_success() {
58 return Ok(resp);
59 }
60 let status = resp.status().as_u16();
61 match resp.json::<HelmError>() {
62 Ok(e) => Err(HelmApiError {
63 status,
64 message: e.error.message,
65 reason_code: e.error.reason_code,
66 }),
67 Err(_) => Err(HelmApiError {
68 status,
69 message: "unknown error".into(),
70 reason_code: ReasonCode::ErrorInternal,
71 }),
72 }
73 }
74
75 pub fn chat_completions(
77 &self,
78 req: &ChatCompletionRequest,
79 ) -> Result<ChatCompletionResponse, HelmApiError> {
80 let resp = self
81 .client
82 .post(self.url("/v1/chat/completions"))
83 .json(req)
84 .send()
85 .map_err(|e| HelmApiError {
86 status: 0,
87 message: e.to_string(),
88 reason_code: ReasonCode::ErrorInternal,
89 })?;
90 let resp = self.check(resp)?;
91 resp.json().map_err(|e| HelmApiError {
92 status: 0,
93 message: e.to_string(),
94 reason_code: ReasonCode::ErrorInternal,
95 })
96 }
97
98 pub fn approve_intent(&self, req: &ApprovalRequest) -> Result<Receipt, HelmApiError> {
100 let resp = self
101 .client
102 .post(self.url("/api/v1/kernel/approve"))
103 .json(req)
104 .send()
105 .map_err(|e| HelmApiError {
106 status: 0,
107 message: e.to_string(),
108 reason_code: ReasonCode::ErrorInternal,
109 })?;
110 let resp = self.check(resp)?;
111 resp.json().map_err(|e| HelmApiError {
112 status: 0,
113 message: e.to_string(),
114 reason_code: ReasonCode::ErrorInternal,
115 })
116 }
117
118 pub fn list_sessions(&self) -> Result<Vec<Session>, HelmApiError> {
120 let resp = self
121 .client
122 .get(self.url("/api/v1/proofgraph/sessions"))
123 .send()
124 .map_err(|e| HelmApiError {
125 status: 0,
126 message: e.to_string(),
127 reason_code: ReasonCode::ErrorInternal,
128 })?;
129 let resp = self.check(resp)?;
130 resp.json().map_err(|e| HelmApiError {
131 status: 0,
132 message: e.to_string(),
133 reason_code: ReasonCode::ErrorInternal,
134 })
135 }
136
137 pub fn get_receipts(&self, session_id: &str) -> Result<Vec<Receipt>, HelmApiError> {
139 let resp = self
140 .client
141 .get(self.url(&format!(
142 "/api/v1/proofgraph/sessions/{}/receipts",
143 session_id
144 )))
145 .send()
146 .map_err(|e| HelmApiError {
147 status: 0,
148 message: e.to_string(),
149 reason_code: ReasonCode::ErrorInternal,
150 })?;
151 let resp = self.check(resp)?;
152 resp.json().map_err(|e| HelmApiError {
153 status: 0,
154 message: e.to_string(),
155 reason_code: ReasonCode::ErrorInternal,
156 })
157 }
158
159 pub fn export_evidence(&self, session_id: Option<&str>) -> Result<Vec<u8>, HelmApiError> {
161 let body = serde_json::json!({
162 "session_id": session_id,
163 "format": "tar.gz"
164 });
165 let resp = self
166 .client
167 .post(self.url("/api/v1/evidence/export"))
168 .json(&body)
169 .send()
170 .map_err(|e| HelmApiError {
171 status: 0,
172 message: e.to_string(),
173 reason_code: ReasonCode::ErrorInternal,
174 })?;
175 let resp = self.check(resp)?;
176 resp.bytes()
177 .map(|b| b.to_vec())
178 .map_err(|e| HelmApiError {
179 status: 0,
180 message: e.to_string(),
181 reason_code: ReasonCode::ErrorInternal,
182 })
183 }
184
185 pub fn verify_evidence(&self, bundle: &[u8]) -> Result<VerificationResult, HelmApiError> {
187 let form = reqwest::blocking::multipart::Form::new().part(
188 "bundle",
189 reqwest::blocking::multipart::Part::bytes(bundle.to_vec())
190 .file_name("pack.tar.gz")
191 .mime_str("application/octet-stream")
192 .unwrap(),
193 );
194 let resp = self
195 .client
196 .post(self.url("/api/v1/evidence/verify"))
197 .multipart(form)
198 .send()
199 .map_err(|e| HelmApiError {
200 status: 0,
201 message: e.to_string(),
202 reason_code: ReasonCode::ErrorInternal,
203 })?;
204 let resp = self.check(resp)?;
205 resp.json().map_err(|e| HelmApiError {
206 status: 0,
207 message: e.to_string(),
208 reason_code: ReasonCode::ErrorInternal,
209 })
210 }
211
212 pub fn replay_verify(&self, bundle: &[u8]) -> Result<VerificationResult, HelmApiError> {
214 let form = reqwest::blocking::multipart::Form::new().part(
215 "bundle",
216 reqwest::blocking::multipart::Part::bytes(bundle.to_vec())
217 .file_name("pack.tar.gz")
218 .mime_str("application/octet-stream")
219 .unwrap(),
220 );
221 let resp = self
222 .client
223 .post(self.url("/api/v1/replay/verify"))
224 .multipart(form)
225 .send()
226 .map_err(|e| HelmApiError {
227 status: 0,
228 message: e.to_string(),
229 reason_code: ReasonCode::ErrorInternal,
230 })?;
231 let resp = self.check(resp)?;
232 resp.json().map_err(|e| HelmApiError {
233 status: 0,
234 message: e.to_string(),
235 reason_code: ReasonCode::ErrorInternal,
236 })
237 }
238
239 pub fn get_receipt(&self, receipt_hash: &str) -> Result<Receipt, HelmApiError> {
241 let resp = self
242 .client
243 .get(self.url(&format!(
244 "/api/v1/proofgraph/receipts/{}",
245 receipt_hash
246 )))
247 .send()
248 .map_err(|e| HelmApiError {
249 status: 0,
250 message: e.to_string(),
251 reason_code: ReasonCode::ErrorInternal,
252 })?;
253 let resp = self.check(resp)?;
254 resp.json().map_err(|e| HelmApiError {
255 status: 0,
256 message: e.to_string(),
257 reason_code: ReasonCode::ErrorInternal,
258 })
259 }
260
261 pub fn conformance_run(
263 &self,
264 req: &ConformanceRequest,
265 ) -> Result<ConformanceResult, HelmApiError> {
266 let resp = self
267 .client
268 .post(self.url("/api/v1/conformance/run"))
269 .json(req)
270 .send()
271 .map_err(|e| HelmApiError {
272 status: 0,
273 message: e.to_string(),
274 reason_code: ReasonCode::ErrorInternal,
275 })?;
276 let resp = self.check(resp)?;
277 resp.json().map_err(|e| HelmApiError {
278 status: 0,
279 message: e.to_string(),
280 reason_code: ReasonCode::ErrorInternal,
281 })
282 }
283
284 pub fn get_conformance_report(
286 &self,
287 report_id: &str,
288 ) -> Result<ConformanceResult, HelmApiError> {
289 let resp = self
290 .client
291 .get(self.url(&format!(
292 "/api/v1/conformance/reports/{}",
293 report_id
294 )))
295 .send()
296 .map_err(|e| HelmApiError {
297 status: 0,
298 message: e.to_string(),
299 reason_code: ReasonCode::ErrorInternal,
300 })?;
301 let resp = self.check(resp)?;
302 resp.json().map_err(|e| HelmApiError {
303 status: 0,
304 message: e.to_string(),
305 reason_code: ReasonCode::ErrorInternal,
306 })
307 }
308
309 pub fn health(&self) -> Result<serde_json::Value, HelmApiError> {
311 let resp = self
312 .client
313 .get(self.url("/healthz"))
314 .send()
315 .map_err(|e| HelmApiError {
316 status: 0,
317 message: e.to_string(),
318 reason_code: ReasonCode::ErrorInternal,
319 })?;
320 let resp = self.check(resp)?;
321 resp.json().map_err(|e| HelmApiError {
322 status: 0,
323 message: e.to_string(),
324 reason_code: ReasonCode::ErrorInternal,
325 })
326 }
327
328 pub fn version(&self) -> Result<VersionInfo, HelmApiError> {
330 let resp = self
331 .client
332 .get(self.url("/version"))
333 .send()
334 .map_err(|e| HelmApiError {
335 status: 0,
336 message: e.to_string(),
337 reason_code: ReasonCode::ErrorInternal,
338 })?;
339 let resp = self.check(resp)?;
340 resp.json().map_err(|e| HelmApiError {
341 status: 0,
342 message: e.to_string(),
343 reason_code: ReasonCode::ErrorInternal,
344 })
345 }
346}
347
348#[cfg(test)]
349mod tests {
350 use super::*;
351
352 #[test]
353 fn test_client_creation() {
354 let _client = HelmClient::new("http://localhost:8080");
355 }
356
357 #[test]
358 fn test_reason_code_serde() {
359 let code = ReasonCode::DenyToolNotFound;
360 let json = serde_json::to_string(&code).unwrap();
361 assert_eq!(json, "\"DENY_TOOL_NOT_FOUND\"");
362 }
363}