Skip to main content

acyclic_inference/
lib.rs

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/// Generated customer contract and service traits, also used by deterministic mocks.
13#[allow(missing_docs, unused_qualifications, clippy::all, clippy::pedantic)]
14pub mod wire {
15    tonic::include_proto!("inference.customer.v1");
16}
17
18/// Customer-only reflection; no backend descriptors or implementation are packaged.
19pub const DESCRIPTOR: &[u8] = include_bytes!("../inference_descriptor.bin");
20/// Bounded customer transport ceiling, not a published retention entitlement.
21pub const MAXIMUM_MESSAGE_BYTES: usize = 8 * 1024 * 1024;
22/// Largest caller-supplied PEM trust bundle accepted by [`Inference::connect`].
23pub const MAXIMUM_CA_CERTIFICATE_BYTES: usize = 64 * 1024;
24/// Maximum immutable candidates admitted by one evaluation.
25pub const MAXIMUM_EVALUATION_CANDIDATES: usize = 256;
26/// Maximum ordered cases admitted by one evaluation suite.
27pub const MAXIMUM_EVALUATION_CASES: usize = 4_096;
28/// Maximum metric identities admitted by one evaluation.
29pub const MAXIMUM_EVALUATION_METRICS: usize = 64;
30/// Maximum materialized candidate/case results in one evaluation.
31pub 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/// Transport or contract failure. Reuse the same builder to reconcile uncertainty.
69#[derive(Debug, thiserror::Error)]
70pub enum Error {
71    /// Invalid configuration, request identity or response shape.
72    #[error("invalid customer contract: {0}")]
73    Invalid(&'static str),
74    /// Channel setup failed before an RPC was sent.
75    #[error("customer transport setup failed: {0}")]
76    Transport(#[from] tonic::transport::Error),
77    /// A failed observation never proves an admitted mutation was absent.
78    #[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/// Authenticated customer connection; infrastructure remains entirely service-owned.
89#[derive(Clone)]
90pub struct Inference(Arc<Connection>);
91
92impl Inference {
93    /// Connect using an explicit trusted CA, ambient `WebPKI` roots, and bounded TLS/RPC deadlines.
94    ///
95    /// # Errors
96    /// Rejects non-HTTPS endpoints, invalid credentials and failed TLS setup.
97    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    /// Start an exact immutable Context creation with a stable pre-dispatch identity.
126    #[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    /// Authenticate and attach to an existing immutable retained revision.
139    ///
140    /// # Errors
141    /// Rejects unknown/unretained revisions or an invalid service response.
142    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    /// Prepare one immutable evaluation admission with a caller-known identity.
190    #[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    /// Recover one previously admitted evaluation without admitting new work.
202    #[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    /// Discover exact model revisions and the customer features currently admitted for them.
211    ///
212    /// # Errors
213    /// Rejects unauthenticated, malformed, duplicate, or unbounded capability responses.
214    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    /// Recover one previously admitted Run by its caller-known identity.
253    #[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    /// Recover a previously admitted warm commitment without creating another.
262    #[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        // Tonic owns unavoidable transport metadata copies. Retained credential
273        // storage is zeroizing and is never cloned into another long-lived field.
274        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/// Replayable immutable evaluation admission. Reusing this value reconciles the
318/// same candidate/suite/grader contract and never creates another evaluation.
319#[derive(Clone)]
320pub struct CreateEvaluation {
321    client: Inference,
322    request: wire::CreateEvaluationRequest,
323}
324
325impl CreateEvaluation {
326    /// Caller-known evaluation identity allocated before network effects.
327    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    /// Admit or reconcile this exact immutable evaluation.
339    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/// Recoverable immutable evaluation handle.
370#[derive(Clone)]
371pub struct Evaluation {
372    client: Inference,
373    evaluation_id: [u8; 16],
374}
375
376impl Evaluation {
377    /// Stable caller-known evaluation identity.
378    #[must_use]
379    pub const fn id(&self) -> [u8; 16] {
380        self.evaluation_id
381    }
382
383    /// Inspect durable evaluation state and exact result evidence.
384    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/// Replayable creation builder. Reusing it reconciles the same exact command.
400#[derive(Clone)]
401pub struct CreateContext {
402    client: Inference,
403    request: wire::CreateContextRequest,
404}
405
406impl CreateContext {
407    /// Append exact instruction bytes with a fresh item identity.
408    #[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    /// Add one typed item. Semantic validation belongs to the authenticated service.
414    #[must_use]
415    pub fn item(mut self, item: wire::Item) -> Self {
416        self.request.items.push(item);
417        self
418    }
419
420    /// Caller-known command identity, available before network effects.
421    #[must_use]
422    pub fn identity(&self) -> Option<&wire::RequestIdentity> {
423        self.request.identity.as_ref()
424    }
425
426    /// Create or reconcile this immutable Context; never repeat logical work.
427    ///
428    /// # Errors
429    /// Failed observations require the same builder, not a new creation command.
430    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/// Immutable revision handle; cloning a handle does not create a fork or retention edge.
450#[derive(Clone)]
451pub struct Context {
452    client: Inference,
453    revision: [u8; 32],
454}
455
456/// Explicit customer warm-retention policy. Distribution and capacity remain
457/// service-owned; the opaque profile must come from model discovery.
458#[derive(Clone, Copy, Debug, Eq, PartialEq)]
459pub struct Retention {
460    latency_profile: [u8; 32],
461    expires_at_ms: u64,
462}
463
464impl Retention {
465    /// Request an admitted warm promise through an exact published latency profile.
466    #[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    /// Portable revision identity for later authenticated attachment.
477    #[must_use]
478    pub const fn id(&self) -> [u8; 32] {
479        self.revision
480    }
481
482    /// Read exact retained content and its pinned logical execution identity.
483    ///
484    /// # Errors
485    /// Returns service rejection or malformed response without fabricating content.
486    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    /// Prepare an independently retained fork. Sending twice reconciles that fork.
516    #[must_use]
517    pub fn fork(&self) -> ContextMutation {
518        self.mutation(wire::mutate_context_request::Action::Fork(wire::Empty {}))
519    }
520
521    /// Prepare exact atomic item edits, leaving this revision unchanged.
522    #[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    /// Prepare one exact user-message append.
530    #[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    /// Retain exactly the selected prefix; None explicitly means an empty prefix.
541    #[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    /// Prepare explicit replacement of selected items; no summary is generated.
551    #[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    /// Release only this revision's own edge, never descendants or physical bytes.
566    #[must_use]
567    pub fn release(&self) -> ContextMutation {
568        self.mutation(wire::mutate_context_request::Action::Release(
569            wire::Empty {},
570        ))
571    }
572
573    /// Resolve a target model revision and replay this exact canonical content into a new Context.
574    #[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    /// Prepare one recoverable generation against this exact immutable revision.
584    #[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    /// Prepare one replayable warm-retention admission for this exact revision.
600    #[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/// Replayable warm admission. A failed observation must be reconciled by
626/// sending this same value, never by allocating another request identity.
627#[derive(Clone)]
628pub struct RetainWarm {
629    client: Inference,
630    request: wire::RetainWarmRequest,
631}
632
633impl RetainWarm {
634    /// Caller-known request identity allocated before effects.
635    #[must_use]
636    pub fn identity(&self) -> Option<&wire::RequestIdentity> {
637        self.request.identity.as_ref()
638    }
639
640    /// Admit or reconcile the exact warm promise.
641    ///
642    /// # Errors
643    /// Rejects malformed policy, transport failure, service rejection, or a
644    /// response not bound to the requested Context.
645    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/// Recoverable warm commitment. It never exposes physical placement or KV
667/// allocation identity and therefore remains valid through service rebalancing.
668#[derive(Clone)]
669pub struct WarmContext {
670    client: Inference,
671    commitment: [u8; 32],
672}
673
674impl WarmContext {
675    /// Stable commitment identity.
676    #[must_use]
677    pub const fn id(&self) -> [u8; 32] {
678        self.commitment
679    }
680
681    /// Inspect the latest durable logical warm fact.
682    ///
683    /// # Errors
684    /// Rejects transport/service failure or malformed commitment evidence.
685    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    /// Prepare an extension of the current promise. Reusing the returned builder
700    /// reconciles the same renewal; a failed renewal leaves the prior promise intact.
701    #[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    /// Prepare cleanup and release of only this warm promise.
714    #[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/// Replayable warm renewal.
727#[derive(Clone)]
728pub struct RenewWarm {
729    client: Inference,
730    request: wire::RenewWarmRequest,
731}
732
733impl RenewWarm {
734    /// Extend and reconcile this exact commitment.
735    ///
736    /// # Errors
737    /// Rejects malformed expiry, transport/service failure, or a foreign response.
738    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/// Replayable cleanup-complete release.
756#[derive(Clone)]
757pub struct ReleaseWarm {
758    client: Inference,
759    request: wire::ReleaseWarmRequest,
760}
761
762impl ReleaseWarm {
763    /// Release or reconcile this exact warm promise.
764    ///
765    /// # Errors
766    /// Rejects transport/service failure or a response without factual release.
767    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/// Replayable Run admission builder. Reusing it reconciles one logical Run.
787#[derive(Clone)]
788pub struct GenerateRun {
789    client: Inference,
790    request: wire::GenerateRunRequest,
791}
792
793impl GenerateRun {
794    /// Pin a deterministic sampling seed.
795    #[must_use]
796    pub fn seed(mut self, seed: u64) -> Self {
797        self.request.seed = Some(seed);
798        self
799    }
800
801    /// Caller-known Run identity allocated before network effects.
802    ///
803    /// # Errors
804    /// Rejects a missing, zero, or incorrectly sized identity.
805    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    /// Admit or reconcile this exact Run without creating a successor.
817    ///
818    /// # Errors
819    /// An unavailable response requires replaying this builder, never allocating another Run.
820    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/// Recoverable logical Run. Cloning this handle never repeats execution.
842#[derive(Clone)]
843pub struct Run {
844    client: Inference,
845    run_id: [u8; 16],
846}
847
848impl Run {
849    /// Caller-known stable Run identity.
850    #[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    /// Inspect durable Run state without opening a watch or admitting work.
862    ///
863    /// # Errors
864    /// Returns authenticated service rejection or malformed response evidence.
865    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    /// Resume the ordered event stream at an inclusive zero-based cursor.
877    ///
878    /// # Errors
879    /// Returns transport or authenticated service rejection before the stream is established.
880    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    /// Request durable cancellation of this Run only.
915    ///
916    /// # Errors
917    /// Returns authenticated service rejection or malformed post-cancellation state.
918    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
930/// Validating event-stream observation. Dropping it never cancels the Run.
931pub struct RunEvents {
932    stream: Option<tonic::Streaming<wire::RunEvent>>,
933    expected: u64,
934    terminal: bool,
935}
936
937impl RunEvents {
938    /// Read and validate the next ordered event.
939    ///
940    /// # Errors
941    /// Rejects transport failure, a gap/reorder, malformed event, invalid terminal, or a stream
942    /// that closes without terminal evidence.
943    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/// Replayable mutation with a pre-dispatch command identity, not an execution retry.
1295#[derive(Clone)]
1296pub struct ContextMutation {
1297    client: Inference,
1298    request: wire::MutateContextRequest,
1299}
1300
1301impl ContextMutation {
1302    /// Caller-known command identity before dispatch.
1303    #[must_use]
1304    pub fn identity(&self) -> Option<&wire::RequestIdentity> {
1305        self.request.identity.as_ref()
1306    }
1307
1308    /// Submit or reconcile the exact command. The receipt is not a billing/deletion proof.
1309    ///
1310    /// # Errors
1311    /// An unavailable observation does not imply the mutation failed to commit.
1312    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}