Skip to main content

vyre_runtime/
artifact_admission.rs

1use std::collections::{BTreeMap, BTreeSet};
2use std::sync::{Arc, Mutex, RwLock};
3
4use thiserror::Error;
5use vyre_megakernel::{
6    Artifact, ArtifactEnvelope, CompileError, Diagnostic, TargetCompileError, TargetPayload,
7    TargetPayloadFormat, ValidatedCompileRequest,
8};
9
10use crate::pipeline_cache::{PipelineCacheStore, PipelineFingerprint};
11use vyre_driver::{
12    ArtifactInstance, ArtifactMaterializer, BackendError, BackendRegistration, BindingSet,
13    BoundResource, Completion, DeviceIdentity, Resource, Submission,
14};
15use vyre_megakernel::{AbiAccess, ArtifactValueId, Digest, ResourceLifetime};
16
17/// Failure to authenticate an artifact envelope or select its exact required payload.
18#[derive(Clone, Debug, PartialEq, Eq, Error)]
19#[error("artifact admission rejected: {source}")]
20pub struct ArtifactAdmissionError {
21    #[source]
22    source: CompileError,
23}
24
25impl ArtifactAdmissionError {
26    /// Canonical structured diagnostic produced while decoding or selecting the payload.
27    #[must_use]
28    pub const fn diagnostic(&self) -> &Diagnostic {
29        &self.source.diagnostic
30    }
31
32    /// Recover the canonical error without flattening its diagnostic context.
33    #[must_use]
34    pub fn into_compile_error(self) -> CompileError {
35        self.source
36    }
37}
38
39impl From<CompileError> for ArtifactAdmissionError {
40    fn from(source: CompileError) -> Self {
41        Self { source }
42    }
43}
44
45/// Authenticated canonical envelope with one caller-selected exact payload.
46#[derive(Clone, Debug, PartialEq, Eq)]
47pub struct AdmittedArtifact {
48    envelope: ArtifactEnvelope,
49    target_payload_index: usize,
50}
51
52impl AdmittedArtifact {
53    /// Borrow the authenticated canonical envelope.
54    #[must_use]
55    pub const fn envelope(&self) -> &ArtifactEnvelope {
56        &self.envelope
57    }
58
59    /// Borrow the canonical backend-neutral artifact.
60    #[must_use]
61    pub const fn neutral(&self) -> &Artifact {
62        self.envelope.neutral()
63    }
64
65    /// Borrow the exact target payload selected during admission.
66    #[must_use]
67    pub fn target_payload(&self) -> &TargetPayload {
68        &self.envelope.target_payloads()[self.target_payload_index]
69    }
70
71    /// Consume the admission result and recover its owned canonical envelope.
72    #[must_use]
73    pub fn into_envelope(self) -> ArtifactEnvelope {
74        self.envelope
75    }
76}
77
78/// Decode and authenticate canonical envelope bytes, then require one exact payload format.
79pub fn admit_artifact(
80    envelope_bytes: &[u8],
81    required_format: &TargetPayloadFormat,
82) -> Result<AdmittedArtifact, ArtifactAdmissionError> {
83    let envelope = ArtifactEnvelope::from_bytes(envelope_bytes)?;
84    admit_envelope(envelope, required_format)
85}
86
87/// Authenticate an already-decoded canonical envelope and require one exact payload format.
88///
89/// Prefer this when a producer such as AOT packaging has already decoded the envelope
90/// and only the exact target-format selection remains.
91pub fn admit_envelope(
92    envelope: ArtifactEnvelope,
93    required_format: &TargetPayloadFormat,
94) -> Result<AdmittedArtifact, ArtifactAdmissionError> {
95    let target_payload_index = envelope.require_target_payload_index(required_format)?;
96    Ok(AdmittedArtifact {
97        envelope,
98        target_payload_index,
99    })
100}
101
102/// Load verified cache payload bytes and admit them as a canonical envelope.
103///
104/// `DiskCache` / `PipelineCacheStore` are format-agnostic blob stores. AOT
105/// writes `ArtifactEnvelope` bytes as the payload (plus the store's
106/// own BLAKE3 footer, stripped by [`PipelineCacheStore::get`]). Callers that
107/// treat a cache hit as executable MUST run this helper (or
108/// [`admit_artifact`] on the payload) before dispatch. A miss is `Ok(None)`.
109///
110/// # Errors
111///
112/// Returns [`ArtifactAdmissionError`] when payload bytes are present but are
113/// not an authentic envelope with the required target format.
114pub fn admit_cached_artifact(
115    store: &dyn PipelineCacheStore,
116    fingerprint: &PipelineFingerprint,
117    required_format: &TargetPayloadFormat,
118) -> Result<Option<AdmittedArtifact>, ArtifactAdmissionError> {
119    let Some(payload) = store.get(fingerprint) else {
120        return Ok(None);
121    };
122    admit_artifact(&payload, required_format).map(Some)
123}
124
125/// Runtime materialization or submission failure with structured admission preserved.
126#[derive(Debug, Error)]
127pub enum ArtifactSessionError {
128    /// Canonical envelope or target-format admission failed.
129    #[error(transparent)]
130    Admission(#[from] ArtifactAdmissionError),
131    /// Neutral compilation or target payload construction failed.
132    #[error(transparent)]
133    Compile(#[from] CompileError),
134    /// The registered target compiler rejected the selected artifact.
135    #[error(transparent)]
136    Target(#[from] TargetCompileError),
137    /// Registered device materialization or submission failed.
138    #[error(transparent)]
139    Backend(#[from] BackendError),
140    /// Runtime lifecycle state was poisoned by a panic while locked.
141    #[error("artifact session state is poisoned: {0}. Fix: discard and rebuild the session")]
142    State(String),
143}
144
145struct MaterializedArtifact {
146    admitted: AdmittedArtifact,
147    materializer: Arc<dyn ArtifactMaterializer>,
148    instance: Box<dyn ArtifactInstance>,
149}
150
151/// Authenticated immutable artifact materialized on one registered device generation.
152pub struct ArtifactSession {
153    registration: &'static BackendRegistration,
154    state: RwLock<MaterializedArtifact>,
155}
156
157impl ArtifactSession {
158    /// Compile one validated request, attach the registered target payload, and
159    /// materialize the authenticated artifact.
160    pub fn compile(
161        registration: &'static BackendRegistration,
162        request: &ValidatedCompileRequest,
163    ) -> Result<Self, ArtifactSessionError> {
164        let materializer = Arc::from(registration.materializer()?);
165        Self::compile_with_materializer(registration, request, materializer)
166    }
167    /// Compile and materialize through one caller-owned materializer generation.
168    pub fn compile_with_materializer(
169        registration: &'static BackendRegistration,
170        request: &ValidatedCompileRequest,
171        materializer: Arc<dyn ArtifactMaterializer>,
172    ) -> Result<Self, ArtifactSessionError> {
173        let artifact = vyre_megakernel::compile(request)?;
174        let compiler = registration.target_compiler()?;
175        let envelope = vyre_megakernel::attach_target(artifact, compiler.as_ref())?;
176        Self::from_envelope_with_materializer(registration, envelope, materializer)
177    }
178
179    /// Admit one already-decoded canonical envelope and materialize its exact target bytes.
180    pub fn from_envelope(
181        registration: &'static BackendRegistration,
182        envelope: ArtifactEnvelope,
183    ) -> Result<Self, ArtifactSessionError> {
184        let materializer = Arc::from(registration.materializer()?);
185        Self::from_envelope_with_materializer(registration, envelope, materializer)
186    }
187
188    /// Admit and materialize through one caller-owned materializer generation.
189    pub fn from_envelope_with_materializer(
190        registration: &'static BackendRegistration,
191        envelope: ArtifactEnvelope,
192        materializer: Arc<dyn ArtifactMaterializer>,
193    ) -> Result<Self, ArtifactSessionError> {
194        let admitted = admit_envelope(envelope, materializer.device().target_format())?;
195        let instance = materializer.materialize(admitted.neutral(), admitted.target_payload())?;
196        validate_instance(&admitted, materializer.as_ref(), instance.as_ref())?;
197        Ok(Self {
198            registration,
199            state: RwLock::new(MaterializedArtifact {
200                admitted,
201                materializer,
202                instance,
203            }),
204        })
205    }
206
207    /// Authenticate canonical envelope bytes and materialize the exact device format.
208    pub fn from_bytes(
209        registration: &'static BackendRegistration,
210        envelope_bytes: &[u8],
211    ) -> Result<Self, ArtifactSessionError> {
212        let envelope =
213            ArtifactEnvelope::from_bytes(envelope_bytes).map_err(ArtifactAdmissionError::from)?;
214        Self::from_envelope(registration, envelope)
215    }
216
217    /// Neutral artifact identity shared by every session and device generation.
218    pub fn artifact(&self) -> Result<Digest, ArtifactSessionError> {
219        let state = self
220            .state
221            .read()
222            .map_err(|error| ArtifactSessionError::State(error.to_string()))?;
223        Ok(state.admitted.neutral().digest())
224    }
225    /// Exact authenticated target payload identity materialized by this session.
226    pub fn payload(&self) -> Result<Digest, ArtifactSessionError> {
227        let state = self
228            .state
229            .read()
230            .map_err(|error| ArtifactSessionError::State(error.to_string()))?;
231        Ok(state.admitted.target_payload().digest())
232    }
233
234    /// Current immutable device generation identity.
235    pub fn device(&self) -> Result<DeviceIdentity, ArtifactSessionError> {
236        let state = self
237            .state
238            .read()
239            .map_err(|error| ArtifactSessionError::State(error.to_string()))?;
240        Ok(state.instance.device().clone())
241    }
242
243    /// Build an empty typed binding set for this exact artifact.
244    pub fn bindings(&self) -> Result<BindingSet, ArtifactSessionError> {
245        Ok(BindingSet::new(self.artifact()?))
246    }
247
248    /// Submit typed bindings without exposing the materialized native instance.
249    pub fn submit(
250        &self,
251        bindings: BindingSet,
252    ) -> Result<Box<dyn Submission>, ArtifactSessionError> {
253        let state = self
254            .state
255            .read()
256            .map_err(|error| ArtifactSessionError::State(error.to_string()))?;
257        Ok(state.instance.submit(bindings)?)
258    }
259
260    /// Submit and wait for typed completion/readback.
261    pub fn submit_and_wait(
262        &self,
263        bindings: BindingSet,
264    ) -> Result<Completion, ArtifactSessionError> {
265        Ok(self.submit(bindings)?.wait()?)
266    }
267
268    /// Reacquire the registered device and rematerialize authenticated target bytes.
269    ///
270    /// This path never invokes the target compiler, semantic optimizer, or lowering.
271    pub fn rematerialize(&self) -> Result<DeviceIdentity, ArtifactSessionError> {
272        let mut state = self
273            .state
274            .write()
275            .map_err(|error| ArtifactSessionError::State(error.to_string()))?;
276        let materializer: Arc<dyn ArtifactMaterializer> =
277            Arc::from(self.registration.materializer()?);
278        let admitted = admit_envelope(
279            state.admitted.envelope().clone(),
280            materializer.device().target_format(),
281        )?;
282        let instance = materializer.materialize(admitted.neutral(), admitted.target_payload())?;
283        validate_instance(&admitted, materializer.as_ref(), instance.as_ref())?;
284        let identity = instance.device().clone();
285        *state = MaterializedArtifact {
286            admitted,
287            materializer,
288            instance,
289        };
290        Ok(identity)
291    }
292
293    /// Resolve one canonical artifact ABI value by its stable resource name.
294    pub fn resource(&self, name: &str) -> Result<ArtifactValueId, ArtifactSessionError> {
295        let state = self
296            .state
297            .read()
298            .map_err(|error| ArtifactSessionError::State(error.to_string()))?;
299        state
300            .admitted
301            .neutral()
302            .resources()
303            .iter()
304            .find(|resource| resource.name == name)
305            .map(|resource| resource.value)
306            .ok_or_else(|| {
307                BackendError::InvalidProgram {
308                    fix: format!(
309                        "Fix: artifact ABI does not declare required runtime resource `{name}`."
310                    ),
311                }
312                .into()
313            })
314    }
315    /// Allocate one resident resource from this session's materializer generation.
316    pub fn allocate_resident(&self, byte_len: usize) -> Result<Resource, ArtifactSessionError> {
317        let state = self
318            .state
319            .read()
320            .map_err(|error| ArtifactSessionError::State(error.to_string()))?;
321        Ok(state.materializer.allocate_resident(byte_len)?)
322    }
323
324    /// Upload bytes into one resource owned by this session's materializer.
325    pub fn upload_resident(
326        &self,
327        resource: &Resource,
328        bytes: &[u8],
329    ) -> Result<(), ArtifactSessionError> {
330        let state = self
331            .state
332            .read()
333            .map_err(|error| ArtifactSessionError::State(error.to_string()))?;
334        Ok(state.materializer.upload_resident(resource, bytes)?)
335    }
336
337    /// Upload bytes at one offset into a resource owned by this session's materializer.
338    pub fn upload_resident_at(
339        &self,
340        resource: &Resource,
341        offset_bytes: usize,
342        bytes: &[u8],
343    ) -> Result<(), ArtifactSessionError> {
344        let state = self
345            .state
346            .read()
347            .map_err(|error| ArtifactSessionError::State(error.to_string()))?;
348        Ok(state
349            .materializer
350            .upload_resident_at(resource, offset_bytes, bytes)?)
351    }
352
353    /// Release one resource owned by this session's materializer.
354    pub fn free_resident(&self, resource: Resource) -> Result<(), ArtifactSessionError> {
355        let state = self
356            .state
357            .read()
358            .map_err(|error| ArtifactSessionError::State(error.to_string()))?;
359        Ok(state.materializer.free_resident(resource)?)
360    }
361
362    /// Bind one backend-resident resource per non-shared target slot.
363    pub fn resident_bindings(
364        &self,
365        resources: &[Resource],
366    ) -> Result<BindingSet, ArtifactSessionError> {
367        let state = self
368            .state
369            .read()
370            .map_err(|error| ArtifactSessionError::State(error.to_string()))?;
371        let entries = state.admitted.target_payload().entries();
372        if entries.len() != 1 {
373            return Err(BackendError::UnsupportedFeature {
374                name: "resident bindings for multi-entry artifacts".to_string(),
375                backend: state.instance.device().backend.to_string(),
376            }
377            .into());
378        }
379        let bindings = &entries[0].resource_bindings;
380        if bindings.len() != resources.len() {
381            return Err(BackendError::InvalidProgram {
382                fix: format!(
383                    "Fix: target entry requires {} resident resource(s), but the caller supplied {}.",
384                    bindings.len(),
385                    resources.len()
386                ),
387            }
388            .into());
389        }
390        let mut typed = BindingSet::new(state.admitted.neutral().digest());
391        for (binding, resource) in bindings.into_iter().zip(resources) {
392            typed.insert(binding.resource, BoundResource::Resident(resource.clone()));
393        }
394        Ok(typed)
395    }
396
397    /// Bind host inputs in canonical ABI slot order.
398    pub fn host_bindings(&self, inputs: &[&[u8]]) -> Result<BindingSet, ArtifactSessionError> {
399        let state = self
400            .state
401            .read()
402            .map_err(|error| ArtifactSessionError::State(error.to_string()))?;
403        let artifact = state.admitted.neutral();
404        let mut resources = artifact
405            .abi()
406            .resources
407            .iter()
408            .filter(|resource| match resource.access {
409                AbiAccess::ReadOnly | AbiAccess::Uniform => true,
410                AbiAccess::ReadWrite => artifact
411                    .resources()
412                    .iter()
413                    .find(|record| record.value == resource.value)
414                    .is_none_or(|record| record.lifetime != ResourceLifetime::Output),
415                AbiAccess::WriteOnly => false,
416            })
417            .collect::<Vec<_>>();
418        resources.sort_unstable_by_key(|resource| resource.slot);
419        if resources.len() != inputs.len() {
420            return Err(BackendError::InvalidProgram {
421                fix: format!(
422                    "Fix: artifact ABI requires {} host input buffer(s), but the caller supplied {}.",
423                    resources.len(),
424                    inputs.len()
425                ),
426            }
427            .into());
428        }
429        let mut bindings = BindingSet::new(state.admitted.neutral().digest());
430        for (resource, bytes) in resources.into_iter().zip(inputs) {
431            bindings.insert(resource.value, BoundResource::Host(bytes.to_vec()));
432        }
433        Ok(bindings)
434    }
435
436    /// Submit host inputs in canonical ABI order and wait for typed completion.
437    pub fn submit_host_inputs(&self, inputs: &[&[u8]]) -> Result<Completion, ArtifactSessionError> {
438        self.submit_and_wait(self.host_bindings(inputs)?)
439    }
440
441    /// Project writable completion values in canonical ABI slot order.
442    pub fn ordered_outputs(
443        &self,
444        completion: &Completion,
445    ) -> Result<Vec<Vec<u8>>, ArtifactSessionError> {
446        let state = self
447            .state
448            .read()
449            .map_err(|error| ArtifactSessionError::State(error.to_string()))?;
450        let mut resources = state
451            .admitted
452            .neutral()
453            .abi()
454            .resources
455            .iter()
456            .filter(|resource| {
457                matches!(resource.access, AbiAccess::ReadWrite | AbiAccess::WriteOnly)
458            })
459            .collect::<Vec<_>>();
460        resources.sort_unstable_by_key(|resource| resource.slot);
461        resources
462            .into_iter()
463            .map(|resource| {
464                completion
465                    .outputs
466                    .get(&resource.value)
467                    .or_else(|| completion.retained.get(&resource.value))
468                    .cloned()
469                    .ok_or_else(|| {
470                        BackendError::InvalidProgram {
471                            fix: format!(
472                                "Fix: materializer completion must project writable artifact value {}.",
473                                resource.value.0
474                            ),
475                        }
476                        .into()
477                    })
478            })
479            .collect()
480    }
481
482    fn retained_values(&self) -> Result<BTreeSet<ArtifactValueId>, ArtifactSessionError> {
483        let state = self
484            .state
485            .read()
486            .map_err(|error| ArtifactSessionError::State(error.to_string()))?;
487        Ok(state
488            .admitted
489            .neutral()
490            .resources()
491            .iter()
492            .filter(|resource| resource.lifetime == ResourceLifetime::Retained)
493            .map(|resource| resource.value)
494            .collect())
495    }
496}
497
498/// Runtime-owned retained binding policy over one immutable [`ArtifactSession`].
499pub struct RetainedArtifactSession {
500    session: ArtifactSession,
501    retained_values: BTreeSet<ArtifactValueId>,
502    retained: Mutex<BTreeMap<ArtifactValueId, Vec<u8>>>,
503}
504
505impl RetainedArtifactSession {
506    /// Create retained policy state and require every retained ABI value initially.
507    pub fn new(
508        session: ArtifactSession,
509        initial: BTreeMap<ArtifactValueId, Vec<u8>>,
510    ) -> Result<Self, ArtifactSessionError> {
511        let retained_values = session.retained_values()?;
512        if initial.keys().copied().collect::<BTreeSet<_>>() != retained_values {
513            return Err(BackendError::InvalidProgram {
514                fix: "Fix: initialize exactly every retained artifact value before creating a retained session.".to_string(),
515            }
516            .into());
517        }
518        Ok(Self {
519            session,
520            retained_values,
521            retained: Mutex::new(initial),
522        })
523    }
524
525    /// Neutral artifact identity shared with ephemeral sessions.
526    pub fn artifact(&self) -> Result<Digest, ArtifactSessionError> {
527        self.session.artifact()
528    }
529
530    /// Current immutable device generation identity.
531    pub fn device(&self) -> Result<DeviceIdentity, ArtifactSessionError> {
532        self.session.device()
533    }
534
535    /// Build empty transient bindings for the shared neutral artifact.
536    pub fn bindings(&self) -> Result<BindingSet, ArtifactSessionError> {
537        self.session.bindings()
538    }
539
540    /// Reacquire a device and rematerialize the authenticated artifact bytes.
541    pub fn rematerialize(&self) -> Result<DeviceIdentity, ArtifactSessionError> {
542        self.session.rematerialize()
543    }
544
545    /// Replace runtime-owned retained bytes before the next submission.
546    ///
547    /// # Errors
548    ///
549    /// Returns an error unless the update covers exactly every retained ABI value.
550    pub fn replace_retained(
551        &self,
552        values: BTreeMap<ArtifactValueId, Vec<u8>>,
553    ) -> Result<(), ArtifactSessionError> {
554        if values.keys().copied().collect::<BTreeSet<_>>() != self.retained_values {
555            return Err(BackendError::InvalidProgram {
556                fix: "Fix: replace exactly every retained artifact value.".to_string(),
557            }
558            .into());
559        }
560        *self
561            .retained
562            .lock()
563            .map_err(|error| ArtifactSessionError::State(error.to_string()))? = values;
564        Ok(())
565    }
566
567    /// Submit transient bindings, merge retained state, and atomically retain completion state.
568    pub fn submit_and_wait(
569        &self,
570        mut bindings: BindingSet,
571    ) -> Result<Completion, ArtifactSessionError> {
572        if bindings.artifact() != self.session.artifact()? {
573            return Err(BackendError::InvalidProgram {
574                fix: "Fix: retained session bindings must name the session artifact digest."
575                    .to_string(),
576            }
577            .into());
578        }
579        {
580            let retained = self
581                .retained
582                .lock()
583                .map_err(|error| ArtifactSessionError::State(error.to_string()))?;
584            for (value, bytes) in retained.iter() {
585                bindings.insert(*value, BoundResource::Host(bytes.clone()));
586            }
587        }
588        let completion = self.session.submit_and_wait(bindings)?;
589        if completion.retained.keys().copied().collect::<BTreeSet<_>>() != self.retained_values {
590            return Err(BackendError::InvalidProgram {
591                fix: "Fix: artifact completion must return exactly every retained ABI value."
592                    .to_string(),
593            }
594            .into());
595        }
596        *self
597            .retained
598            .lock()
599            .map_err(|error| ArtifactSessionError::State(error.to_string()))? =
600            completion.retained.clone();
601        Ok(completion)
602    }
603}
604
605fn validate_instance(
606    admitted: &AdmittedArtifact,
607    materializer: &dyn ArtifactMaterializer,
608    instance: &dyn ArtifactInstance,
609) -> Result<(), BackendError> {
610    if instance.artifact() != admitted.neutral().digest()
611        || instance.payload() != admitted.target_payload().digest()
612        || instance.device() != materializer.device().identity()
613    {
614        return Err(BackendError::InvalidProgram {
615            fix: "Fix: materialized instance identities must exactly match the admitted artifact, target payload, and acquired device generation.".to_string(),
616        });
617    }
618    Ok(())
619}