Skip to main content

runmat_execution_artifact/program/
request.rs

1use std::collections::BTreeSet;
2
3use runmat_execution::value::{ValueLimits, ValuePayload, ValueRef, ValueRefKind};
4use serde::{Deserialize, Serialize};
5
6use super::{ExecutableForm, ProgramArtifact, ProgramBuildRecipe};
7use crate::{ArtifactError, ArtifactResult};
8
9pub const PROGRAM_EXECUTION_REQUEST_SCHEMA_V1: u16 = 1;
10pub const MAX_PROGRAM_EXECUTION_ARGUMENTS: usize = 4096;
11pub const MAX_PROGRAM_EXECUTION_RESULT_OBJECTS: usize = 65_538;
12
13#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
14#[serde(deny_unknown_fields)]
15pub struct ProgramExecutionDescriptor {
16    pub schema_version: u16,
17    pub recipe: ProgramBuildRecipe,
18    pub artifact: ProgramArtifact,
19    pub function: usize,
20    pub requested_outputs: u16,
21}
22
23impl ProgramExecutionDescriptor {
24    pub fn validate(&self) -> ArtifactResult<()> {
25        self.artifact.validate_against(&self.recipe)?;
26        if self.schema_version != PROGRAM_EXECUTION_REQUEST_SCHEMA_V1
27            || !entrypoint_matches(self.artifact.form, self.function, &self.recipe.entrypoint)
28            || self.requested_outputs != self.recipe.outputs.requested_outputs
29        {
30            return Err(ArtifactError::Invalid(
31                "program descriptor has an inconsistent callable or output contract".into(),
32            ));
33        }
34        Ok(())
35    }
36
37    pub fn validate_for_portable_host(&self) -> ArtifactResult<()> {
38        self.validate()?;
39        self.artifact.target.validate_for_portable_host()
40    }
41}
42
43#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
44#[serde(deny_unknown_fields)]
45pub struct ProgramExecutionInputs {
46    pub schema_version: u16,
47    pub arguments: Vec<ValuePayload>,
48}
49
50impl ProgramExecutionInputs {
51    pub fn validate(&self) -> ArtifactResult<()> {
52        if self.schema_version != PROGRAM_EXECUTION_REQUEST_SCHEMA_V1
53            || self.arguments.len() > MAX_PROGRAM_EXECUTION_ARGUMENTS
54        {
55            return Err(ArtifactError::Invalid(
56                "program inputs use an unsupported schema or exceed their bound".into(),
57            ));
58        }
59        for argument in &self.arguments {
60            argument
61                .validate(ValueLimits::default())
62                .map_err(|error| ArtifactError::Invalid(error.to_string()))?;
63        }
64        Ok(())
65    }
66}
67
68#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
69#[serde(deny_unknown_fields)]
70pub struct ProgramExecutionRequest {
71    pub schema_version: u16,
72    pub recipe: ProgramBuildRecipe,
73    pub artifact: ProgramArtifact,
74    pub function: usize,
75    pub arguments: Vec<ValuePayload>,
76    pub requested_outputs: u16,
77}
78
79impl ProgramExecutionRequest {
80    pub fn from_parts(
81        descriptor: ProgramExecutionDescriptor,
82        inputs: ProgramExecutionInputs,
83    ) -> ArtifactResult<Self> {
84        descriptor.validate()?;
85        inputs.validate()?;
86        let request = Self {
87            schema_version: PROGRAM_EXECUTION_REQUEST_SCHEMA_V1,
88            recipe: descriptor.recipe,
89            artifact: descriptor.artifact,
90            function: descriptor.function,
91            arguments: inputs.arguments,
92            requested_outputs: descriptor.requested_outputs,
93        };
94        request.validate()?;
95        Ok(request)
96    }
97
98    pub fn validate(&self) -> ArtifactResult<()> {
99        if self.schema_version != PROGRAM_EXECUTION_REQUEST_SCHEMA_V1 {
100            return Err(ArtifactError::Invalid(
101                "unsupported program execution request schema".into(),
102            ));
103        }
104        self.artifact.validate_against(&self.recipe)?;
105        if !entrypoint_matches(self.artifact.form, self.function, &self.recipe.entrypoint)
106            || self.requested_outputs != self.recipe.outputs.requested_outputs
107            || self.arguments.len() > MAX_PROGRAM_EXECUTION_ARGUMENTS
108            || (matches!(
109                self.artifact.form,
110                ExecutableForm::InterpreterScriptV1 | ExecutableForm::TestAttemptV1
111            ) && !self.arguments.is_empty())
112        {
113            return Err(ArtifactError::Invalid(
114                "program execution request has an inconsistent callable, output contract, or argument count".into(),
115            ));
116        }
117        if self.artifact.form == ExecutableForm::ExecutableUnitV3 {
118            let envelope = self
119                .artifact
120                .executable_unit()?
121                .expect("executable-unit form returns its validated envelope");
122            if usize::try_from(envelope.manifest.identity.entrypoint_function.0).ok()
123                != Some(self.function)
124                || (envelope.manifest.identity.entrypoint_kind
125                    == runmat_execution::ExecutableEntrypointKind::Script
126                    && !self.arguments.is_empty())
127            {
128                return Err(ArtifactError::Invalid(
129                    "executable unit request does not match its declared entrypoint".into(),
130                ));
131            }
132        }
133        for argument in &self.arguments {
134            argument
135                .validate(ValueLimits::default())
136                .map_err(|error| ArtifactError::Invalid(error.to_string()))?;
137        }
138        Ok(())
139    }
140
141    pub fn validate_for_portable_host(&self) -> ArtifactResult<()> {
142        self.validate()?;
143        self.artifact.target.validate_for_portable_host()
144    }
145}
146
147fn entrypoint_matches(form: ExecutableForm, function: usize, entrypoint: &str) -> bool {
148    match form {
149        ExecutableForm::InterpreterBytecodeV1 => function.to_string() == entrypoint,
150        ExecutableForm::InterpreterScriptV1 => function == 0 && entrypoint == "script",
151        ExecutableForm::TestAttemptV1 => function == 0 && entrypoint == "test_attempt",
152        ExecutableForm::MeshingWorkload => function == 0 && entrypoint == "meshing_workload",
153        ExecutableForm::ExecutableUnitV3 => function.to_string() == entrypoint,
154        ExecutableForm::NativeObjectV1 => function.to_string() == entrypoint,
155    }
156}
157
158#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
159#[serde(tag = "outcome", rename_all = "snake_case", deny_unknown_fields)]
160pub enum ProgramExecutionResponse {
161    Success {
162        value: ValuePayload,
163    },
164    ExternalizedSuccess {
165        outputs: Vec<ValuePayload>,
166        result_objects: Vec<ValueRef>,
167    },
168    Failure {
169        message: String,
170    },
171}
172
173impl ProgramExecutionResponse {
174    pub fn validate_against(&self, request: &ProgramExecutionRequest) -> ArtifactResult<()> {
175        request.validate()?;
176        match self {
177            Self::Success { value } => {
178                if request.requested_outputs != 1 {
179                    return Err(ArtifactError::Invalid(
180                        "single-value response differs from its output contract".into(),
181                    ));
182                }
183                value
184                    .validate(ValueLimits::default())
185                    .map_err(|error| ArtifactError::Invalid(error.to_string()))
186            }
187            Self::ExternalizedSuccess {
188                outputs,
189                result_objects,
190            } => validate_externalized_success(request, outputs, result_objects),
191            Self::Failure { message } => {
192                if message.is_empty() || message.len() > 1024 * 1024 {
193                    return Err(ArtifactError::Limit(
194                        "program failure message is empty or exceeds its byte bound".into(),
195                    ));
196                }
197                Ok(())
198            }
199        }
200    }
201}
202
203fn validate_externalized_success(
204    request: &ProgramExecutionRequest,
205    outputs: &[ValuePayload],
206    result_objects: &[ValueRef],
207) -> ArtifactResult<()> {
208    if outputs.len() != usize::from(request.requested_outputs)
209        || result_objects.is_empty()
210        || result_objects.len() > MAX_PROGRAM_EXECUTION_RESULT_OBJECTS
211    {
212        return Err(ArtifactError::Limit(
213            "externalized program response exceeds its output or object inventory contract".into(),
214        ));
215    }
216    for output in outputs {
217        output
218            .validate(ValueLimits::default())
219            .map_err(|error| ArtifactError::Invalid(error.to_string()))?;
220    }
221    let mut value_ids = BTreeSet::new();
222    let mut logical_digests = BTreeSet::new();
223    for object in result_objects {
224        ValuePayload::Object(Box::new(object.clone()))
225            .validate(ValueLimits::default())
226            .map_err(|error| ArtifactError::Invalid(error.to_string()))?;
227        if object.kind != ValueRefKind::ResultObject
228            || object.resident_fence.is_some()
229            || !value_ids.insert(object.id)
230            || !logical_digests.insert(object.logical_digest)
231        {
232            return Err(ArtifactError::Invalid(
233                "externalized program response inventory is invalid or duplicated".into(),
234            ));
235        }
236    }
237    if outputs.iter().any(|output| match output {
238        ValuePayload::Object(reference) if reference.kind == ValueRefKind::ResultObject => {
239            !result_objects.contains(reference.as_ref())
240        }
241        _ => false,
242    }) {
243        return Err(ArtifactError::Invalid(
244            "externalized program response inventory omits a result root".into(),
245        ));
246    }
247    Ok(())
248}
249
250#[cfg(test)]
251mod tests {
252    use runmat_execution::identity::ValueId;
253    use runmat_execution::schema::VALUE_PAYLOAD_SCHEMA_V1;
254    use runmat_execution::{Digest, OutputContract, ProgramEnvironment, ProgramRevision};
255
256    use super::*;
257    use crate::ExecutableForm;
258
259    fn request() -> ProgramExecutionRequest {
260        let revision = ProgramRevision::new(
261            Digest::sha256(b"graph"),
262            Digest::sha256(b"source"),
263            ProgramEnvironment::new(
264                1,
265                1,
266                Digest::sha256(b"runtime"),
267                Digest::sha256(b"catalog"),
268                "matlab",
269            )
270            .unwrap(),
271        )
272        .unwrap();
273        let recipe = ProgramBuildRecipe {
274            schema_version: crate::PROGRAM_BUILD_RECIPE_SCHEMA_VERSION,
275            program_revision: revision,
276            entrypoint: "7".into(),
277            outputs: OutputContract {
278                requested_outputs: 1,
279            },
280            execution_mode: "interpreter".into(),
281            target: crate::ProgramTarget::portable("portable-test"),
282            features: Default::default(),
283            compile_options: Default::default(),
284            source_objects: Vec::new(),
285            expected_artifact_id: None,
286        };
287        let artifact = ProgramArtifact::materialize(
288            &recipe,
289            ExecutableForm::InterpreterBytecodeV1,
290            b"program".to_vec(),
291        )
292        .unwrap();
293        ProgramExecutionRequest {
294            schema_version: PROGRAM_EXECUTION_REQUEST_SCHEMA_V1,
295            recipe,
296            artifact,
297            function: 7,
298            arguments: Vec::new(),
299            requested_outputs: 1,
300        }
301    }
302
303    fn result_reference(bytes: &[u8]) -> ValueRef {
304        let logical_digest = Digest::sha256(bytes);
305        ValueRef {
306            schema_version: VALUE_PAYLOAD_SCHEMA_V1,
307            id: ValueId::derive(&[b"program-response-test", logical_digest.bytes()]),
308            logical_digest,
309            encoded_length: bytes.len() as u64,
310            media_type: "application/vnd.runmat.test-object".into(),
311            value_schema: "runmat.test-object.v1".into(),
312            encryption_context: Digest::sha256(b"test-encryption-context"),
313            kind: ValueRefKind::ResultObject,
314            authorization_scope: "test-scope".into(),
315            resident_fence: None,
316        }
317    }
318
319    #[test]
320    fn exact_program_request_validates_every_identity_boundary() {
321        request().validate().unwrap();
322        let mut mismatched = request();
323        mismatched.function = 8;
324        assert!(mismatched.validate().is_err());
325        let mut tampered = request();
326        tampered.artifact.executable_bytes.push(0);
327        assert!(tampered.validate().is_err());
328    }
329
330    #[test]
331    fn exact_program_request_rejects_unknown_schemas_and_output_drift() {
332        let mut unknown = request();
333        unknown.schema_version += 1;
334        assert!(unknown.validate().is_err());
335        let mut outputs = request();
336        outputs.requested_outputs = 2;
337        assert!(outputs.validate().is_err());
338    }
339
340    #[test]
341    fn script_form_has_an_explicit_argument_free_entrypoint() {
342        let mut script = request();
343        script.recipe.entrypoint = "script".into();
344        script.function = 0;
345        script.artifact = ProgramArtifact::materialize(
346            &script.recipe,
347            ExecutableForm::InterpreterScriptV1,
348            b"script-bytecode".to_vec(),
349        )
350        .unwrap();
351        script.validate().unwrap();
352        script
353            .arguments
354            .push(runmat_execution::value::ValuePayload::Inline(Box::new(
355                runmat_execution::value::InlineValue::String("unexpected".into()),
356            )));
357        assert!(script.validate().is_err());
358    }
359
360    #[test]
361    fn externalized_response_requires_a_complete_unique_result_inventory() {
362        let request = request();
363        let root = result_reference(b"root");
364        let response = ProgramExecutionResponse::ExternalizedSuccess {
365            outputs: vec![ValuePayload::Object(Box::new(root.clone()))],
366            result_objects: vec![root.clone(), result_reference(b"chunk")],
367        };
368        response.validate_against(&request).unwrap();
369
370        let missing_root = ProgramExecutionResponse::ExternalizedSuccess {
371            outputs: vec![ValuePayload::Object(Box::new(root.clone()))],
372            result_objects: vec![result_reference(b"chunk")],
373        };
374        assert!(missing_root.validate_against(&request).is_err());
375
376        let duplicate = ProgramExecutionResponse::ExternalizedSuccess {
377            outputs: vec![ValuePayload::Object(Box::new(root.clone()))],
378            result_objects: vec![root.clone(), root],
379        };
380        assert!(duplicate.validate_against(&request).is_err());
381    }
382}