Skip to main content

helm_sdk/
lib.rs

1//! HELM SDK — Rust client for the HELM kernel API.
2//! Minimal deps: reqwest + serde.
3
4use reqwest::blocking::Client;
5use std::time::Duration;
6
7pub mod client;
8pub mod types_gen;
9pub use types_gen::*;
10
11/// Error returned by HELM API calls.
12#[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
31/// Typed client for the HELM kernel API.
32pub struct HelmClient {
33    base_url: String,
34    client: Client,
35}
36
37impl HelmClient {
38    /// Create a new client.
39    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    /// POST /v1/chat/completions
76    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    /// POST /api/v1/kernel/approve
99    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    /// GET /api/v1/proofgraph/sessions
119    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    /// GET /api/v1/proofgraph/sessions/{id}/receipts
138    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    /// POST /api/v1/evidence/export — returns raw bytes
160    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    /// POST /api/v1/evidence/verify
186    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    /// POST /api/v1/replay/verify
213    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    /// GET /api/v1/proofgraph/receipts/{hash}
240    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    /// POST /api/v1/conformance/run
262    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    /// GET /api/v1/conformance/reports/{id}
285    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    /// GET /healthz
310    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    /// GET /version
329    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}