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