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