1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use sha2::{Digest, Sha256};
5use std::sync::Arc;
6use std::time::Duration;
7use tonic::Request;
8use tonic::metadata::{Ascii, MetadataValue};
9use tonic::transport::{Certificate, Channel, ClientTlsConfig, Endpoint};
10use zeroize::{Zeroize, Zeroizing};
11
12#[allow(missing_docs, unused_qualifications, clippy::all, clippy::pedantic)]
14pub mod wire {
15 tonic::include_proto!("inference.customer.v1");
16}
17
18pub const DESCRIPTOR: &[u8] = include_bytes!("../inference_descriptor.bin");
20pub const MAXIMUM_MESSAGE_BYTES: usize = 8 * 1024 * 1024;
22pub const MAXIMUM_CA_CERTIFICATE_BYTES: usize = 64 * 1024;
24pub const MAXIMUM_EVALUATION_CANDIDATES: usize = 256;
26pub const MAXIMUM_EVALUATION_CASES: usize = 4_096;
28pub const MAXIMUM_EVALUATION_METRICS: usize = 64;
30pub const MAXIMUM_EVALUATION_RESULTS: u64 = 65_536;
32const INVALID_CA_CERTIFICATE_LENGTH: &str = "CA certificate must contain 1 to 65536 bytes";
33
34impl Drop for wire::Item {
35 fn drop(&mut self) {
36 self.payload.zeroize();
37 }
38}
39
40impl Drop for wire::Replace {
41 fn drop(&mut self) {
42 self.payload.zeroize();
43 }
44}
45
46impl Drop for wire::RunEvent {
47 fn drop(&mut self) {
48 scrub_run_event(self);
49 }
50}
51
52impl Drop for wire::RunResult {
53 fn drop(&mut self) {
54 scrub_run_result(self);
55 }
56}
57
58fn scrub_run_event(event: &mut wire::RunEvent) {
59 if let Some(wire::run_event::Event::Output(output)) = event.event.as_mut() {
60 output.zeroize();
61 }
62}
63
64fn scrub_run_result(result: &mut wire::RunResult) {
65 result.output.zeroize();
66}
67
68#[derive(Debug, thiserror::Error)]
70pub enum Error {
71 #[error("invalid customer contract: {0}")]
73 Invalid(&'static str),
74 #[error("customer transport setup failed: {0}")]
76 Transport(#[from] tonic::transport::Error),
77 #[error("customer operation observation failed: {0}")]
79 Observation(#[from] tonic::Status),
80}
81
82struct Connection {
83 channel: Channel,
84 authorization: Zeroizing<String>,
85 client_instance: [u8; 16],
86}
87
88#[derive(Clone)]
90pub struct Inference(Arc<Connection>);
91
92impl Inference {
93 pub async fn connect(endpoint: &str, api_key: &str, ca_pem: &[u8]) -> Result<Self, Error> {
98 let endpoint = Endpoint::new(endpoint.to_owned())?;
99 if endpoint.uri().scheme_str() != Some("https") {
100 return Err(Error::Invalid("HTTPS is required"));
101 }
102 if ca_pem.is_empty() || ca_pem.len() > MAXIMUM_CA_CERTIFICATE_BYTES {
103 return Err(Error::Invalid(INVALID_CA_CERTIFICATE_LENGTH));
104 }
105 let authorization = authorization(api_key)?;
106 let channel = endpoint
107 .connect_timeout(Duration::from_secs(10))
108 .timeout(Duration::from_secs(60))
109 .http2_keep_alive_interval(Duration::from_secs(30))
110 .keep_alive_timeout(Duration::from_secs(10))
111 .tls_config(
112 ClientTlsConfig::new()
113 .with_enabled_roots()
114 .ca_certificate(Certificate::from_pem(ca_pem)),
115 )?
116 .connect()
117 .await?;
118 Ok(Self(Arc::new(Connection {
119 channel,
120 authorization,
121 client_instance: *uuid::Uuid::now_v7().as_bytes(),
122 })))
123 }
124
125 #[must_use]
127 pub fn context(&self, model: impl Into<String>) -> CreateContext {
128 CreateContext {
129 client: self.clone(),
130 request: wire::CreateContextRequest {
131 identity: Some(self.identity()),
132 model: model.into(),
133 items: Vec::new(),
134 },
135 }
136 }
137
138 pub async fn attach(&self, revision: [u8; 32]) -> Result<Context, Error> {
143 nonzero(&revision)?;
144 let context = Context {
145 client: self.clone(),
146 revision,
147 };
148 context.inspect().await?;
149 Ok(context)
150 }
151
152 fn identity(&self) -> wire::RequestIdentity {
153 wire::RequestIdentity {
154 client_instance: self.0.client_instance.to_vec(),
155 request_id: uuid::Uuid::now_v7().as_bytes().to_vec(),
156 }
157 }
158
159 fn rpc(&self) -> wire::contexts_service_client::ContextsServiceClient<Channel> {
160 wire::contexts_service_client::ContextsServiceClient::new(self.0.channel.clone())
161 .max_decoding_message_size(MAXIMUM_MESSAGE_BYTES)
162 .max_encoding_message_size(MAXIMUM_MESSAGE_BYTES)
163 }
164
165 fn runs(&self) -> wire::runs_service_client::RunsServiceClient<Channel> {
166 wire::runs_service_client::RunsServiceClient::new(self.0.channel.clone())
167 .max_decoding_message_size(MAXIMUM_MESSAGE_BYTES)
168 .max_encoding_message_size(MAXIMUM_MESSAGE_BYTES)
169 }
170
171 fn warm(&self) -> wire::warm_contexts_service_client::WarmContextsServiceClient<Channel> {
172 wire::warm_contexts_service_client::WarmContextsServiceClient::new(self.0.channel.clone())
173 .max_decoding_message_size(MAXIMUM_MESSAGE_BYTES)
174 .max_encoding_message_size(MAXIMUM_MESSAGE_BYTES)
175 }
176
177 fn discovery(&self) -> wire::models_service_client::ModelsServiceClient<Channel> {
178 wire::models_service_client::ModelsServiceClient::new(self.0.channel.clone())
179 .max_decoding_message_size(MAXIMUM_MESSAGE_BYTES)
180 .max_encoding_message_size(MAXIMUM_MESSAGE_BYTES)
181 }
182
183 fn evaluations(&self) -> wire::evaluations_service_client::EvaluationsServiceClient<Channel> {
184 wire::evaluations_service_client::EvaluationsServiceClient::new(self.0.channel.clone())
185 .max_decoding_message_size(MAXIMUM_MESSAGE_BYTES)
186 .max_encoding_message_size(MAXIMUM_MESSAGE_BYTES)
187 }
188
189 #[must_use]
191 pub fn evaluation(&self, spec: wire::EvaluationSpec) -> CreateEvaluation {
192 CreateEvaluation {
193 client: self.clone(),
194 request: wire::CreateEvaluationRequest {
195 identity: Some(self.identity()),
196 spec: Some(spec),
197 },
198 }
199 }
200
201 #[must_use]
203 pub fn recover_evaluation(&self, evaluation_id: [u8; 16]) -> Evaluation {
204 Evaluation {
205 client: self.clone(),
206 evaluation_id,
207 }
208 }
209
210 pub async fn models(&self) -> Result<Vec<wire::ModelCapability>, Error> {
215 let response = self
216 .discovery()
217 .list(self.request(wire::ListModelsRequest {})?)
218 .await?
219 .into_inner();
220 if response.models.is_empty() || response.models.len() > 4_096 {
221 return Err(Error::Invalid("model capability count is invalid"));
222 }
223 let mut names = std::collections::BTreeSet::new();
224 for model in &response.models {
225 let mut retention_profiles = std::collections::BTreeSet::new();
226 if model.model.is_empty()
227 || model.model.len() > 256
228 || !names.insert(model.model.as_str())
229 || fixed::<32>(&model.execution_profile).is_err()
230 || model.maximum_context == 0
231 || model.maximum_output == 0
232 || model.features.is_empty()
233 || model.features.len() > 64
234 || model
235 .features
236 .iter()
237 .any(|feature| feature.is_empty() || feature.len() > 64)
238 || model.retention_profiles.len() > 64
239 || model.retention_profiles.iter().any(|profile| {
240 fixed::<32>(&profile.profile).is_err()
241 || profile.minimum_duration_ms == 0
242 || profile.maximum_duration_ms < profile.minimum_duration_ms
243 || !retention_profiles.insert(profile.profile.as_slice())
244 })
245 {
246 return Err(Error::Invalid("model capability is invalid"));
247 }
248 }
249 Ok(response.models)
250 }
251
252 #[must_use]
254 pub fn recover_run(&self, run_id: [u8; 16]) -> Run {
255 Run {
256 client: self.clone(),
257 run_id,
258 }
259 }
260
261 #[must_use]
263 pub fn recover_warm(&self, commitment: [u8; 32]) -> WarmContext {
264 WarmContext {
265 client: self.clone(),
266 commitment,
267 }
268 }
269
270 fn request<T>(&self, value: T) -> Result<Request<T>, Error> {
271 let mut request = Request::new(value);
272 let mut authorization = MetadataValue::<Ascii>::try_from(self.0.authorization.as_str())
275 .map_err(|_| Error::Invalid("invalid API key"))?;
276 authorization.set_sensitive(true);
277 request
278 .metadata_mut()
279 .insert("authorization", authorization);
280 request.set_timeout(Duration::from_secs(60));
281 Ok(request)
282 }
283}
284
285fn authorization(api_key: &str) -> Result<Zeroizing<String>, Error> {
286 if api_key.is_empty() || api_key.len() > 8_192 {
287 return Err(Error::Invalid("invalid API key"));
288 }
289 let value = Zeroizing::new(format!("Bearer {api_key}"));
290 MetadataValue::<Ascii>::try_from(value.as_str())
291 .map_err(|_| Error::Invalid("invalid API key"))?;
292 Ok(value)
293}
294
295fn nonzero<const N: usize>(value: &[u8; N]) -> Result<(), Error> {
296 if *value == [0; N] {
297 return Err(Error::Invalid("zero identity"));
298 }
299 Ok(())
300}
301
302fn fixed<const N: usize>(value: &[u8]) -> Result<[u8; N], Error> {
303 let bytes = value
304 .try_into()
305 .map_err(|_| Error::Invalid("identity length differs"))?;
306 nonzero(&bytes)?;
307 Ok(bytes)
308}
309
310fn bounded<M: prost::Message>(message: &M) -> Result<(), Error> {
311 if message.encoded_len() > MAXIMUM_MESSAGE_BYTES {
312 return Err(Error::Invalid("message exceeds transport ceiling"));
313 }
314 Ok(())
315}
316
317#[derive(Clone)]
320pub struct CreateEvaluation {
321 client: Inference,
322 request: wire::CreateEvaluationRequest,
323}
324
325impl CreateEvaluation {
326 pub fn id(&self) -> Result<[u8; 16], Error> {
328 fixed(
329 &self
330 .request
331 .identity
332 .as_ref()
333 .ok_or(Error::Invalid("missing evaluation identity"))?
334 .request_id,
335 )
336 }
337
338 pub async fn send(&self) -> Result<Evaluation, Error> {
340 bounded(&self.request)?;
341 let expected = self.id()?;
342 validate_evaluation_spec(
343 self.request
344 .spec
345 .as_ref()
346 .ok_or(Error::Invalid("evaluation spec is absent"))?,
347 )?;
348 let view = self
349 .client
350 .evaluations()
351 .create(self.client.request(self.request.clone())?)
352 .await?
353 .into_inner();
354 validate_evaluation_admission(
355 &view,
356 expected,
357 self.request
358 .spec
359 .as_ref()
360 .ok_or(Error::Invalid("evaluation spec is absent"))?,
361 )?;
362 Ok(Evaluation {
363 client: self.client.clone(),
364 evaluation_id: expected,
365 })
366 }
367}
368
369#[derive(Clone)]
371pub struct Evaluation {
372 client: Inference,
373 evaluation_id: [u8; 16],
374}
375
376impl Evaluation {
377 #[must_use]
379 pub const fn id(&self) -> [u8; 16] {
380 self.evaluation_id
381 }
382
383 pub async fn inspect(&self) -> Result<wire::EvaluationView, Error> {
385 nonzero(&self.evaluation_id)?;
386 let view = self
387 .client
388 .evaluations()
389 .inspect(self.client.request(wire::InspectEvaluationRequest {
390 evaluation_id: self.evaluation_id.to_vec(),
391 })?)
392 .await?
393 .into_inner();
394 validate_evaluation_view(&view, self.evaluation_id)?;
395 Ok(view)
396 }
397}
398
399#[derive(Clone)]
401pub struct CreateContext {
402 client: Inference,
403 request: wire::CreateContextRequest,
404}
405
406impl CreateContext {
407 #[must_use]
409 pub fn instructions(self, text: impl Into<String>) -> Self {
410 self.item(text_item(wire::ItemKind::Instruction, text.into()))
411 }
412
413 #[must_use]
415 pub fn item(mut self, item: wire::Item) -> Self {
416 self.request.items.push(item);
417 self
418 }
419
420 #[must_use]
422 pub fn identity(&self) -> Option<&wire::RequestIdentity> {
423 self.request.identity.as_ref()
424 }
425
426 pub async fn create(&self) -> Result<Context, Error> {
431 bounded(&self.request)?;
432 let receipt = self
433 .client
434 .rpc()
435 .create(self.client.request(self.request.clone())?)
436 .await?
437 .into_inner();
438 validate_receipt(&receipt)?;
439 if !receipt.retained {
440 return Err(Error::Invalid("creation did not retain a revision"));
441 }
442 Ok(Context {
443 client: self.client.clone(),
444 revision: fixed(&receipt.revision)?,
445 })
446 }
447}
448
449#[derive(Clone)]
451pub struct Context {
452 client: Inference,
453 revision: [u8; 32],
454}
455
456#[derive(Clone, Copy, Debug, Eq, PartialEq)]
459pub struct Retention {
460 latency_profile: [u8; 32],
461 expires_at_ms: u64,
462}
463
464impl Retention {
465 #[must_use]
467 pub const fn warm_until(latency_profile: [u8; 32], expires_at_ms: u64) -> Self {
468 Self {
469 latency_profile,
470 expires_at_ms,
471 }
472 }
473}
474
475impl Context {
476 #[must_use]
478 pub const fn id(&self) -> [u8; 32] {
479 self.revision
480 }
481
482 pub async fn inspect(&self) -> Result<wire::ContextView, Error> {
487 let view = self
488 .client
489 .rpc()
490 .inspect(self.client.request(wire::InspectContextRequest {
491 revision: self.revision.to_vec(),
492 })?)
493 .await?
494 .into_inner();
495 if fixed::<32>(&view.revision)? != self.revision {
496 return Err(Error::Invalid("revision differs"));
497 }
498 fixed::<32>(&view.lineage)?;
499 fixed::<32>(&view.execution_profile)?;
500 fixed::<32>(&view.content_digest)?;
501 if let Some(parent) = &view.parent {
502 fixed::<32>(parent)?;
503 }
504 if view.model.is_empty() || view.model.len() > 256 {
505 return Err(Error::Invalid("Context model is invalid"));
506 }
507 validate_provenance(
508 view.provenance
509 .as_ref()
510 .ok_or(Error::Invalid("Context provenance is absent"))?,
511 )?;
512 Ok(view)
513 }
514
515 #[must_use]
517 pub fn fork(&self) -> ContextMutation {
518 self.mutation(wire::mutate_context_request::Action::Fork(wire::Empty {}))
519 }
520
521 #[must_use]
523 pub fn edit(&self, edits: Vec<wire::Edit>) -> ContextMutation {
524 self.mutation(wire::mutate_context_request::Action::Edit(wire::Edits {
525 edits,
526 }))
527 }
528
529 #[must_use]
531 pub fn append(&self, text: impl Into<String>) -> ContextMutation {
532 self.edit(vec![wire::Edit {
533 action: Some(wire::edit::Action::Append(text_item(
534 wire::ItemKind::User,
535 text.into(),
536 ))),
537 }])
538 }
539
540 #[must_use]
542 pub fn truncate(&self, through: Option<[u8; 16]>) -> ContextMutation {
543 self.mutation(wire::mutate_context_request::Action::Truncate(
544 wire::Truncate {
545 through: through.map(|id| id.to_vec()),
546 },
547 ))
548 }
549
550 #[must_use]
552 pub fn compact(
553 &self,
554 selected: Vec<[u8; 16]>,
555 replacement: Vec<wire::Item>,
556 ) -> ContextMutation {
557 self.mutation(wire::mutate_context_request::Action::Compact(
558 wire::Compact {
559 selected: selected.into_iter().map(|id| id.to_vec()).collect(),
560 replacement,
561 },
562 ))
563 }
564
565 #[must_use]
567 pub fn release(&self) -> ContextMutation {
568 self.mutation(wire::mutate_context_request::Action::Release(
569 wire::Empty {},
570 ))
571 }
572
573 #[must_use]
575 pub fn transfer(&self, model: impl Into<String>) -> ContextMutation {
576 self.mutation(wire::mutate_context_request::Action::Transfer(
577 wire::Transfer {
578 model: model.into(),
579 },
580 ))
581 }
582
583 #[must_use]
585 pub fn generate(&self, input: impl Into<String>, maximum_output: u64) -> GenerateRun {
586 let identity = self.client.identity();
587 GenerateRun {
588 client: self.client.clone(),
589 request: wire::GenerateRunRequest {
590 identity: Some(identity),
591 context: self.revision.to_vec(),
592 input: Some(text_item(wire::ItemKind::User, input.into())),
593 maximum_output,
594 seed: None,
595 },
596 }
597 }
598
599 #[must_use]
601 pub fn retain(&self, policy: Retention) -> RetainWarm {
602 RetainWarm {
603 client: self.client.clone(),
604 request: wire::RetainWarmRequest {
605 identity: Some(self.client.identity()),
606 context: self.revision.to_vec(),
607 latency_profile: policy.latency_profile.to_vec(),
608 expires_at_ms: policy.expires_at_ms,
609 },
610 }
611 }
612
613 fn mutation(&self, action: wire::mutate_context_request::Action) -> ContextMutation {
614 ContextMutation {
615 client: self.client.clone(),
616 request: wire::MutateContextRequest {
617 identity: Some(self.client.identity()),
618 source: self.revision.to_vec(),
619 action: Some(action),
620 },
621 }
622 }
623}
624
625#[derive(Clone)]
628pub struct RetainWarm {
629 client: Inference,
630 request: wire::RetainWarmRequest,
631}
632
633impl RetainWarm {
634 #[must_use]
636 pub fn identity(&self) -> Option<&wire::RequestIdentity> {
637 self.request.identity.as_ref()
638 }
639
640 pub async fn send(&self) -> Result<WarmContext, Error> {
646 bounded(&self.request)?;
647 let expected_context = fixed::<32>(&self.request.context)?;
648 fixed::<32>(&self.request.latency_profile)?;
649 if self.request.expires_at_ms == 0 {
650 return Err(Error::Invalid("warm expiry is zero"));
651 }
652 let view = self
653 .client
654 .warm()
655 .retain(self.client.request(self.request.clone())?)
656 .await?
657 .into_inner();
658 validate_warm_view(&view, Some(expected_context), None)?;
659 Ok(WarmContext {
660 client: self.client.clone(),
661 commitment: fixed(&view.commitment)?,
662 })
663 }
664}
665
666#[derive(Clone)]
669pub struct WarmContext {
670 client: Inference,
671 commitment: [u8; 32],
672}
673
674impl WarmContext {
675 #[must_use]
677 pub const fn id(&self) -> [u8; 32] {
678 self.commitment
679 }
680
681 pub async fn inspect(&self) -> Result<wire::WarmView, Error> {
686 nonzero(&self.commitment)?;
687 let view = self
688 .client
689 .warm()
690 .inspect(self.client.request(wire::InspectWarmRequest {
691 commitment: self.commitment.to_vec(),
692 })?)
693 .await?
694 .into_inner();
695 validate_warm_view(&view, None, Some(self.commitment))?;
696 Ok(view)
697 }
698
699 #[must_use]
702 pub fn renew(&self, expires_at_ms: u64) -> RenewWarm {
703 RenewWarm {
704 client: self.client.clone(),
705 request: wire::RenewWarmRequest {
706 identity: Some(self.client.identity()),
707 commitment: self.commitment.to_vec(),
708 expires_at_ms,
709 },
710 }
711 }
712
713 #[must_use]
715 pub fn release(&self) -> ReleaseWarm {
716 ReleaseWarm {
717 client: self.client.clone(),
718 request: wire::ReleaseWarmRequest {
719 identity: Some(self.client.identity()),
720 commitment: self.commitment.to_vec(),
721 },
722 }
723 }
724}
725
726#[derive(Clone)]
728pub struct RenewWarm {
729 client: Inference,
730 request: wire::RenewWarmRequest,
731}
732
733impl RenewWarm {
734 pub async fn send(&self) -> Result<wire::WarmView, Error> {
739 bounded(&self.request)?;
740 let commitment = fixed::<32>(&self.request.commitment)?;
741 if self.request.expires_at_ms == 0 {
742 return Err(Error::Invalid("warm expiry is zero"));
743 }
744 let view = self
745 .client
746 .warm()
747 .renew(self.client.request(self.request.clone())?)
748 .await?
749 .into_inner();
750 validate_warm_view(&view, None, Some(commitment))?;
751 Ok(view)
752 }
753}
754
755#[derive(Clone)]
757pub struct ReleaseWarm {
758 client: Inference,
759 request: wire::ReleaseWarmRequest,
760}
761
762impl ReleaseWarm {
763 pub async fn send(&self) -> Result<wire::WarmView, Error> {
768 bounded(&self.request)?;
769 let commitment = fixed::<32>(&self.request.commitment)?;
770 let view = self
771 .client
772 .warm()
773 .release(self.client.request(self.request.clone())?)
774 .await?
775 .into_inner();
776 validate_warm_view(&view, None, Some(commitment))?;
777 if wire::WarmState::try_from(view.state).unwrap_or(wire::WarmState::Unspecified)
778 != wire::WarmState::Released
779 {
780 return Err(Error::Invalid("warm release is not terminal"));
781 }
782 Ok(view)
783 }
784}
785
786#[derive(Clone)]
788pub struct GenerateRun {
789 client: Inference,
790 request: wire::GenerateRunRequest,
791}
792
793impl GenerateRun {
794 #[must_use]
796 pub fn seed(mut self, seed: u64) -> Self {
797 self.request.seed = Some(seed);
798 self
799 }
800
801 pub fn id(&self) -> Result<[u8; 16], Error> {
806 fixed(
807 &self
808 .request
809 .identity
810 .as_ref()
811 .ok_or(Error::Invalid("missing Run identity"))?
812 .request_id,
813 )
814 }
815
816 pub async fn send(&self) -> Result<Run, Error> {
821 bounded(&self.request)?;
822 if self.request.maximum_output == 0 {
823 return Err(Error::Invalid("zero output bound"));
824 }
825 let run_id = self.id()?;
826 let response = self
827 .client
828 .runs()
829 .generate(self.client.request(self.request.clone())?)
830 .await?
831 .into_inner();
832 let view = response.run.ok_or(Error::Invalid("missing Run response"))?;
833 validate_run_view(&view, run_id)?;
834 Ok(Run {
835 client: self.client.clone(),
836 run_id,
837 })
838 }
839}
840
841#[derive(Clone)]
843pub struct Run {
844 client: Inference,
845 run_id: [u8; 16],
846}
847
848impl Run {
849 #[must_use]
851 pub const fn id(&self) -> [u8; 16] {
852 self.run_id
853 }
854
855 fn inspect_request(&self) -> wire::InspectRunRequest {
856 wire::InspectRunRequest {
857 run_id: self.run_id.to_vec(),
858 }
859 }
860
861 pub async fn inspect(&self) -> Result<wire::RunView, Error> {
866 let view = self
867 .client
868 .runs()
869 .inspect(self.client.request(self.inspect_request())?)
870 .await?
871 .into_inner();
872 validate_run_view(&view, self.run_id)?;
873 Ok(view)
874 }
875
876 pub async fn watch(&self, from_sequence: u64) -> Result<RunEvents, Error> {
881 let view = self.inspect().await?;
882 if view.result.is_some() {
883 let end = view
884 .last_sequence
885 .checked_add(1)
886 .ok_or(Error::Invalid("Run sequence exhausted"))?;
887 if from_sequence > end {
888 return Err(Error::Invalid("Run cursor exceeds retained events"));
889 }
890 if from_sequence == end {
891 return Ok(RunEvents {
892 stream: None,
893 expected: from_sequence,
894 terminal: true,
895 });
896 }
897 }
898 let stream = self
899 .client
900 .runs()
901 .watch(self.client.request(wire::WatchRunRequest {
902 run_id: self.run_id.to_vec(),
903 from_sequence,
904 })?)
905 .await?
906 .into_inner();
907 Ok(RunEvents {
908 stream: Some(stream),
909 expected: from_sequence,
910 terminal: false,
911 })
912 }
913
914 pub async fn cancel(&self) -> Result<wire::RunView, Error> {
919 let view = self
920 .client
921 .runs()
922 .cancel(self.client.request(self.inspect_request())?)
923 .await?
924 .into_inner();
925 validate_run_view(&view, self.run_id)?;
926 Ok(view)
927 }
928}
929
930pub struct RunEvents {
932 stream: Option<tonic::Streaming<wire::RunEvent>>,
933 expected: u64,
934 terminal: bool,
935}
936
937impl RunEvents {
938 pub async fn next(&mut self) -> Result<Option<wire::RunEvent>, Error> {
944 let Some(stream) = self.stream.as_mut() else {
945 return Ok(None);
946 };
947 let Some(event) = stream.message().await? else {
948 if self.terminal {
949 return Ok(None);
950 }
951 return Err(Error::Invalid("Run stream ended before terminal"));
952 };
953 if self.terminal || event.sequence != self.expected || event.event.is_none() {
954 return Err(Error::Invalid("Run event order or shape differs"));
955 }
956 self.expected = self
957 .expected
958 .checked_add(1)
959 .ok_or(Error::Invalid("Run sequence exhausted"))?;
960 if let Some(wire::run_event::Event::Terminal(value)) = event.event {
961 if wire::RunTerminal::try_from(value).unwrap_or(wire::RunTerminal::Unspecified)
962 == wire::RunTerminal::Unspecified
963 {
964 return Err(Error::Invalid("Run terminal is invalid"));
965 }
966 self.terminal = true;
967 }
968 Ok(Some(event))
969 }
970}
971
972fn validate_evaluation_spec(spec: &wire::EvaluationSpec) -> Result<(), Error> {
973 if spec.candidates.is_empty() || spec.candidates.len() > MAXIMUM_EVALUATION_CANDIDATES {
974 return Err(Error::Invalid("evaluation candidate count is invalid"));
975 }
976 let mut candidate_digests = std::collections::BTreeSet::new();
977 for candidate in &spec.candidates {
978 let digest = fixed::<32>(&candidate.digest)?;
979 if candidate.media_type.is_empty()
980 || candidate.media_type.len() > 256
981 || candidate.logical_size == 0
982 || !candidate_digests.insert(digest)
983 {
984 return Err(Error::Invalid("evaluation candidate is invalid"));
985 }
986 }
987
988 let suite = spec
989 .suite
990 .as_ref()
991 .ok_or(Error::Invalid("evaluation suite is absent"))?;
992 if suite.identity.is_empty()
993 || suite.identity.len() > 256
994 || suite.cases.is_empty()
995 || suite.cases.len() > MAXIMUM_EVALUATION_CASES
996 {
997 return Err(Error::Invalid("evaluation suite is invalid"));
998 }
999 fixed::<32>(&suite.digest)?;
1000 let mut case_ids = std::collections::BTreeSet::new();
1001 for case in &suite.cases {
1002 let case_id = fixed::<16>(&case.case_id)?;
1003 let inline = !case.input.is_empty();
1004 let artifact = case.input_artifact_digest.as_ref();
1005 if !case_ids.insert(case_id) || inline == artifact.is_some() {
1006 return Err(Error::Invalid("evaluation case is invalid"));
1007 }
1008 if let Some(digest) = artifact {
1009 fixed::<32>(digest)?;
1010 }
1011 }
1012
1013 let grader = spec
1014 .grader
1015 .as_ref()
1016 .ok_or(Error::Invalid("evaluation grader is absent"))?;
1017 if grader.handle.is_empty() || grader.handle.len() > 4_096 {
1018 return Err(Error::Invalid("evaluation grader is invalid"));
1019 }
1020 fixed::<32>(&grader.artifact_digest)?;
1021
1022 if spec.metrics.is_empty() || spec.metrics.len() > MAXIMUM_EVALUATION_METRICS {
1023 return Err(Error::Invalid("evaluation metric count is invalid"));
1024 }
1025 let mut metric_ids = std::collections::BTreeSet::new();
1026 for metric in &spec.metrics {
1027 if metric.identity.is_empty()
1028 || metric.identity.len() > 256
1029 || !metric_ids.insert(metric.identity.as_str())
1030 || wire::EvaluationAggregation::try_from(metric.aggregation)
1031 .unwrap_or(wire::EvaluationAggregation::Unspecified)
1032 == wire::EvaluationAggregation::Unspecified
1033 {
1034 return Err(Error::Invalid("evaluation metric is invalid"));
1035 }
1036 }
1037
1038 let possible_results = u64::try_from(spec.candidates.len())
1039 .ok()
1040 .and_then(|candidates| {
1041 u64::try_from(suite.cases.len())
1042 .ok()
1043 .and_then(|cases| candidates.checked_mul(cases))
1044 })
1045 .ok_or(Error::Invalid("evaluation result count overflow"))?;
1046 if spec.maximum_case_results == 0
1047 || spec.maximum_case_results > possible_results
1048 || spec.maximum_case_results > MAXIMUM_EVALUATION_RESULTS
1049 {
1050 return Err(Error::Invalid("evaluation result bound is invalid"));
1051 }
1052 fixed::<32>(&spec.spec_digest)?;
1053 Ok(())
1054}
1055
1056fn validate_exact_rational(value: Option<&wire::ExactRational>) -> Result<(), Error> {
1057 if value.is_none_or(|value| value.denominator == 0) {
1058 return Err(Error::Invalid("evaluation rational is invalid"));
1059 }
1060 Ok(())
1061}
1062
1063fn evaluation_observation_binding(native: &[u8; 32], observation: &[u8; 32]) -> [u8; 32] {
1064 let mut digest = Sha256::new();
1065 digest.update(b"acyclic.inference.grader-observation.v1\0");
1066 digest.update(native);
1067 digest.update(observation);
1068 digest.finalize().into()
1069}
1070
1071fn validate_evaluation_admission(
1072 view: &wire::EvaluationView,
1073 expected: [u8; 16],
1074 expected_spec: &wire::EvaluationSpec,
1075) -> Result<(), Error> {
1076 if view.spec.as_ref() != Some(expected_spec) {
1077 return Err(Error::Invalid("evaluation admission spec differs"));
1078 }
1079 validate_evaluation_view(view, expected)
1080}
1081
1082fn validate_evaluation_view(view: &wire::EvaluationView, expected: [u8; 16]) -> Result<(), Error> {
1083 if fixed::<16>(&view.evaluation_id)? != expected || view.sequence == 0 {
1084 return Err(Error::Invalid("evaluation identity differs"));
1085 }
1086 let spec = view
1087 .spec
1088 .as_ref()
1089 .ok_or(Error::Invalid("evaluation spec is absent"))?;
1090 validate_evaluation_spec(spec)?;
1091 let state =
1092 wire::EvaluationState::try_from(view.state).unwrap_or(wire::EvaluationState::Unspecified);
1093 if state == wire::EvaluationState::Unspecified
1094 || (state == wire::EvaluationState::Completed) != view.result.is_some()
1095 {
1096 return Err(Error::Invalid("evaluation state is invalid"));
1097 }
1098 let Some(result) = &view.result else {
1099 return Ok(());
1100 };
1101 validate_evaluation_result(spec, result)
1102}
1103
1104fn validate_evaluation_result(
1105 spec: &wire::EvaluationSpec,
1106 result: &wire::EvaluationResult,
1107) -> Result<(), Error> {
1108 if fixed::<32>(&result.spec_digest)? != fixed::<32>(&spec.spec_digest)?
1109 || result.case_results.is_empty()
1110 || u64::try_from(result.case_results.len()).unwrap_or(u64::MAX) != spec.maximum_case_results
1111 {
1112 return Err(Error::Invalid("evaluation result is invalid"));
1113 }
1114 fixed::<32>(&result.result_digest)?;
1115
1116 let suite = spec
1117 .suite
1118 .as_ref()
1119 .ok_or(Error::Invalid("evaluation suite is absent"))?;
1120 let candidates: std::collections::BTreeSet<_> = spec
1121 .candidates
1122 .iter()
1123 .map(|candidate| candidate.digest.as_slice())
1124 .collect();
1125 let cases: std::collections::BTreeSet<_> = suite
1126 .cases
1127 .iter()
1128 .map(|case| case.case_id.as_slice())
1129 .collect();
1130 let metrics: std::collections::BTreeSet<_> = spec
1131 .metrics
1132 .iter()
1133 .map(|metric| metric.identity.as_str())
1134 .collect();
1135 let mut observations = std::collections::BTreeSet::new();
1136 for case in &result.case_results {
1137 validate_evaluation_case(case, &candidates, &cases, &metrics, &mut observations)?;
1138 }
1139 validate_evaluation_aggregates(spec, result, &candidates, &metrics)
1140}
1141
1142fn validate_evaluation_case<'a>(
1143 case: &'a wire::EvaluationCaseResult,
1144 candidates: &std::collections::BTreeSet<&[u8]>,
1145 cases: &std::collections::BTreeSet<&[u8]>,
1146 metrics: &std::collections::BTreeSet<&str>,
1147 observations: &mut std::collections::BTreeSet<(&'a [u8], &'a [u8])>,
1148) -> Result<(), Error> {
1149 let observation = case
1150 .observation
1151 .as_ref()
1152 .ok_or(Error::Invalid("evaluation grader observation is absent"))?;
1153 let native_output_digest = fixed::<32>(&observation.native_output_digest)?;
1154 let grader_observation_digest = fixed::<32>(&observation.observation_digest)?;
1155 if fixed::<32>(&observation.binding_digest)?
1156 != evaluation_observation_binding(&native_output_digest, &grader_observation_digest)
1157 {
1158 return Err(Error::Invalid(
1159 "evaluation grader observation binding differs",
1160 ));
1161 }
1162 let outcome = wire::EvaluationCaseOutcome::try_from(case.outcome)
1163 .unwrap_or(wire::EvaluationCaseOutcome::Unspecified);
1164 if !candidates.contains(case.candidate_digest.as_slice())
1165 || !cases.contains(case.case_id.as_slice())
1166 || !observations.insert((case.candidate_digest.as_slice(), case.case_id.as_slice()))
1167 || outcome == wire::EvaluationCaseOutcome::Unspecified
1168 {
1169 return Err(Error::Invalid("evaluation case result is invalid"));
1170 }
1171 let mut observed_metrics = std::collections::BTreeSet::new();
1172 for metric in &case.metrics {
1173 if !metrics.contains(metric.metric_identity.as_str())
1174 || !observed_metrics.insert(metric.metric_identity.as_str())
1175 {
1176 return Err(Error::Invalid("evaluation case metric is invalid"));
1177 }
1178 validate_exact_rational(metric.value.as_ref())?;
1179 }
1180 if (outcome == wire::EvaluationCaseOutcome::Scored && observed_metrics.len() != metrics.len())
1181 || (outcome != wire::EvaluationCaseOutcome::Scored && !observed_metrics.is_empty())
1182 {
1183 return Err(Error::Invalid("evaluation case metric coverage is invalid"));
1184 }
1185 Ok(())
1186}
1187
1188fn validate_evaluation_aggregates(
1189 spec: &wire::EvaluationSpec,
1190 result: &wire::EvaluationResult,
1191 candidates: &std::collections::BTreeSet<&[u8]>,
1192 metrics: &std::collections::BTreeSet<&str>,
1193) -> Result<(), Error> {
1194 let mut aggregates = std::collections::BTreeSet::new();
1195 for aggregate in &result.aggregates {
1196 let Some(metric) = spec
1197 .metrics
1198 .iter()
1199 .find(|metric| metric.identity == aggregate.metric_identity)
1200 else {
1201 return Err(Error::Invalid("evaluation aggregate metric is invalid"));
1202 };
1203 if !candidates.contains(aggregate.candidate_digest.as_slice())
1204 || metric.aggregation != aggregate.aggregation
1205 || !aggregates.insert((
1206 aggregate.candidate_digest.as_slice(),
1207 aggregate.metric_identity.as_str(),
1208 ))
1209 {
1210 return Err(Error::Invalid("evaluation aggregate is invalid"));
1211 }
1212 validate_exact_rational(aggregate.value.as_ref())?;
1213 }
1214 if aggregates.len() != candidates.len().saturating_mul(metrics.len()) {
1215 return Err(Error::Invalid("evaluation aggregate coverage is invalid"));
1216 }
1217 Ok(())
1218}
1219
1220fn validate_run_view(view: &wire::RunView, expected: [u8; 16]) -> Result<(), Error> {
1221 if fixed::<16>(&view.run_id)? != expected || fixed::<32>(&view.input).is_err() {
1222 return Err(Error::Invalid("Run identity differs"));
1223 }
1224 if view.model.is_empty() || view.model.len() > 256 {
1225 return Err(Error::Invalid("Run model is invalid"));
1226 }
1227 if let Some(result) = &view.result {
1228 let terminal =
1229 wire::RunTerminal::try_from(result.terminal).unwrap_or(wire::RunTerminal::Unspecified);
1230 if terminal == wire::RunTerminal::Unspecified {
1231 return Err(Error::Invalid("Run result terminal is invalid"));
1232 }
1233 if let Some(receipt) = &result.receipt {
1234 fixed::<32>(&receipt.receipt_id)?;
1235 fixed::<32>(&receipt.model_profile)?;
1236 fixed::<32>(&receipt.meter_revision)?;
1237 fixed::<32>(&receipt.rate_card_revision)?;
1238 if receipt.usage.is_none() {
1239 return Err(Error::Invalid("Run usage is absent"));
1240 }
1241 }
1242 }
1243 Ok(())
1244}
1245
1246fn validate_warm_view(
1247 view: &wire::WarmView,
1248 expected_context: Option<[u8; 32]>,
1249 expected_commitment: Option<[u8; 32]>,
1250) -> Result<(), Error> {
1251 let commitment = fixed::<32>(&view.commitment)?;
1252 let context = fixed::<32>(&view.context)?;
1253 fixed::<32>(&view.model_profile)?;
1254 fixed::<32>(&view.latency_profile)?;
1255 fixed::<32>(&view.evidence_digest)?;
1256 fixed::<32>(&view.admission_receipt_id)?;
1257 let state = wire::WarmState::try_from(view.state).unwrap_or(wire::WarmState::Unspecified);
1258 if expected_context.is_some_and(|expected| expected != context)
1259 || expected_commitment.is_some_and(|expected| expected != commitment)
1260 || view.expires_at_ms == 0
1261 || view.sequence == 0
1262 || state == wire::WarmState::Unspecified
1263 {
1264 return Err(Error::Invalid("warm commitment shape differs"));
1265 }
1266 Ok(())
1267}
1268
1269fn validate_provenance(value: &wire::ContextProvenance) -> Result<(), Error> {
1270 use wire::context_provenance::Origin;
1271 match value
1272 .origin
1273 .as_ref()
1274 .ok_or(Error::Invalid("Context provenance is absent"))?
1275 {
1276 Origin::Created(_) => Ok(()),
1277 Origin::Derived(value) | Origin::Forked(value) => fixed::<32>(&value.source).map(|_| ()),
1278 Origin::Transferred(value) => fixed::<32>(&value.source).map(|_| ()),
1279 Origin::Generated(value) => {
1280 fixed::<16>(&value.run_id)?;
1281 fixed::<32>(&value.terminal_receipt_digest).map(|_| ())
1282 }
1283 Origin::RunInput(value) => {
1284 fixed::<32>(&value.source)?;
1285 fixed::<16>(&value.run_id)?;
1286 if value.maximum_output == 0 {
1287 return Err(Error::Invalid("Run input output bound is zero"));
1288 }
1289 Ok(())
1290 }
1291 }
1292}
1293
1294#[derive(Clone)]
1296pub struct ContextMutation {
1297 client: Inference,
1298 request: wire::MutateContextRequest,
1299}
1300
1301impl ContextMutation {
1302 #[must_use]
1304 pub fn identity(&self) -> Option<&wire::RequestIdentity> {
1305 self.request.identity.as_ref()
1306 }
1307
1308 pub async fn send(&self) -> Result<wire::MutationReceipt, Error> {
1313 bounded(&self.request)?;
1314 let receipt = self
1315 .client
1316 .rpc()
1317 .mutate(self.client.request(self.request.clone())?)
1318 .await?
1319 .into_inner();
1320 validate_receipt(&receipt)?;
1321 Ok(receipt)
1322 }
1323}
1324
1325fn validate_receipt(receipt: &wire::MutationReceipt) -> Result<(), Error> {
1326 fixed::<32>(&receipt.revision)?;
1327 fixed::<32>(&receipt.command_digest)?;
1328 if receipt.sequence == 0 {
1329 return Err(Error::Invalid("missing publication sequence"));
1330 }
1331 Ok(())
1332}
1333
1334fn text_item(kind: wire::ItemKind, text: String) -> wire::Item {
1335 wire::Item {
1336 id: uuid::Uuid::now_v7().as_bytes().to_vec(),
1337 kind: kind as i32,
1338 payload: text.into_bytes(),
1339 link: Vec::new(),
1340 continuation_profile: Vec::new(),
1341 }
1342}
1343
1344#[cfg(test)]
1345mod tests {
1346 use super::*;
1347 use prost::Message;
1348
1349 fn evaluation_spec() -> wire::EvaluationSpec {
1350 wire::EvaluationSpec {
1351 candidates: vec![wire::EvaluationArtifact {
1352 digest: vec![1; 32],
1353 media_type: "application/vnd.acyclic.model".to_owned(),
1354 logical_size: 1024,
1355 }],
1356 suite: Some(wire::EvaluationSuite {
1357 identity: "native-output-binding-v1".to_owned(),
1358 digest: vec![2; 32],
1359 cases: vec![wire::EvaluationCase {
1360 case_id: vec![3; 16],
1361 input: b"deterministic device input".to_vec(),
1362 input_artifact_digest: None,
1363 }],
1364 }),
1365 grader: Some(wire::EvaluationGrader {
1366 handle: b"grader://exact-v1".to_vec(),
1367 artifact_digest: vec![4; 32],
1368 }),
1369 metrics: vec![wire::EvaluationMetric {
1370 identity: "exact-match".to_owned(),
1371 aggregation: wire::EvaluationAggregation::Mean.into(),
1372 }],
1373 maximum_case_results: 1,
1374 spec_digest: vec![5; 32],
1375 }
1376 }
1377
1378 fn completed_evaluation() -> wire::EvaluationView {
1379 let native_output_digest = [7; 32];
1380 let grader_observation_digest = [8; 32];
1381 wire::EvaluationView {
1382 evaluation_id: vec![6; 16],
1383 spec: Some(evaluation_spec()),
1384 state: wire::EvaluationState::Completed.into(),
1385 result: Some(wire::EvaluationResult {
1386 spec_digest: vec![5; 32],
1387 case_results: vec![wire::EvaluationCaseResult {
1388 candidate_digest: vec![1; 32],
1389 case_id: vec![3; 16],
1390 observation: Some(wire::EvaluationGraderObservation {
1391 native_output_digest: native_output_digest.to_vec(),
1392 observation_digest: grader_observation_digest.to_vec(),
1393 binding_digest: evaluation_observation_binding(
1394 &native_output_digest,
1395 &grader_observation_digest,
1396 )
1397 .to_vec(),
1398 }),
1399 metrics: vec![wire::EvaluationMetricValue {
1400 metric_identity: "exact-match".to_owned(),
1401 value: Some(wire::ExactRational {
1402 numerator: 1,
1403 denominator: 1,
1404 }),
1405 }],
1406 outcome: wire::EvaluationCaseOutcome::Scored.into(),
1407 }],
1408 aggregates: vec![wire::EvaluationAggregate {
1409 candidate_digest: vec![1; 32],
1410 metric_identity: "exact-match".to_owned(),
1411 aggregation: wire::EvaluationAggregation::Mean.into(),
1412 value: Some(wire::ExactRational {
1413 numerator: 1,
1414 denominator: 1,
1415 }),
1416 }],
1417 result_digest: vec![9; 32],
1418 }),
1419 sequence: 2,
1420 }
1421 }
1422
1423 #[test]
1424 #[allow(
1425 clippy::indexing_slicing,
1426 reason = "each index is preceded by an assert_eq! on the corresponding Vec's len(), so the index is proven in-bounds"
1427 )]
1428 fn descriptor_contains_only_customer_contract() -> Result<(), Box<dyn std::error::Error>> {
1429 let descriptor = prost_types::FileDescriptorSet::decode(DESCRIPTOR)?;
1430 assert_eq!(descriptor.file.len(), 1);
1431 let file = &descriptor.file[0];
1432 assert_eq!(file.package.as_deref(), Some("inference.customer.v1"));
1433 assert!(file.dependency.is_empty());
1434 assert_eq!(file.service.len(), 5);
1435 assert_eq!(file.service[0].name.as_deref(), Some("ModelsService"));
1436 assert_eq!(file.service[0].method.len(), 1);
1437 assert_eq!(file.service[1].name.as_deref(), Some("ContextsService"));
1438 assert_eq!(file.service[1].method.len(), 3);
1439 assert_eq!(file.service[2].name.as_deref(), Some("WarmContextsService"));
1440 assert_eq!(file.service[2].method.len(), 4);
1441 assert_eq!(file.service[3].name.as_deref(), Some("RunsService"));
1442 assert_eq!(file.service[3].method.len(), 4);
1443 assert_eq!(file.service[4].name.as_deref(), Some("EvaluationsService"));
1444 assert_eq!(file.service[4].method.len(), 2);
1445 let names: Vec<_> = file
1446 .message_type
1447 .iter()
1448 .map(|message| message.name.as_deref())
1449 .collect();
1450 assert_eq!(
1451 names,
1452 [
1453 "ListModelsRequest",
1454 "ListModelsResponse",
1455 "ModelCapability",
1456 "RetentionProfile",
1457 "RetainWarmRequest",
1458 "InspectWarmRequest",
1459 "RenewWarmRequest",
1460 "ReleaseWarmRequest",
1461 "WarmView",
1462 "EvaluationArtifact",
1463 "EvaluationCase",
1464 "EvaluationSuite",
1465 "EvaluationGrader",
1466 "EvaluationMetric",
1467 "EvaluationSpec",
1468 "CreateEvaluationRequest",
1469 "InspectEvaluationRequest",
1470 "ExactRational",
1471 "EvaluationMetricValue",
1472 "EvaluationCaseResult",
1473 "EvaluationGraderObservation",
1474 "EvaluationAggregate",
1475 "EvaluationResult",
1476 "EvaluationView",
1477 "RequestIdentity",
1478 "Item",
1479 "CreateContextRequest",
1480 "InspectContextRequest",
1481 "Empty",
1482 "Insert",
1483 "Replace",
1484 "Edit",
1485 "Edits",
1486 "Truncate",
1487 "Compact",
1488 "Transfer",
1489 "MutateContextRequest",
1490 "MutationReceipt",
1491 "ContextView",
1492 "ContextProvenance",
1493 "ProvenanceSource",
1494 "TransferProvenance",
1495 "GenerationProvenance",
1496 "RunInputProvenance",
1497 "GenerateRunRequest",
1498 "GenerateRunResponse",
1499 "InspectRunRequest",
1500 "WatchRunRequest",
1501 "LogicalUsage",
1502 "UsageReceipt",
1503 "RunResult",
1504 "RunView",
1505 "RunEvent",
1506 "RunProgress"
1507 ]
1508 .map(Some)
1509 );
1510 let manifest = include_str!("../Cargo.toml");
1511 for dependency in ["inference-protocol", "inference-client", "git ="] {
1512 assert!(
1513 !manifest.contains(dependency),
1514 "private/source dependency in customer manifest"
1515 );
1516 }
1517 Ok(())
1518 }
1519
1520 #[test]
1521 fn rejects_incomplete_receipts_and_sensitive_credentials() -> Result<(), Error> {
1522 assert!(validate_receipt(&wire::MutationReceipt::default()).is_err());
1523 assert!(validate_warm_view(&wire::WarmView::default(), None, None).is_err());
1524 let valid = wire::WarmView {
1525 commitment: vec![1; 32],
1526 context: vec![2; 32],
1527 model_profile: vec![3; 32],
1528 latency_profile: vec![4; 32],
1529 expires_at_ms: 1,
1530 state: wire::WarmState::Active.into(),
1531 evidence_digest: vec![5; 32],
1532 admission_receipt_id: vec![6; 32],
1533 sequence: 1,
1534 };
1535 validate_warm_view(&valid, Some([2; 32]), Some([1; 32]))?;
1536 assert!(authorization("").is_err());
1537 assert!(authorization("line\nbreak").is_err());
1538 let secret = authorization("secret")?;
1539 assert_eq!(secret.as_str(), "Bearer secret");
1540 Ok(())
1541 }
1542
1543 #[test]
1544 fn evaluation_contract_binds_native_output_to_exact_grader_observation() -> Result<(), Error> {
1545 let view = completed_evaluation();
1546 validate_evaluation_view(&view, [6; 16])?;
1547
1548 let mut wrong_output = view.clone();
1549 wrong_output
1550 .result
1551 .as_mut()
1552 .and_then(|result| result.case_results.first_mut())
1553 .and_then(|result| result.observation.as_mut())
1554 .ok_or(Error::Invalid("missing case result"))?
1555 .native_output_digest = vec![0; 32];
1556 assert!(validate_evaluation_view(&wrong_output, [6; 16]).is_err());
1557
1558 let mut wrong_grader = view.clone();
1559 wrong_grader
1560 .result
1561 .as_mut()
1562 .and_then(|result| result.case_results.first_mut())
1563 .and_then(|result| result.observation.as_mut())
1564 .ok_or(Error::Invalid("missing case result"))?
1565 .observation_digest = vec![9; 32];
1566 assert!(validate_evaluation_view(&wrong_grader, [6; 16]).is_err());
1567
1568 let mut missing_score = view.clone();
1569 missing_score
1570 .result
1571 .as_mut()
1572 .and_then(|result| result.case_results.first_mut())
1573 .ok_or(Error::Invalid("missing case result"))?
1574 .metrics
1575 .clear();
1576 assert!(validate_evaluation_view(&missing_score, [6; 16]).is_err());
1577
1578 let mut missing_aggregate = view.clone();
1579 missing_aggregate
1580 .result
1581 .as_mut()
1582 .ok_or(Error::Invalid("missing evaluation result"))?
1583 .aggregates
1584 .clear();
1585 assert!(validate_evaluation_view(&missing_aggregate, [6; 16]).is_err());
1586
1587 let mut changed_spec = evaluation_spec();
1588 changed_spec.spec_digest = vec![10; 32];
1589 assert!(validate_evaluation_admission(&view, [6; 16], &changed_spec).is_err());
1590
1591 let mut unbounded = evaluation_spec();
1592 unbounded.maximum_case_results = 2;
1593 assert!(validate_evaluation_spec(&unbounded).is_err());
1594 Ok(())
1595 }
1596
1597 #[tokio::test]
1598 async fn request_metadata_marks_the_ephemeral_bearer_copy_sensitive() -> Result<(), Error> {
1599 let client = Inference(Arc::new(Connection {
1600 channel: Endpoint::from_static("https://localhost").connect_lazy(),
1601 authorization: authorization("secret")?,
1602 client_instance: [1; 16],
1603 }));
1604 let request = client.request(())?;
1605 let bearer = request
1606 .metadata()
1607 .get("authorization")
1608 .ok_or(Error::Invalid("missing authorization"))?;
1609 assert!(bearer.is_sensitive());
1610 assert_eq!(client.0.authorization.as_str(), "Bearer secret");
1611 Ok(())
1612 }
1613
1614 #[tokio::test]
1615 async fn connection_rejects_insecure_or_unbounded_trust_configuration() {
1616 assert!(matches!(
1617 Inference::connect("http://localhost", "secret", b"certificate").await,
1618 Err(Error::Invalid("HTTPS is required"))
1619 ));
1620 assert!(matches!(
1621 Inference::connect("https://localhost", "secret", b"").await,
1622 Err(Error::Invalid(INVALID_CA_CERTIFICATE_LENGTH))
1623 ));
1624 let oversized = vec![b'x'; MAXIMUM_CA_CERTIFICATE_BYTES + 1];
1625 assert!(matches!(
1626 Inference::connect("https://localhost", "secret", &oversized).await,
1627 Err(Error::Invalid(INVALID_CA_CERTIFICATE_LENGTH))
1628 ));
1629 }
1630
1631 #[test]
1632 fn customer_output_destructors_scrub_owned_buffers() -> Result<(), Error> {
1633 let mut event = wire::RunEvent {
1634 sequence: 0,
1635 event: Some(wire::run_event::Event::Output(vec![7; 32])),
1636 };
1637 scrub_run_event(&mut event);
1638 let Some(wire::run_event::Event::Output(bytes)) = event.event.as_ref() else {
1639 return Err(Error::Invalid("output event is absent"));
1640 };
1641 assert!(bytes.iter().all(|byte| *byte == 0));
1642
1643 let mut result = wire::RunResult {
1644 output: vec![9; 32],
1645 context: None,
1646 terminal: wire::RunTerminal::Completed.into(),
1647 receipt: None,
1648 };
1649 scrub_run_result(&mut result);
1650 assert!(result.output.iter().all(|byte| *byte == 0));
1651 Ok(())
1652 }
1653}