Skip to main content

a3s_code_core/evaluation/
protocol.rs

1//! Versioned, language-neutral transport for the evaluation substrate.
2//!
3//! The runtime contracts in the sibling modules are Rust-first traits and
4//! stores.  This module is the deliberately small wire boundary used by a
5//! host when it projects evidence, auxiliary-run lifecycle, and immutable
6//! result records to another process or SDK.  It contains no reviewer rubric,
7//! finding vocabulary, authorization, or Cloud business state.
8
9use super::auxiliary_run::{
10    AuxiliaryRunOutputV1, AuxiliaryRunSnapshotV1, AuxiliaryRunSpecV1, AUXILIARY_MAX_OUTPUT_BYTES,
11};
12use super::evidence::{EvidenceReadRequestV1, EvidenceSnapshotV1};
13use super::result::{EvaluationRecordV1, EvaluationResultV1};
14use serde::{de::DeserializeOwned, Deserialize, Serialize};
15use serde_json::Value;
16use thiserror::Error;
17
18/// Version carried by the evaluation wire envelope.
19pub const EVALUATION_PROTOCOL_VERSION_V1: u16 = 1;
20
21/// Stable schema identifier for the cross-process evaluation envelope.
22pub const EVALUATION_PROTOCOL_SCHEMA_V1: &str = "a3s.code.evaluation-wire.v1";
23
24/// Maximum encoded envelope size accepted at a process boundary.
25pub const EVALUATION_PROTOCOL_MAX_MESSAGE_BYTES: usize = 32 * 1024 * 1024;
26
27macro_rules! define_evaluation_wire_kinds_v1 {
28    ($( $variant:ident => $constant:ident = $wire_name:literal => $payload:ident ),+ $(,)?) => {
29        /// Closed top-level payload kinds in evaluation wire version 1.
30        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
31        #[serde(rename_all = "snake_case")]
32        pub enum EvaluationWireKindV1 {
33            $( $variant, )+
34        }
35
36        /// Canonical string constants for the version-one wire catalog.
37        #[derive(Debug, Clone, Copy)]
38        pub struct EvaluationWireTypeV1;
39
40        impl EvaluationWireTypeV1 {
41            $( pub const $constant: &'static str = $wire_name; )+
42        }
43
44        /// One source-of-truth descriptor used by SDK artifact generation and
45        /// parity tests.  The payload type is documentation metadata; Rust
46        /// validation still uses the concrete type in the match below.
47        #[derive(Debug, Clone, Copy, PartialEq, Eq)]
48        pub struct EvaluationWireKindDescriptorV1 {
49            pub kind: EvaluationWireKindV1,
50            pub wire_name: &'static str,
51            pub constant_name: &'static str,
52            pub payload_type: &'static str,
53        }
54
55        /// Ordered catalog known by evaluation wire version 1.
56        pub const EVALUATION_WIRE_KIND_DESCRIPTORS_V1: &[EvaluationWireKindDescriptorV1] = &[
57            $( EvaluationWireKindDescriptorV1 {
58                kind: EvaluationWireKindV1::$variant,
59                wire_name: $wire_name,
60                constant_name: stringify!($constant),
61                payload_type: stringify!($payload),
62            }, )+
63        ];
64
65        impl EvaluationWireKindV1 {
66            /// Return the canonical wire spelling for this kind.
67            pub const fn wire_name(self) -> &'static str {
68                match self {
69                    $( Self::$variant => $wire_name, )+
70                }
71            }
72
73            /// Return the Rust payload type projected by this kind.
74            pub const fn payload_type(self) -> &'static str {
75                match self {
76                    $( Self::$variant => stringify!($payload), )+
77                }
78            }
79
80            /// Parse one canonical wire spelling without accepting aliases.
81            pub fn from_wire_name(value: &str) -> Option<Self> {
82                match value {
83                    $( $wire_name => Some(Self::$variant), )+
84                    _ => None,
85                }
86            }
87        }
88    };
89}
90
91// Keep this catalog intentionally boring and one-entry-per-line.  The SDK
92// generator parses these lines, while Rust compiles the same list into the
93// enum, constants, and descriptors above.
94define_evaluation_wire_kinds_v1! {
95    EvidenceReadRequest => EVIDENCE_READ_REQUEST = "evidence_read_request" => EvidenceReadRequestV1,
96    EvidenceSnapshot => EVIDENCE_SNAPSHOT = "evidence_snapshot" => EvidenceSnapshotV1,
97    AuxiliaryRunSpec => AUXILIARY_RUN_SPEC = "auxiliary_run_spec" => AuxiliaryRunSpecV1,
98    AuxiliaryRunSnapshot => AUXILIARY_RUN_SNAPSHOT = "auxiliary_run_snapshot" => AuxiliaryRunSnapshotV1,
99    AuxiliaryRunOutput => AUXILIARY_RUN_OUTPUT = "auxiliary_run_output" => AuxiliaryRunOutputV1,
100    EvaluationResult => EVALUATION_RESULT = "evaluation_result" => EvaluationResultV1,
101    EvaluationRecord => EVALUATION_RECORD = "evaluation_record" => EvaluationRecordV1,
102}
103
104/// Errors produced while decoding or validating an evaluation wire message.
105#[derive(Debug, Clone, PartialEq, Eq, Error)]
106pub enum EvaluationProtocolError {
107    #[error("evaluation wire schema is unsupported")]
108    UnsupportedSchema,
109    #[error("evaluation wire version {0} is unsupported")]
110    UnsupportedVersion(u16),
111    #[error("evaluation wire kind is unknown")]
112    UnknownKind,
113    #[error("evaluation wire field `{0}` is invalid")]
114    InvalidField(&'static str),
115    #[error("evaluation wire payload is invalid: {0}")]
116    Payload(String),
117    #[error("evaluation wire value exceeds its bounded encoding")]
118    Encoding,
119    #[error("evaluation wire serialization failed: {0}")]
120    Serialization(String),
121}
122
123impl EvaluationProtocolError {
124    /// Stable machine-readable error code for SDK and host boundaries.
125    pub const fn code(&self) -> &'static str {
126        match self {
127            Self::UnsupportedSchema => "a3s.code.evaluation_protocol.unsupported_schema",
128            Self::UnsupportedVersion(_) => "a3s.code.evaluation_protocol.unsupported_version",
129            Self::UnknownKind => "a3s.code.evaluation_protocol.unknown_kind",
130            Self::InvalidField(_) => "a3s.code.evaluation_protocol.invalid_field",
131            Self::Payload(_) => "a3s.code.evaluation_protocol.payload",
132            Self::Encoding => "a3s.code.evaluation_protocol.encoding",
133            Self::Serialization(_) => "a3s.code.evaluation_protocol.serialization",
134        }
135    }
136}
137
138/// A strict, versioned envelope carrying one evaluation substrate value.
139///
140/// The envelope uses a JSON `Value` for the payload so the same transport can
141/// cross Node, Python, and Go without requiring those SDKs to instantiate Rust
142/// types.  [`Self::validate`] immediately decodes the value into the closed
143/// payload type selected by `kind`, preserving Rust-side validation and
144/// rejecting unknown payload fields.
145#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
146#[serde(deny_unknown_fields)]
147pub struct EvaluationWireEnvelopeV1 {
148    pub schema: String,
149    pub version: u16,
150    pub kind: EvaluationWireKindV1,
151    pub payload: Value,
152}
153
154impl EvaluationWireEnvelopeV1 {
155    /// Construct and validate an envelope from an arbitrary JSON payload.
156    pub fn new(
157        kind: EvaluationWireKindV1,
158        payload: Value,
159    ) -> Result<Self, EvaluationProtocolError> {
160        let envelope = Self {
161            schema: EVALUATION_PROTOCOL_SCHEMA_V1.to_string(),
162            version: EVALUATION_PROTOCOL_VERSION_V1,
163            kind,
164            payload,
165        };
166        envelope.validate()?;
167        Ok(envelope)
168    }
169
170    /// Decode and validate a bounded JSON wire message.
171    pub fn from_slice(bytes: &[u8]) -> Result<Self, EvaluationProtocolError> {
172        if bytes.len() > EVALUATION_PROTOCOL_MAX_MESSAGE_BYTES {
173            return Err(EvaluationProtocolError::Encoding);
174        }
175        let value: Value = serde_json::from_slice(bytes)
176            .map_err(|error| EvaluationProtocolError::Serialization(error.to_string()))?;
177        Self::from_value(value)
178    }
179
180    /// Decode and validate a JSON value at the process boundary.
181    pub fn from_value(value: Value) -> Result<Self, EvaluationProtocolError> {
182        let encoded = serde_json::to_vec(&value)
183            .map_err(|error| EvaluationProtocolError::Serialization(error.to_string()))?;
184        if encoded.len() > EVALUATION_PROTOCOL_MAX_MESSAGE_BYTES {
185            return Err(EvaluationProtocolError::Encoding);
186        }
187        let kind = value
188            .get("kind")
189            .and_then(Value::as_str)
190            .ok_or(EvaluationProtocolError::InvalidField("kind"))?;
191        if EvaluationWireKindV1::from_wire_name(kind).is_none() {
192            return Err(EvaluationProtocolError::UnknownKind);
193        }
194        let envelope: Self = serde_json::from_value(value)
195            .map_err(|error| EvaluationProtocolError::Serialization(error.to_string()))?;
196        envelope.validate()?;
197        Ok(envelope)
198    }
199
200    /// Encode a validated envelope with the process-boundary size limit.
201    pub fn to_vec(&self) -> Result<Vec<u8>, EvaluationProtocolError> {
202        self.validate()?;
203        let bytes = serde_json::to_vec(self)
204            .map_err(|error| EvaluationProtocolError::Serialization(error.to_string()))?;
205        if bytes.len() > EVALUATION_PROTOCOL_MAX_MESSAGE_BYTES {
206            return Err(EvaluationProtocolError::Encoding);
207        }
208        Ok(bytes)
209    }
210
211    /// Validate envelope identity, bounded encoding, and the selected payload.
212    pub fn validate(&self) -> Result<(), EvaluationProtocolError> {
213        if self.schema != EVALUATION_PROTOCOL_SCHEMA_V1 {
214            return Err(EvaluationProtocolError::UnsupportedSchema);
215        }
216        if self.version != EVALUATION_PROTOCOL_VERSION_V1 {
217            return Err(EvaluationProtocolError::UnsupportedVersion(self.version));
218        }
219        let encoded = serde_json::to_vec(self)
220            .map_err(|error| EvaluationProtocolError::Serialization(error.to_string()))?;
221        if encoded.len() > EVALUATION_PROTOCOL_MAX_MESSAGE_BYTES {
222            return Err(EvaluationProtocolError::Encoding);
223        }
224
225        match self.kind {
226            EvaluationWireKindV1::EvidenceReadRequest => {
227                let payload: EvidenceReadRequestV1 = self.decode_payload()?;
228                payload
229                    .validate()
230                    .map_err(|error| EvaluationProtocolError::Payload(error.to_string()))?;
231            }
232            EvaluationWireKindV1::EvidenceSnapshot => {
233                let payload: EvidenceSnapshotV1 = self.decode_payload()?;
234                payload
235                    .validate()
236                    .map_err(|error| EvaluationProtocolError::Payload(error.to_string()))?;
237            }
238            EvaluationWireKindV1::AuxiliaryRunSpec => {
239                let payload: AuxiliaryRunSpecV1 = self.decode_payload()?;
240                // The envelope can prove the spec's internal digest and shape;
241                // the host still binds it to an actual evidence snapshot at
242                // admission time.
243                payload
244                    .validate(&payload.evidence_digest)
245                    .map_err(|error| EvaluationProtocolError::Payload(error.to_string()))?;
246            }
247            EvaluationWireKindV1::AuxiliaryRunSnapshot => {
248                let payload: AuxiliaryRunSnapshotV1 = self.decode_payload()?;
249                payload
250                    .validate()
251                    .map_err(|error| EvaluationProtocolError::Payload(error.to_string()))?;
252            }
253            EvaluationWireKindV1::AuxiliaryRunOutput => {
254                let payload: AuxiliaryRunOutputV1 = self.decode_payload()?;
255                payload
256                    .validate(AUXILIARY_MAX_OUTPUT_BYTES, None)
257                    .map_err(|error| EvaluationProtocolError::Payload(error.to_string()))?;
258            }
259            EvaluationWireKindV1::EvaluationResult => {
260                let payload: EvaluationResultV1 = self.decode_payload()?;
261                payload
262                    .validate()
263                    .map_err(|error| EvaluationProtocolError::Payload(error.to_string()))?;
264            }
265            EvaluationWireKindV1::EvaluationRecord => {
266                let payload: EvaluationRecordV1 = self.decode_payload()?;
267                payload
268                    .validate()
269                    .map_err(|error| EvaluationProtocolError::Payload(error.to_string()))?;
270            }
271        }
272        Ok(())
273    }
274
275    /// Return the payload kind without exposing a second string authority.
276    pub const fn kind(&self) -> EvaluationWireKindV1 {
277        self.kind
278    }
279
280    /// Borrow the raw JSON payload for a host transport adapter.
281    pub fn payload(&self) -> &Value {
282        &self.payload
283    }
284
285    /// Construct an evidence read request envelope.
286    pub fn from_evidence_read_request(
287        payload: EvidenceReadRequestV1,
288    ) -> Result<Self, EvaluationProtocolError> {
289        Self::from_typed(EvaluationWireKindV1::EvidenceReadRequest, payload)
290    }
291
292    /// Construct an evidence snapshot envelope.
293    pub fn from_evidence_snapshot(
294        payload: EvidenceSnapshotV1,
295    ) -> Result<Self, EvaluationProtocolError> {
296        Self::from_typed(EvaluationWireKindV1::EvidenceSnapshot, payload)
297    }
298
299    /// Construct an auxiliary specification envelope.
300    pub fn from_auxiliary_run_spec(
301        payload: AuxiliaryRunSpecV1,
302    ) -> Result<Self, EvaluationProtocolError> {
303        Self::from_typed(EvaluationWireKindV1::AuxiliaryRunSpec, payload)
304    }
305
306    /// Construct an auxiliary lifecycle snapshot envelope.
307    pub fn from_auxiliary_run_snapshot(
308        payload: AuxiliaryRunSnapshotV1,
309    ) -> Result<Self, EvaluationProtocolError> {
310        Self::from_typed(EvaluationWireKindV1::AuxiliaryRunSnapshot, payload)
311    }
312
313    /// Construct an auxiliary output envelope.
314    pub fn from_auxiliary_run_output(
315        payload: AuxiliaryRunOutputV1,
316    ) -> Result<Self, EvaluationProtocolError> {
317        Self::from_typed(EvaluationWireKindV1::AuxiliaryRunOutput, payload)
318    }
319
320    /// Construct an evaluation result envelope.
321    pub fn from_evaluation_result(
322        payload: EvaluationResultV1,
323    ) -> Result<Self, EvaluationProtocolError> {
324        Self::from_typed(EvaluationWireKindV1::EvaluationResult, payload)
325    }
326
327    /// Construct an immutable evaluation record envelope.
328    pub fn from_evaluation_record(
329        payload: EvaluationRecordV1,
330    ) -> Result<Self, EvaluationProtocolError> {
331        Self::from_typed(EvaluationWireKindV1::EvaluationRecord, payload)
332    }
333
334    /// Decode a typed payload after checking that the envelope kind matches.
335    pub fn payload_as<T>(
336        &self,
337        expected: EvaluationWireKindV1,
338    ) -> Result<T, EvaluationProtocolError>
339    where
340        T: DeserializeOwned,
341    {
342        self.validate()?;
343        if self.kind != expected {
344            return Err(EvaluationProtocolError::InvalidField("kind"));
345        }
346        self.decode_payload()
347    }
348
349    fn from_typed<T: Serialize>(
350        kind: EvaluationWireKindV1,
351        payload: T,
352    ) -> Result<Self, EvaluationProtocolError> {
353        let value = serde_json::to_value(payload)
354            .map_err(|error| EvaluationProtocolError::Serialization(error.to_string()))?;
355        Self::new(kind, value)
356    }
357
358    fn decode_payload<T: DeserializeOwned>(&self) -> Result<T, EvaluationProtocolError> {
359        serde_json::from_value(self.payload.clone())
360            .map_err(|error| EvaluationProtocolError::Payload(error.to_string()))
361    }
362}
363
364#[cfg(test)]
365mod tests {
366    use super::*;
367    use crate::evaluation::{
368        AuxiliaryCapabilityProfileV1, AuxiliaryModeV1, EvidenceContentModeV1, EvidenceLimitsV1,
369        ExecutionFrameV1, ExecutionTargetV1,
370    };
371
372    fn target() -> ExecutionTargetV1 {
373        ExecutionTargetV1::new("session-protocol", "run-protocol")
374    }
375
376    fn auxiliary_spec() -> AuxiliaryRunSpecV1 {
377        AuxiliaryRunSpecV1::new(
378            ExecutionFrameV1::root(target()),
379            "protocol-fixture",
380            "return JSON",
381            "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
382        )
383        .with_id("aux-protocol")
384        .with_mode(AuxiliaryModeV1::Advisory)
385        .with_capabilities(AuxiliaryCapabilityProfileV1::tool_free())
386    }
387
388    #[test]
389    fn catalog_is_ordered_and_self_consistent() {
390        assert_eq!(EVALUATION_WIRE_KIND_DESCRIPTORS_V1.len(), 7);
391        for descriptor in EVALUATION_WIRE_KIND_DESCRIPTORS_V1 {
392            assert_eq!(descriptor.kind.wire_name(), descriptor.wire_name);
393            assert!(!descriptor.payload_type.is_empty());
394        }
395        assert_eq!(
396            EvaluationWireTypeV1::EVIDENCE_SNAPSHOT,
397            EvaluationWireKindV1::EvidenceSnapshot.wire_name()
398        );
399    }
400
401    #[test]
402    fn strict_envelope_round_trip_and_typed_projection() {
403        let mut request = EvidenceReadRequestV1::new(target());
404        request.content_mode = EvidenceContentModeV1::BoundedPayload;
405        request.limits = EvidenceLimitsV1::default();
406        let envelope = EvaluationWireEnvelopeV1::from_evidence_read_request(request.clone())
407            .expect("valid request envelope");
408        let bytes = envelope.to_vec().expect("encode");
409        let decoded = EvaluationWireEnvelopeV1::from_slice(&bytes).expect("decode");
410        let projected: EvidenceReadRequestV1 = decoded
411            .payload_as(EvaluationWireKindV1::EvidenceReadRequest)
412            .expect("typed payload");
413        assert_eq!(projected, request);
414        assert_eq!(decoded.kind().wire_name(), "evidence_read_request");
415    }
416
417    #[test]
418    fn unknown_fields_and_versions_fail_closed() {
419        let envelope = EvaluationWireEnvelopeV1::from_auxiliary_run_spec(auxiliary_spec())
420            .expect("valid spec envelope");
421        let mut value = serde_json::to_value(&envelope).expect("serialize");
422        value
423            .as_object_mut()
424            .expect("object")
425            .insert("future_field".to_string(), Value::Bool(true));
426        assert!(EvaluationWireEnvelopeV1::from_slice(
427            &serde_json::to_vec(&value).expect("serialize unknown")
428        )
429        .is_err());
430
431        let mut versioned = serde_json::to_value(&envelope).expect("serialize");
432        versioned
433            .as_object_mut()
434            .expect("object")
435            .insert("version".to_string(), Value::from(2));
436        let decoded: EvaluationWireEnvelopeV1 =
437            serde_json::from_value(versioned).expect("shape remains valid");
438        assert!(matches!(
439            decoded.validate(),
440            Err(EvaluationProtocolError::UnsupportedVersion(2))
441        ));
442    }
443
444    #[test]
445    fn mismatched_payload_kind_is_rejected() {
446        let envelope = EvaluationWireEnvelopeV1::from_auxiliary_run_spec(auxiliary_spec())
447            .expect("valid spec envelope");
448        assert!(matches!(
449            envelope.payload_as::<EvidenceReadRequestV1>(EvaluationWireKindV1::EvidenceReadRequest),
450            Err(EvaluationProtocolError::InvalidField("kind"))
451        ));
452    }
453
454    #[test]
455    fn unknown_kind_has_a_stable_boundary_error() {
456        let value = serde_json::json!({
457            "schema": EVALUATION_PROTOCOL_SCHEMA_V1,
458            "version": EVALUATION_PROTOCOL_VERSION_V1,
459            "kind": "future_kind",
460            "payload": {}
461        });
462        assert!(matches!(
463            EvaluationWireEnvelopeV1::from_value(value),
464            Err(EvaluationProtocolError::UnknownKind)
465        ));
466    }
467}