a3s_code_core/evaluation/
protocol.rs1use 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
18pub const EVALUATION_PROTOCOL_VERSION_V1: u16 = 1;
20
21pub const EVALUATION_PROTOCOL_SCHEMA_V1: &str = "a3s.code.evaluation-wire.v1";
23
24pub 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 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
31 #[serde(rename_all = "snake_case")]
32 pub enum EvaluationWireKindV1 {
33 $( $variant, )+
34 }
35
36 #[derive(Debug, Clone, Copy)]
38 pub struct EvaluationWireTypeV1;
39
40 impl EvaluationWireTypeV1 {
41 $( pub const $constant: &'static str = $wire_name; )+
42 }
43
44 #[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 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 pub const fn wire_name(self) -> &'static str {
68 match self {
69 $( Self::$variant => $wire_name, )+
70 }
71 }
72
73 pub const fn payload_type(self) -> &'static str {
75 match self {
76 $( Self::$variant => stringify!($payload), )+
77 }
78 }
79
80 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
91define_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#[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 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#[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 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 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 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 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 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 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 pub const fn kind(&self) -> EvaluationWireKindV1 {
277 self.kind
278 }
279
280 pub fn payload(&self) -> &Value {
282 &self.payload
283 }
284
285 pub fn from_evidence_read_request(
287 payload: EvidenceReadRequestV1,
288 ) -> Result<Self, EvaluationProtocolError> {
289 Self::from_typed(EvaluationWireKindV1::EvidenceReadRequest, payload)
290 }
291
292 pub fn from_evidence_snapshot(
294 payload: EvidenceSnapshotV1,
295 ) -> Result<Self, EvaluationProtocolError> {
296 Self::from_typed(EvaluationWireKindV1::EvidenceSnapshot, payload)
297 }
298
299 pub fn from_auxiliary_run_spec(
301 payload: AuxiliaryRunSpecV1,
302 ) -> Result<Self, EvaluationProtocolError> {
303 Self::from_typed(EvaluationWireKindV1::AuxiliaryRunSpec, payload)
304 }
305
306 pub fn from_auxiliary_run_snapshot(
308 payload: AuxiliaryRunSnapshotV1,
309 ) -> Result<Self, EvaluationProtocolError> {
310 Self::from_typed(EvaluationWireKindV1::AuxiliaryRunSnapshot, payload)
311 }
312
313 pub fn from_auxiliary_run_output(
315 payload: AuxiliaryRunOutputV1,
316 ) -> Result<Self, EvaluationProtocolError> {
317 Self::from_typed(EvaluationWireKindV1::AuxiliaryRunOutput, payload)
318 }
319
320 pub fn from_evaluation_result(
322 payload: EvaluationResultV1,
323 ) -> Result<Self, EvaluationProtocolError> {
324 Self::from_typed(EvaluationWireKindV1::EvaluationResult, payload)
325 }
326
327 pub fn from_evaluation_record(
329 payload: EvaluationRecordV1,
330 ) -> Result<Self, EvaluationProtocolError> {
331 Self::from_typed(EvaluationWireKindV1::EvaluationRecord, payload)
332 }
333
334 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}