1use reqwest::blocking::Client;
5use serde::{Deserialize, Serialize};
6use std::time::Duration;
7
8pub mod client;
9pub mod types_gen;
10pub use types_gen::*;
11
12#[cfg(feature = "codegen")]
14pub mod generated {
15 pub mod kernel {
16 include!("generated/helm.kernel.v1.rs");
17 }
18 pub mod authority {
19 include!("generated/helm.authority.v1.rs");
20 }
21 pub mod effects {
22 include!("generated/helm.effects.v1.rs");
23 }
24 pub mod intervention {
25 include!("generated/helm.intervention.v1.rs");
26 }
27 pub mod truth {
28 include!("generated/helm.truth.v1.rs");
29 }
30}
31
32#[derive(Debug)]
34pub struct HelmApiError {
35 pub status: u16,
36 pub message: String,
37 pub reason_code: ReasonCode,
38}
39
40impl std::fmt::Display for HelmApiError {
41 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42 write!(
43 f,
44 "HELM API {}: {} ({:?})",
45 self.status, self.message, self.reason_code
46 )
47 }
48}
49
50impl std::error::Error for HelmApiError {}
51
52#[derive(Clone, Debug, Serialize, Deserialize)]
53pub struct EvidenceEnvelopeExportRequest {
54 pub manifest_id: String,
55 pub envelope: String,
56 pub native_evidence_hash: String,
57 #[serde(skip_serializing_if = "Option::is_none")]
58 pub subject: Option<String>,
59 #[serde(default, skip_serializing_if = "is_false")]
60 pub experimental: bool,
61}
62
63fn is_false(value: &bool) -> bool {
64 !*value
65}
66
67#[derive(Clone, Debug, Serialize, Deserialize)]
68pub struct EvidenceEnvelopeManifest {
69 pub manifest_id: String,
70 pub envelope: String,
71 pub native_evidence_hash: String,
72 pub native_authority: bool,
73 pub created_at: String,
74 #[serde(default)]
75 pub subject: Option<String>,
76 #[serde(default)]
77 pub statement_hash: Option<String>,
78 #[serde(default)]
79 pub payload_type: Option<String>,
80 #[serde(default)]
81 pub payload_hash: Option<String>,
82 #[serde(default)]
83 pub experimental: bool,
84 #[serde(default)]
85 pub manifest_hash: Option<String>,
86}
87
88pub type EvidenceEnvelopePayload = serde_json::Value;
89pub type ApprovalWebAuthnChallenge = serde_json::Value;
90pub type ApprovalWebAuthnAssertion = serde_json::Value;
91
92#[derive(Clone, Debug, Serialize, Deserialize)]
93pub struct NegativeBoundaryVector {
94 pub id: String,
95 pub category: String,
96 pub trigger: String,
97 pub expected_verdict: String,
98 pub expected_reason_code: String,
99 pub must_emit_receipt: bool,
100 pub must_not_dispatch: bool,
101 #[serde(default)]
102 pub must_bind_evidence: Vec<String>,
103}
104
105#[derive(Clone, Debug, Serialize, Deserialize)]
106pub struct McpRegistryDiscoverRequest {
107 pub server_id: String,
108 #[serde(skip_serializing_if = "Option::is_none")]
109 pub name: Option<String>,
110 #[serde(skip_serializing_if = "Option::is_none")]
111 pub transport: Option<String>,
112 #[serde(skip_serializing_if = "Option::is_none")]
113 pub endpoint: Option<String>,
114 #[serde(default, skip_serializing_if = "Vec::is_empty")]
115 pub tool_names: Vec<String>,
116 #[serde(default = "default_mcp_risk")]
117 pub risk: String,
118 #[serde(skip_serializing_if = "Option::is_none")]
119 pub reason: Option<String>,
120}
121
122fn default_mcp_risk() -> String {
123 "unknown".to_string()
124}
125
126#[derive(Clone, Debug, Serialize, Deserialize)]
127pub struct McpRegistryApprovalRequest {
128 pub server_id: String,
129 pub approver_id: String,
130 pub approval_receipt_id: String,
131 #[serde(skip_serializing_if = "Option::is_none")]
132 pub reason: Option<String>,
133}
134
135#[derive(Clone, Debug, Serialize, Deserialize)]
136pub struct McpQuarantineRecord {
137 pub server_id: String,
138 pub risk: String,
139 pub state: String,
140 pub discovered_at: String,
141 #[serde(default)]
142 pub name: Option<String>,
143 #[serde(default)]
144 pub transport: Option<String>,
145 #[serde(default)]
146 pub endpoint: Option<String>,
147 #[serde(default)]
148 pub tool_names: Vec<String>,
149 #[serde(default)]
150 pub approved_at: Option<String>,
151 #[serde(default)]
152 pub approved_by: Option<String>,
153 #[serde(default)]
154 pub approval_receipt_id: Option<String>,
155 #[serde(default)]
156 pub revoked_at: Option<String>,
157 #[serde(default)]
158 pub expires_at: Option<String>,
159 #[serde(default)]
160 pub reason: Option<String>,
161}
162
163#[derive(Clone, Debug, Serialize, Deserialize)]
164pub struct SandboxBackendProfile {
165 pub name: String,
166 pub kind: String,
167 pub runtime: String,
168 pub hosted: bool,
169 pub deny_network_by_default: bool,
170 pub native_isolation: bool,
171 #[serde(default)]
172 pub experimental: bool,
173}
174
175#[derive(Clone, Debug, Serialize, Deserialize)]
176pub struct SandboxGrant {
177 pub grant_id: String,
178 pub runtime: String,
179 pub profile: String,
180 pub env: serde_json::Value,
181 pub network: serde_json::Value,
182 pub declared_at: String,
183 #[serde(default)]
184 pub runtime_version: Option<String>,
185 #[serde(default)]
186 pub image_digest: Option<String>,
187 #[serde(default)]
188 pub template_digest: Option<String>,
189 #[serde(default)]
190 pub filesystem_preopens: Vec<serde_json::Value>,
191 #[serde(default)]
192 pub limits: Option<serde_json::Value>,
193 #[serde(default)]
194 pub policy_epoch: Option<String>,
195 #[serde(default)]
196 pub grant_hash: Option<String>,
197}
198
199#[derive(Clone, Debug, Serialize, Deserialize)]
200#[serde(untagged)]
201pub enum SandboxGrantInspection {
202 Profiles(Vec<SandboxBackendProfile>),
203 Grant(SandboxGrant),
204}
205
206pub struct HelmClient {
208 base_url: String,
209 client: Client,
210}
211
212impl HelmClient {
213 pub fn new(base_url: &str) -> Self {
215 Self {
216 base_url: base_url.trim_end_matches('/').to_string(),
217 client: Client::builder()
218 .timeout(Duration::from_secs(30))
219 .build()
220 .expect("failed to build HTTP client"),
221 }
222 }
223
224 fn url(&self, path: &str) -> String {
225 format!("{}{}", self.base_url, path)
226 }
227
228 fn check(
229 &self,
230 resp: reqwest::blocking::Response,
231 ) -> Result<reqwest::blocking::Response, HelmApiError> {
232 if resp.status().is_success() {
233 return Ok(resp);
234 }
235 let status = resp.status().as_u16();
236 match resp.json::<HelmError>() {
237 Ok(e) => Err(HelmApiError {
238 status,
239 message: e.error.message,
240 reason_code: e.error.reason_code,
241 }),
242 Err(_) => Err(HelmApiError {
243 status,
244 message: "unknown error".into(),
245 reason_code: ReasonCode::ErrorInternal,
246 }),
247 }
248 }
249
250 fn get_value(&self, path: &str) -> Result<serde_json::Value, HelmApiError> {
251 let resp = self
252 .client
253 .get(self.url(path))
254 .send()
255 .map_err(|e| HelmApiError {
256 status: 0,
257 message: e.to_string(),
258 reason_code: ReasonCode::ErrorInternal,
259 })?;
260 let resp = self.check(resp)?;
261 resp.json().map_err(|e| HelmApiError {
262 status: 0,
263 message: e.to_string(),
264 reason_code: ReasonCode::ErrorInternal,
265 })
266 }
267
268 fn post_value<T: Serialize>(
269 &self,
270 path: &str,
271 body: &T,
272 ) -> Result<serde_json::Value, HelmApiError> {
273 let resp = self
274 .client
275 .post(self.url(path))
276 .json(body)
277 .send()
278 .map_err(|e| HelmApiError {
279 status: 0,
280 message: e.to_string(),
281 reason_code: ReasonCode::ErrorInternal,
282 })?;
283 let resp = self.check(resp)?;
284 resp.json().map_err(|e| HelmApiError {
285 status: 0,
286 message: e.to_string(),
287 reason_code: ReasonCode::ErrorInternal,
288 })
289 }
290
291 fn put_value<T: Serialize>(
292 &self,
293 path: &str,
294 body: &T,
295 ) -> Result<serde_json::Value, HelmApiError> {
296 let resp = self
297 .client
298 .put(self.url(path))
299 .json(body)
300 .send()
301 .map_err(|e| HelmApiError {
302 status: 0,
303 message: e.to_string(),
304 reason_code: ReasonCode::ErrorInternal,
305 })?;
306 let resp = self.check(resp)?;
307 resp.json().map_err(|e| HelmApiError {
308 status: 0,
309 message: e.to_string(),
310 reason_code: ReasonCode::ErrorInternal,
311 })
312 }
313
314 pub fn get_boundary_status(&self) -> Result<serde_json::Value, HelmApiError> {
315 self.get_value("/api/v1/boundary/status")
316 }
317
318 pub fn list_boundary_capabilities(&self) -> Result<serde_json::Value, HelmApiError> {
319 self.get_value("/api/v1/boundary/capabilities")
320 }
321
322 pub fn list_boundary_records(&self) -> Result<serde_json::Value, HelmApiError> {
323 self.get_value("/api/v1/boundary/records")
324 }
325
326 pub fn get_boundary_record(&self, record_id: &str) -> Result<serde_json::Value, HelmApiError> {
327 self.get_value(&format!(
328 "/api/v1/boundary/records/{}",
329 encode_query(record_id)
330 ))
331 }
332
333 pub fn verify_boundary_record(
334 &self,
335 record_id: &str,
336 ) -> Result<serde_json::Value, HelmApiError> {
337 self.post_value(
338 &format!(
339 "/api/v1/boundary/records/{}/verify",
340 encode_query(record_id)
341 ),
342 &serde_json::json!({}),
343 )
344 }
345
346 pub fn list_boundary_checkpoints(&self) -> Result<serde_json::Value, HelmApiError> {
347 self.get_value("/api/v1/boundary/checkpoints")
348 }
349
350 pub fn create_boundary_checkpoint(&self) -> Result<serde_json::Value, HelmApiError> {
351 self.post_value("/api/v1/boundary/checkpoints", &serde_json::json!({}))
352 }
353
354 pub fn verify_boundary_checkpoint(
355 &self,
356 checkpoint_id: &str,
357 ) -> Result<serde_json::Value, HelmApiError> {
358 self.post_value(
359 &format!(
360 "/api/v1/boundary/checkpoints/{}/verify",
361 encode_query(checkpoint_id)
362 ),
363 &serde_json::json!({}),
364 )
365 }
366
367 pub fn chat_completions(
369 &self,
370 req: &ChatCompletionRequest,
371 ) -> Result<ChatCompletionResponse, HelmApiError> {
372 let resp = self
373 .client
374 .post(self.url("/v1/chat/completions"))
375 .json(req)
376 .send()
377 .map_err(|e| HelmApiError {
378 status: 0,
379 message: e.to_string(),
380 reason_code: ReasonCode::ErrorInternal,
381 })?;
382 let resp = self.check(resp)?;
383 resp.json().map_err(|e| HelmApiError {
384 status: 0,
385 message: e.to_string(),
386 reason_code: ReasonCode::ErrorInternal,
387 })
388 }
389
390 pub fn evaluate_decision<T: Serialize>(
392 &self,
393 req: &T,
394 ) -> Result<serde_json::Value, HelmApiError> {
395 self.post_value("/api/v1/evaluate", req)
396 }
397
398 pub fn approve_intent(&self, req: &ApprovalRequest) -> Result<Receipt, HelmApiError> {
400 let resp = self
401 .client
402 .post(self.url("/api/v1/kernel/approve"))
403 .json(req)
404 .send()
405 .map_err(|e| HelmApiError {
406 status: 0,
407 message: e.to_string(),
408 reason_code: ReasonCode::ErrorInternal,
409 })?;
410 let resp = self.check(resp)?;
411 resp.json().map_err(|e| HelmApiError {
412 status: 0,
413 message: e.to_string(),
414 reason_code: ReasonCode::ErrorInternal,
415 })
416 }
417
418 pub fn list_sessions(&self) -> Result<Vec<Session>, HelmApiError> {
420 let resp = self
421 .client
422 .get(self.url("/api/v1/proofgraph/sessions"))
423 .send()
424 .map_err(|e| HelmApiError {
425 status: 0,
426 message: e.to_string(),
427 reason_code: ReasonCode::ErrorInternal,
428 })?;
429 let resp = self.check(resp)?;
430 resp.json().map_err(|e| HelmApiError {
431 status: 0,
432 message: e.to_string(),
433 reason_code: ReasonCode::ErrorInternal,
434 })
435 }
436
437 pub fn get_receipts(&self, session_id: &str) -> Result<Vec<Receipt>, HelmApiError> {
439 let resp = self
440 .client
441 .get(self.url(&format!(
442 "/api/v1/proofgraph/sessions/{}/receipts",
443 session_id
444 )))
445 .send()
446 .map_err(|e| HelmApiError {
447 status: 0,
448 message: e.to_string(),
449 reason_code: ReasonCode::ErrorInternal,
450 })?;
451 let resp = self.check(resp)?;
452 resp.json().map_err(|e| HelmApiError {
453 status: 0,
454 message: e.to_string(),
455 reason_code: ReasonCode::ErrorInternal,
456 })
457 }
458
459 pub fn export_evidence(&self, session_id: Option<&str>) -> Result<Vec<u8>, HelmApiError> {
461 let body = serde_json::json!({
462 "session_id": session_id,
463 "format": "tar.gz"
464 });
465 let resp = self
466 .client
467 .post(self.url("/api/v1/evidence/export"))
468 .json(&body)
469 .send()
470 .map_err(|e| HelmApiError {
471 status: 0,
472 message: e.to_string(),
473 reason_code: ReasonCode::ErrorInternal,
474 })?;
475 let resp = self.check(resp)?;
476 resp.bytes().map(|b| b.to_vec()).map_err(|e| HelmApiError {
477 status: 0,
478 message: e.to_string(),
479 reason_code: ReasonCode::ErrorInternal,
480 })
481 }
482
483 pub fn verify_evidence(&self, bundle: &[u8]) -> Result<VerificationResult, HelmApiError> {
485 let form = reqwest::blocking::multipart::Form::new().part(
486 "bundle",
487 reqwest::blocking::multipart::Part::bytes(bundle.to_vec())
488 .file_name("pack.tar.gz")
489 .mime_str("application/octet-stream")
490 .unwrap(),
491 );
492 let resp = self
493 .client
494 .post(self.url("/api/v1/evidence/verify"))
495 .multipart(form)
496 .send()
497 .map_err(|e| HelmApiError {
498 status: 0,
499 message: e.to_string(),
500 reason_code: ReasonCode::ErrorInternal,
501 })?;
502 let resp = self.check(resp)?;
503 resp.json().map_err(|e| HelmApiError {
504 status: 0,
505 message: e.to_string(),
506 reason_code: ReasonCode::ErrorInternal,
507 })
508 }
509
510 pub fn replay_verify(&self, bundle: &[u8]) -> Result<VerificationResult, HelmApiError> {
512 let form = reqwest::blocking::multipart::Form::new().part(
513 "bundle",
514 reqwest::blocking::multipart::Part::bytes(bundle.to_vec())
515 .file_name("pack.tar.gz")
516 .mime_str("application/octet-stream")
517 .unwrap(),
518 );
519 let resp = self
520 .client
521 .post(self.url("/api/v1/replay/verify"))
522 .multipart(form)
523 .send()
524 .map_err(|e| HelmApiError {
525 status: 0,
526 message: e.to_string(),
527 reason_code: ReasonCode::ErrorInternal,
528 })?;
529 let resp = self.check(resp)?;
530 resp.json().map_err(|e| HelmApiError {
531 status: 0,
532 message: e.to_string(),
533 reason_code: ReasonCode::ErrorInternal,
534 })
535 }
536
537 pub fn create_evidence_envelope_manifest(
539 &self,
540 req: &EvidenceEnvelopeExportRequest,
541 ) -> Result<EvidenceEnvelopeManifest, HelmApiError> {
542 let resp = self
543 .client
544 .post(self.url("/api/v1/evidence/envelopes"))
545 .json(req)
546 .send()
547 .map_err(|e| HelmApiError {
548 status: 0,
549 message: e.to_string(),
550 reason_code: ReasonCode::ErrorInternal,
551 })?;
552 let resp = self.check(resp)?;
553 resp.json().map_err(|e| HelmApiError {
554 status: 0,
555 message: e.to_string(),
556 reason_code: ReasonCode::ErrorInternal,
557 })
558 }
559
560 pub fn list_evidence_envelope_manifests(&self) -> Result<serde_json::Value, HelmApiError> {
561 self.get_value("/api/v1/evidence/envelopes")
562 }
563
564 pub fn get_evidence_envelope_manifest(
565 &self,
566 manifest_id: &str,
567 ) -> Result<serde_json::Value, HelmApiError> {
568 self.get_value(&format!(
569 "/api/v1/evidence/envelopes/{}",
570 encode_query(manifest_id)
571 ))
572 }
573
574 pub fn get_evidence_envelope_payload(
575 &self,
576 manifest_id: &str,
577 ) -> Result<EvidenceEnvelopePayload, HelmApiError> {
578 self.get_value(&format!(
579 "/api/v1/evidence/envelopes/{}/payload",
580 encode_query(manifest_id)
581 ))
582 }
583
584 pub fn verify_evidence_envelope_manifest(
585 &self,
586 manifest_id: &str,
587 ) -> Result<serde_json::Value, HelmApiError> {
588 self.post_value(
589 &format!(
590 "/api/v1/evidence/envelopes/{}/verify",
591 encode_query(manifest_id)
592 ),
593 &serde_json::json!({}),
594 )
595 }
596
597 pub fn get_receipt(&self, receipt_hash: &str) -> Result<Receipt, HelmApiError> {
599 let resp = self
600 .client
601 .get(self.url(&format!("/api/v1/proofgraph/receipts/{}", receipt_hash)))
602 .send()
603 .map_err(|e| HelmApiError {
604 status: 0,
605 message: e.to_string(),
606 reason_code: ReasonCode::ErrorInternal,
607 })?;
608 let resp = self.check(resp)?;
609 resp.json().map_err(|e| HelmApiError {
610 status: 0,
611 message: e.to_string(),
612 reason_code: ReasonCode::ErrorInternal,
613 })
614 }
615
616 pub fn conformance_run(
618 &self,
619 req: &ConformanceRequest,
620 ) -> Result<ConformanceResult, HelmApiError> {
621 let resp = self
622 .client
623 .post(self.url("/api/v1/conformance/run"))
624 .json(req)
625 .send()
626 .map_err(|e| HelmApiError {
627 status: 0,
628 message: e.to_string(),
629 reason_code: ReasonCode::ErrorInternal,
630 })?;
631 let resp = self.check(resp)?;
632 resp.json().map_err(|e| HelmApiError {
633 status: 0,
634 message: e.to_string(),
635 reason_code: ReasonCode::ErrorInternal,
636 })
637 }
638
639 pub fn get_conformance_report(
641 &self,
642 report_id: &str,
643 ) -> Result<ConformanceResult, HelmApiError> {
644 let resp = self
645 .client
646 .get(self.url(&format!("/api/v1/conformance/reports/{}", report_id)))
647 .send()
648 .map_err(|e| HelmApiError {
649 status: 0,
650 message: e.to_string(),
651 reason_code: ReasonCode::ErrorInternal,
652 })?;
653 let resp = self.check(resp)?;
654 resp.json().map_err(|e| HelmApiError {
655 status: 0,
656 message: e.to_string(),
657 reason_code: ReasonCode::ErrorInternal,
658 })
659 }
660
661 pub fn list_negative_conformance_vectors(
663 &self,
664 ) -> Result<Vec<NegativeBoundaryVector>, HelmApiError> {
665 let resp = self
666 .client
667 .get(self.url("/api/v1/conformance/negative"))
668 .send()
669 .map_err(|e| HelmApiError {
670 status: 0,
671 message: e.to_string(),
672 reason_code: ReasonCode::ErrorInternal,
673 })?;
674 let resp = self.check(resp)?;
675 resp.json().map_err(|e| HelmApiError {
676 status: 0,
677 message: e.to_string(),
678 reason_code: ReasonCode::ErrorInternal,
679 })
680 }
681
682 pub fn list_conformance_reports(&self) -> Result<serde_json::Value, HelmApiError> {
683 self.get_value("/api/v1/conformance/reports")
684 }
685
686 pub fn list_conformance_vectors(&self) -> Result<serde_json::Value, HelmApiError> {
687 self.get_value("/api/v1/conformance/vectors")
688 }
689
690 pub fn list_mcp_registry(&self) -> Result<Vec<McpQuarantineRecord>, HelmApiError> {
692 let resp = self
693 .client
694 .get(self.url("/api/v1/mcp/registry"))
695 .send()
696 .map_err(|e| HelmApiError {
697 status: 0,
698 message: e.to_string(),
699 reason_code: ReasonCode::ErrorInternal,
700 })?;
701 let resp = self.check(resp)?;
702 resp.json().map_err(|e| HelmApiError {
703 status: 0,
704 message: e.to_string(),
705 reason_code: ReasonCode::ErrorInternal,
706 })
707 }
708
709 pub fn discover_mcp_server(
711 &self,
712 req: &McpRegistryDiscoverRequest,
713 ) -> Result<McpQuarantineRecord, HelmApiError> {
714 let resp = self
715 .client
716 .post(self.url("/api/v1/mcp/registry"))
717 .json(req)
718 .send()
719 .map_err(|e| HelmApiError {
720 status: 0,
721 message: e.to_string(),
722 reason_code: ReasonCode::ErrorInternal,
723 })?;
724 let resp = self.check(resp)?;
725 resp.json().map_err(|e| HelmApiError {
726 status: 0,
727 message: e.to_string(),
728 reason_code: ReasonCode::ErrorInternal,
729 })
730 }
731
732 pub fn approve_mcp_server(
734 &self,
735 req: &McpRegistryApprovalRequest,
736 ) -> Result<McpQuarantineRecord, HelmApiError> {
737 let resp = self
738 .client
739 .post(self.url("/api/v1/mcp/registry/approve"))
740 .json(req)
741 .send()
742 .map_err(|e| HelmApiError {
743 status: 0,
744 message: e.to_string(),
745 reason_code: ReasonCode::ErrorInternal,
746 })?;
747 let resp = self.check(resp)?;
748 resp.json().map_err(|e| HelmApiError {
749 status: 0,
750 message: e.to_string(),
751 reason_code: ReasonCode::ErrorInternal,
752 })
753 }
754
755 pub fn get_mcp_registry_record(
756 &self,
757 server_id: &str,
758 ) -> Result<McpQuarantineRecord, HelmApiError> {
759 let resp = self
760 .client
761 .get(self.url(&format!("/api/v1/mcp/registry/{}", encode_query(server_id))))
762 .send()
763 .map_err(|e| HelmApiError {
764 status: 0,
765 message: e.to_string(),
766 reason_code: ReasonCode::ErrorInternal,
767 })?;
768 let resp = self.check(resp)?;
769 resp.json().map_err(|e| HelmApiError {
770 status: 0,
771 message: e.to_string(),
772 reason_code: ReasonCode::ErrorInternal,
773 })
774 }
775
776 pub fn approve_mcp_registry_record(
777 &self,
778 server_id: &str,
779 req: &McpRegistryApprovalRequest,
780 ) -> Result<McpQuarantineRecord, HelmApiError> {
781 let resp = self
782 .client
783 .post(self.url(&format!(
784 "/api/v1/mcp/registry/{}/approve",
785 encode_query(server_id)
786 )))
787 .json(req)
788 .send()
789 .map_err(|e| HelmApiError {
790 status: 0,
791 message: e.to_string(),
792 reason_code: ReasonCode::ErrorInternal,
793 })?;
794 let resp = self.check(resp)?;
795 resp.json().map_err(|e| HelmApiError {
796 status: 0,
797 message: e.to_string(),
798 reason_code: ReasonCode::ErrorInternal,
799 })
800 }
801
802 pub fn revoke_mcp_registry_record(
803 &self,
804 server_id: &str,
805 reason: Option<&str>,
806 ) -> Result<McpQuarantineRecord, HelmApiError> {
807 let body = serde_json::json!({ "reason": reason.unwrap_or("") });
808 let resp = self
809 .client
810 .post(self.url(&format!(
811 "/api/v1/mcp/registry/{}/revoke",
812 encode_query(server_id)
813 )))
814 .json(&body)
815 .send()
816 .map_err(|e| HelmApiError {
817 status: 0,
818 message: e.to_string(),
819 reason_code: ReasonCode::ErrorInternal,
820 })?;
821 let resp = self.check(resp)?;
822 resp.json().map_err(|e| HelmApiError {
823 status: 0,
824 message: e.to_string(),
825 reason_code: ReasonCode::ErrorInternal,
826 })
827 }
828
829 pub fn scan_mcp_server<T: Serialize>(
830 &self,
831 req: &T,
832 ) -> Result<serde_json::Value, HelmApiError> {
833 self.post_value("/api/v1/mcp/scan", req)
834 }
835
836 pub fn list_mcp_auth_profiles(&self) -> Result<serde_json::Value, HelmApiError> {
837 self.get_value("/api/v1/mcp/auth-profiles")
838 }
839
840 pub fn put_mcp_auth_profile<T: Serialize>(
841 &self,
842 profile_id: &str,
843 profile: &T,
844 ) -> Result<serde_json::Value, HelmApiError> {
845 self.put_value(
846 &format!("/api/v1/mcp/auth-profiles/{}", encode_query(profile_id)),
847 profile,
848 )
849 }
850
851 pub fn authorize_mcp_call<T: Serialize>(
852 &self,
853 req: &T,
854 ) -> Result<serde_json::Value, HelmApiError> {
855 self.post_value("/api/v1/mcp/authorize-call", req)
856 }
857
858 pub fn inspect_sandbox_grants(
860 &self,
861 runtime: Option<&str>,
862 profile: Option<&str>,
863 policy_epoch: Option<&str>,
864 ) -> Result<SandboxGrantInspection, HelmApiError> {
865 let mut path = "/api/v1/sandbox/grants/inspect".to_string();
866 let mut params = Vec::new();
867 if let Some(runtime) = runtime {
868 params.push(format!("runtime={}", encode_query(runtime)));
869 }
870 if let Some(profile) = profile {
871 params.push(format!("profile={}", encode_query(profile)));
872 }
873 if let Some(policy_epoch) = policy_epoch {
874 params.push(format!("policy_epoch={}", encode_query(policy_epoch)));
875 }
876 if !params.is_empty() {
877 path.push('?');
878 path.push_str(¶ms.join("&"));
879 }
880 let resp = self
881 .client
882 .get(self.url(&path))
883 .send()
884 .map_err(|e| HelmApiError {
885 status: 0,
886 message: e.to_string(),
887 reason_code: ReasonCode::ErrorInternal,
888 })?;
889 let resp = self.check(resp)?;
890 resp.json().map_err(|e| HelmApiError {
891 status: 0,
892 message: e.to_string(),
893 reason_code: ReasonCode::ErrorInternal,
894 })
895 }
896
897 pub fn list_sandbox_profiles(&self) -> Result<serde_json::Value, HelmApiError> {
898 self.get_value("/api/v1/sandbox/profiles")
899 }
900
901 pub fn list_sandbox_grants(&self) -> Result<serde_json::Value, HelmApiError> {
902 self.get_value("/api/v1/sandbox/grants")
903 }
904
905 pub fn create_sandbox_grant<T: Serialize>(
906 &self,
907 req: &T,
908 ) -> Result<serde_json::Value, HelmApiError> {
909 self.post_value("/api/v1/sandbox/grants", req)
910 }
911
912 pub fn get_sandbox_grant(&self, grant_id: &str) -> Result<serde_json::Value, HelmApiError> {
913 self.get_value(&format!(
914 "/api/v1/sandbox/grants/{}",
915 encode_query(grant_id)
916 ))
917 }
918
919 pub fn verify_sandbox_grant(&self, grant_id: &str) -> Result<serde_json::Value, HelmApiError> {
920 self.post_value(
921 &format!("/api/v1/sandbox/grants/{}/verify", encode_query(grant_id)),
922 &serde_json::json!({}),
923 )
924 }
925
926 pub fn preflight_sandbox_grant<T: Serialize>(
927 &self,
928 req: &T,
929 ) -> Result<serde_json::Value, HelmApiError> {
930 self.post_value("/api/v1/sandbox/preflight", req)
931 }
932
933 pub fn list_agent_identities(&self) -> Result<serde_json::Value, HelmApiError> {
934 self.get_value("/api/v1/identity/agents")
935 }
936
937 pub fn get_authz_health(&self) -> Result<serde_json::Value, HelmApiError> {
938 self.get_value("/api/v1/authz/health")
939 }
940
941 pub fn check_authz<T: Serialize>(&self, req: &T) -> Result<serde_json::Value, HelmApiError> {
942 self.post_value("/api/v1/authz/check", req)
943 }
944
945 pub fn list_authz_snapshots(&self) -> Result<serde_json::Value, HelmApiError> {
946 self.get_value("/api/v1/authz/snapshots")
947 }
948
949 pub fn get_authz_snapshot(&self, snapshot_id: &str) -> Result<serde_json::Value, HelmApiError> {
950 self.get_value(&format!(
951 "/api/v1/authz/snapshots/{}",
952 encode_query(snapshot_id)
953 ))
954 }
955
956 pub fn list_approval_ceremonies(&self) -> Result<serde_json::Value, HelmApiError> {
957 self.get_value("/api/v1/approvals")
958 }
959
960 pub fn create_approval_ceremony<T: Serialize>(
961 &self,
962 req: &T,
963 ) -> Result<serde_json::Value, HelmApiError> {
964 self.post_value("/api/v1/approvals", req)
965 }
966
967 pub fn transition_approval_ceremony<T: Serialize>(
968 &self,
969 approval_id: &str,
970 action: &str,
971 req: &T,
972 ) -> Result<serde_json::Value, HelmApiError> {
973 self.post_value(
974 &format!(
975 "/api/v1/approvals/{}/{}",
976 encode_query(approval_id),
977 encode_query(action)
978 ),
979 req,
980 )
981 }
982
983 pub fn create_approval_webauthn_challenge<T: Serialize>(
984 &self,
985 approval_id: &str,
986 req: &T,
987 ) -> Result<ApprovalWebAuthnChallenge, HelmApiError> {
988 self.post_value(
989 &format!(
990 "/api/v1/approvals/{}/webauthn/challenge",
991 encode_query(approval_id)
992 ),
993 req,
994 )
995 }
996
997 pub fn assert_approval_webauthn_challenge<T: Serialize>(
998 &self,
999 approval_id: &str,
1000 req: &T,
1001 ) -> Result<serde_json::Value, HelmApiError> {
1002 self.post_value(
1003 &format!(
1004 "/api/v1/approvals/{}/webauthn/assert",
1005 encode_query(approval_id)
1006 ),
1007 req,
1008 )
1009 }
1010
1011 pub fn list_budget_ceilings(&self) -> Result<serde_json::Value, HelmApiError> {
1012 self.get_value("/api/v1/budgets")
1013 }
1014
1015 pub fn put_budget_ceiling<T: Serialize>(
1016 &self,
1017 budget_id: &str,
1018 req: &T,
1019 ) -> Result<serde_json::Value, HelmApiError> {
1020 self.put_value(&format!("/api/v1/budgets/{}", encode_query(budget_id)), req)
1021 }
1022
1023 pub fn get_coexistence_capabilities(&self) -> Result<serde_json::Value, HelmApiError> {
1024 self.get_value("/api/v1/coexistence/capabilities")
1025 }
1026
1027 pub fn get_telemetry_otel_config(&self) -> Result<serde_json::Value, HelmApiError> {
1028 self.get_value("/api/v1/telemetry/otel/config")
1029 }
1030
1031 pub fn export_telemetry<T: Serialize>(
1032 &self,
1033 req: &T,
1034 ) -> Result<serde_json::Value, HelmApiError> {
1035 self.post_value("/api/v1/telemetry/export", req)
1036 }
1037
1038 pub fn health(&self) -> Result<serde_json::Value, HelmApiError> {
1040 let resp = self
1041 .client
1042 .get(self.url("/healthz"))
1043 .send()
1044 .map_err(|e| HelmApiError {
1045 status: 0,
1046 message: e.to_string(),
1047 reason_code: ReasonCode::ErrorInternal,
1048 })?;
1049 let resp = self.check(resp)?;
1050 resp.json().map_err(|e| HelmApiError {
1051 status: 0,
1052 message: e.to_string(),
1053 reason_code: ReasonCode::ErrorInternal,
1054 })
1055 }
1056
1057 pub fn version(&self) -> Result<VersionInfo, HelmApiError> {
1059 let resp = self
1060 .client
1061 .get(self.url("/version"))
1062 .send()
1063 .map_err(|e| HelmApiError {
1064 status: 0,
1065 message: e.to_string(),
1066 reason_code: ReasonCode::ErrorInternal,
1067 })?;
1068 let resp = self.check(resp)?;
1069 resp.json().map_err(|e| HelmApiError {
1070 status: 0,
1071 message: e.to_string(),
1072 reason_code: ReasonCode::ErrorInternal,
1073 })
1074 }
1075}
1076
1077fn encode_query(value: &str) -> String {
1078 value
1079 .bytes()
1080 .flat_map(|b| match b {
1081 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
1082 vec![b as char]
1083 }
1084 _ => format!("%{b:02X}").chars().collect(),
1085 })
1086 .collect()
1087}
1088
1089#[cfg(test)]
1090mod tests {
1091 use super::*;
1092
1093 #[test]
1094 fn test_client_creation() {
1095 let _client = HelmClient::new("http://localhost:8080");
1096 }
1097
1098 #[test]
1099 fn test_reason_code_serde() {
1100 let code = ReasonCode::DenyToolNotFound;
1101 let json = serde_json::to_string(&code).unwrap();
1102 assert_eq!(json, "\"DENY_TOOL_NOT_FOUND\"");
1103 }
1104
1105 #[test]
1106 fn test_execution_boundary_types_serde() {
1107 let req = EvidenceEnvelopeExportRequest {
1108 manifest_id: "env1".to_string(),
1109 envelope: "dsse".to_string(),
1110 native_evidence_hash: "sha256:native".to_string(),
1111 subject: None,
1112 experimental: false,
1113 };
1114 let json = serde_json::to_string(&req).unwrap();
1115 assert!(json.contains("native_evidence_hash"));
1116
1117 let manifest: EvidenceEnvelopeManifest = serde_json::from_str(
1118 r#"{"manifest_id":"env1","envelope":"dsse","native_evidence_hash":"sha256:native","native_authority":false,"created_at":"2026-05-05T00:00:00Z","payload_type":"application/vnd.dsse+json","payload_hash":"sha256:payload","manifest_hash":"sha256:manifest"}"#,
1119 )
1120 .unwrap();
1121 assert_eq!(manifest.payload_hash.as_deref(), Some("sha256:payload"));
1122
1123 let record: McpQuarantineRecord = serde_json::from_str(
1124 r#"{"server_id":"mcp1","risk":"high","state":"quarantined","discovered_at":"2026-05-05T00:00:00Z"}"#,
1125 )
1126 .unwrap();
1127 assert_eq!(record.server_id, "mcp1");
1128
1129 let grant: SandboxGrant = serde_json::from_str(
1130 r#"{"grant_id":"grant1","runtime":"wazero","profile":"deny-default","env":{"mode":"deny-all"},"network":{"mode":"deny-all"},"declared_at":"2026-05-05T00:00:00Z"}"#,
1131 )
1132 .unwrap();
1133 assert_eq!(grant.grant_id, "grant1");
1134 }
1135}