Skip to main content

rustev_contract/
serve.rs

1//! Pure service documents and operation metadata (spec 042).
2//!
3//! Engineering bounds: names use the existing 256-byte Handle; file and
4//! endpoint references use 4096 bytes; configuration collections use 1024
5//! entries, listeners 64. Queues and concurrency without a specified range
6//! use 0 or 1 through 65536; timeouts use 1 through 600000 milliseconds.
7//! These values describe data, never permission or transport behavior.
8use crate::bounded::{ParseError, ParseLimits, parse_bounded};
9use crate::canonical::{CanonicalError, tagged_digest};
10use crate::ids::{ContentDigest, DescriptorId, ExecutionPolicyId, PlanId};
11use crate::plan_bundle::{PlanBundle, PlanBundleSignature, RegistrationId, Signer, SigningMode};
12use crate::scope::Handle;
13use crate::{Document, DocumentError, Timestamp};
14use schemars::{JsonSchema, Schema, SchemaGenerator};
15use serde::{Deserialize, Deserializer, Serialize};
16use std::{borrow::Cow, collections::BTreeMap, marker::PhantomData};
17
18pub const MIB: usize = 1024 * 1024;
19/// Envelope bounds leave room for nested documents' own scanners.
20pub const SERVICE_LIMITS: ParseLimits = ParseLimits {
21    max_bytes: 4 * MIB,
22    max_depth: 64,
23    max_string_bytes: 8 * MIB,
24    max_collection_len: 10000,
25    max_total_values: 1100000,
26    max_number_bytes: 40,
27    allow_fractional_numbers: true,
28};
29pub const BUNDLE_LIMITS: ParseLimits = ParseLimits {
30    max_bytes: 16 * MIB,
31    ..SERVICE_LIMITS
32};
33pub const DECISION_LIMITS: ParseLimits = ParseLimits {
34    max_bytes: 9 * MIB,
35    ..SERVICE_LIMITS
36};
37pub const REFUSAL_LIMITS: ParseLimits = ParseLimits {
38    max_bytes: 4096,
39    ..SERVICE_LIMITS
40};
41
42/// Inclusive, validated integer range. No implicit defaults.
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
44#[serde(transparent)]
45pub struct Number<const MIN: u64, const MAX: u64>(u64);
46impl<const MIN: u64, const MAX: u64> Number<MIN, MAX> {
47    pub fn new(value: u64) -> Result<Self, String> {
48        if (MIN..=MAX).contains(&value) {
49            Ok(Self(value))
50        } else {
51            Err(format!("integer outside {MIN}..={MAX}"))
52        }
53    }
54    pub fn get(self) -> u64 {
55        self.0
56    }
57}
58impl<'de, const MIN: u64, const MAX: u64> Deserialize<'de> for Number<MIN, MAX> {
59    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
60        Self::new(u64::deserialize(d)?).map_err(serde::de::Error::custom)
61    }
62}
63impl<const MIN: u64, const MAX: u64> JsonSchema for Number<MIN, MAX> {
64    fn schema_name() -> Cow<'static, str> {
65        format!("Number_{MIN}_{MAX}").into()
66    }
67    fn json_schema(_: &mut SchemaGenerator) -> Schema {
68        // `u64::MAX` is not exactly representable as a binary64 JSON number,
69        // so an unbounded-above count states only its minimum; the parser
70        // still enforces the `u64` range.
71        if MAX == u64::MAX {
72            schemars::json_schema!({"type":"integer","minimum":MIN})
73        } else {
74            schemars::json_schema!({"type":"integer","minimum":MIN,"maximum":MAX})
75        }
76    }
77}
78/// UTF-8 byte bounds, applied after JSON unescaping.
79#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
80#[serde(transparent)]
81pub struct Text<const MIN: usize, const MAX: usize>(String);
82impl<const MIN: usize, const MAX: usize> Text<MIN, MAX> {
83    pub fn new(value: impl Into<String>) -> Result<Self, String> {
84        let value = value.into();
85        if (MIN..=MAX).contains(&value.len()) {
86            Ok(Self(value))
87        } else {
88            Err(format!("text outside {MIN}..={MAX} bytes"))
89        }
90    }
91    pub fn as_str(&self) -> &str {
92        &self.0
93    }
94}
95impl<'de, const MIN: usize, const MAX: usize> Deserialize<'de> for Text<MIN, MAX> {
96    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
97        Self::new(String::deserialize(d)?).map_err(serde::de::Error::custom)
98    }
99}
100impl<const MIN: usize, const MAX: usize> JsonSchema for Text<MIN, MAX> {
101    fn schema_name() -> Cow<'static, str> {
102        format!("Text_{MIN}_{MAX}").into()
103    }
104    fn json_schema(_: &mut SchemaGenerator) -> Schema {
105        schemars::json_schema!({"type":"string","minLength":MIN,"maxLength":MAX,"description":"Length is enforced in decoded UTF-8 bytes by the parser."})
106    }
107}
108#[derive(Debug, Clone, PartialEq, Serialize)]
109#[serde(transparent)]
110pub struct List<T, const MIN: usize, const MAX: usize>(Vec<T>);
111impl<T, const MIN: usize, const MAX: usize> List<T, MIN, MAX> {
112    pub fn new(values: Vec<T>) -> Result<Self, String> {
113        if (MIN..=MAX).contains(&values.len()) {
114            Ok(Self(values))
115        } else {
116            Err(format!("list outside {MIN}..={MAX} entries"))
117        }
118    }
119    pub fn as_slice(&self) -> &[T] {
120        &self.0
121    }
122}
123impl<'de, T: Deserialize<'de>, const MIN: usize, const MAX: usize> Deserialize<'de>
124    for List<T, MIN, MAX>
125{
126    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
127        Self::new(Vec::<T>::deserialize(d)?).map_err(serde::de::Error::custom)
128    }
129}
130impl<T: JsonSchema, const MIN: usize, const MAX: usize> JsonSchema for List<T, MIN, MAX> {
131    fn schema_name() -> Cow<'static, str> {
132        format!("List_{}_{}_{}", T::schema_name(), MIN, MAX).into()
133    }
134    fn json_schema(g: &mut SchemaGenerator) -> Schema {
135        let items = g.subschema_for::<T>();
136        schemars::json_schema!({"type":"array","items":items,"minItems":MIN,"maxItems":MAX})
137    }
138}
139/// Explicit absence for identity-bearing fields.
140#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
141#[serde(rename_all = "snake_case", deny_unknown_fields)]
142pub enum Maybe<T> {
143    None,
144    Some(T),
145}
146
147/// An embedded document, checked by its own bounded parser. Serialization
148/// preserves its original bytes, including evidence number representations.
149#[derive(Debug, Clone)]
150pub struct Embedded<T> {
151    raw: Box<serde_json::value::RawValue>,
152    marker: PhantomData<T>,
153}
154impl<T> PartialEq for Embedded<T> {
155    fn eq(&self, other: &Self) -> bool {
156        self.raw.get() == other.raw.get()
157    }
158}
159impl<T: Document> Embedded<T> {
160    pub fn parse(bytes: &[u8]) -> Result<Self, DocumentError> {
161        T::parse(bytes)?;
162        let raw = serde_json::from_slice(bytes).map_err(|e| invalid(e.to_string()))?;
163        Ok(Self {
164            raw,
165            marker: PhantomData,
166        })
167    }
168    pub fn bytes(&self) -> &[u8] {
169        self.raw.get().as_bytes()
170    }
171    pub fn document(&self) -> Result<T, DocumentError> {
172        T::parse(self.bytes())
173    }
174}
175impl<T> Serialize for Embedded<T> {
176    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
177        self.raw.serialize(s)
178    }
179}
180impl<'de, T: Document> Deserialize<'de> for Embedded<T> {
181    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
182        let raw = Box::<serde_json::value::RawValue>::deserialize(d)?;
183        T::parse(raw.get().as_bytes()).map_err(serde::de::Error::custom)?;
184        Ok(Self {
185            raw,
186            marker: PhantomData,
187        })
188    }
189}
190/// An embedded document is described by name, not inlined: its own schema
191/// file describes it, and its own bounded parser checks it. A schema is never
192/// stricter than the parser (spec 027 3.2.2), so the looser object is exact
193/// enough, and it keeps each envelope from copying the run record, judgment
194/// and bundle schemas into every service document.
195impl<T: Document> JsonSchema for Embedded<T> {
196    fn schema_name() -> Cow<'static, str> {
197        format!("Embedded_{}", T::SCHEMA.replace(['.', '/', '-'], "_")).into()
198    }
199    fn json_schema(_: &mut SchemaGenerator) -> Schema {
200        schemars::json_schema!({
201            "type": "object",
202            "description": format!(
203                "An embedded `{}` document, carried as its original bytes and checked by that document's own bounded parser.",
204                T::SCHEMA
205            )
206        })
207    }
208}
209fn present<'de, D: Deserializer<'de>, T: Deserialize<'de>>(d: D) -> Result<Option<T>, D::Error> {
210    T::deserialize(d).map(Some)
211}
212pub(crate) fn invalid(message: impl Into<String>) -> DocumentError {
213    DocumentError::Parse(ParseError::Schema {
214        message: message.into(),
215    })
216}
217pub(crate) trait Validate {
218    fn validate(&self) -> Result<(), String> {
219        Ok(())
220    }
221}
222/// Fixed schema marker fields also reject wrong schemas in nested serde reads.
223macro_rules! service_document {
224    ($name:ident,$marker:ident,$schema:literal,$limits:expr,{$($fields:tt)*})=>{
225        #[derive(Debug,Clone,Copy,PartialEq,Eq,Serialize,Deserialize,JsonSchema)]
226        pub enum $marker {#[serde(rename=$schema)] V1}
227        #[derive(Debug,Clone,PartialEq,Serialize,Deserialize,JsonSchema)]
228        #[serde(deny_unknown_fields)]
229        #[schemars(transform = crate::serve::document_shape)]
230        pub struct $name {pub schema:$marker,$($fields)*}
231        impl Document for $name {
232            const SCHEMA:&'static str=$schema;const LIMITS:ParseLimits=$limits;
233            fn schema(&self)->&str{Self::SCHEMA}
234            fn parse(bytes:&[u8])->Result<Self,DocumentError>{let value:Self=parse_bounded(bytes,&Self::LIMITS).map_err(DocumentError::Parse)?;value.validate().map_err(invalid)?;Ok(value)}
235            fn canonical(&self)->Result<Vec<u8>,CanonicalError>{canonical_envelope(self)}
236            fn record_canonical(&self)->Result<Vec<u8>,CanonicalError>{canonical_envelope(self)}
237        }
238    };
239}
240pub(crate) use service_document;
241/// Express cross-field rules where JSON Schema can do so. The parser remains
242/// authoritative, including relationships between counts and array lengths.
243pub(crate) fn document_shape(schema: &mut Schema) {
244    let object = schema.ensure_object();
245    // Optional members may be omitted but never explicitly null.
246    if let Some(properties) = object
247        .get_mut("properties")
248        .and_then(serde_json::Value::as_object_mut)
249    {
250        for name in [
251            "snapshot",
252            "context",
253            "judgment",
254            "run_record",
255            "attempts",
256            "compile_refusal",
257        ] {
258            if let Some(property) = properties
259                .get_mut(name)
260                .and_then(serde_json::Value::as_object_mut)
261            {
262                // Drop the `null` branch serde's `Option` adds, so the schema
263                // and the generated declarations state "absent or a value",
264                // as the parser does, rather than allowing `null`.
265                let branches = property
266                    .get("anyOf")
267                    .and_then(serde_json::Value::as_array)
268                    .map(|b| {
269                        b.iter()
270                            .filter(|v| v.get("type") != Some(&serde_json::json!("null")))
271                            .cloned()
272                            .collect::<Vec<_>>()
273                    });
274                match branches {
275                    Some(mut b) if b.len() == 1 => {
276                        property.remove("anyOf");
277                        if let Some(serde_json::Value::Object(only)) = b.pop() {
278                            for (k, v) in only {
279                                property.entry(k).or_insert(v);
280                            }
281                        }
282                    }
283                    Some(b) => {
284                        property.insert("anyOf".into(), serde_json::Value::Array(b));
285                    }
286                    None => {
287                        if let Some(serde_json::Value::Array(types)) = property.get_mut("type") {
288                            types.retain(|t| t != "null");
289                        }
290                    }
291                }
292            }
293        }
294    }
295    let properties = object
296        .get("properties")
297        .and_then(serde_json::Value::as_object)
298        .cloned()
299        .unwrap_or_default();
300    // Exactly one of `snapshot` and `context` is the parser's rule. A
301    // top-level `oneOf` would state it, but the declaration generator turns
302    // such a schema into an open TypeScript type, so it is stated in the
303    // description instead; the schema stays looser than the parser, never
304    // stricter (spec 027 3.2.2).
305    if properties.contains_key("snapshot") && properties.contains_key("context") {
306        object.insert(
307            "description".into(),
308            serde_json::json!("Exactly one of `snapshot` and `context` is present; the parser refuses both or neither."),
309        );
310    }
311    if properties.contains_key("compile_refusal") {
312        object.insert("if".into(),serde_json::json!({"properties":{"code":{"const":"compile_refused"}},"required":["code"]}));
313        object.insert(
314            "then".into(),
315            serde_json::json!({"required":["compile_refusal"]}),
316        );
317        object.insert(
318            "else".into(),
319            serde_json::json!({"not":{"required":["compile_refusal"]}}),
320        );
321    }
322    if properties.contains_key("run_record") {
323        object.insert("allOf".into(),serde_json::json!([
324            {"if":{"properties":{"status":{"const":"evidence_not_delivered"}}},"then":{"required":["run_record"],"properties":{"evidence":{"required":["not_delivered"]}}},"else":{"not":{"required":["run_record"]},"properties":{"evidence":{"not":{"required":["not_delivered"]}}}}},
325            {"if":{"properties":{"status":{"const":"judged"}}},"then":{"required":["judgment"]}},
326            {"if":{"properties":{"status":{"const":"cancelled"}}},"then":{"not":{"required":["judgment"]}}}
327        ]));
328    }
329}
330
331/// Sort envelope keys while retaining the runtime's evidence bytes verbatim.
332/// Other values use the existing record canonicalizer, without changing it.
333pub(crate) fn canonical_envelope<T: Serialize>(value: &T) -> Result<Vec<u8>, CanonicalError> {
334    fn render(raw: &serde_json::value::RawValue) -> Result<Vec<u8>, CanonicalError> {
335        if raw.get().starts_with('{') {
336            let fields: BTreeMap<String, Box<serde_json::value::RawValue>> =
337                serde_json::from_str(raw.get()).map_err(|e| CanonicalError::Serialize {
338                    message: e.to_string(),
339                })?;
340            let evidence = fields.get("schema").is_some_and(|s| {
341                matches!(
342                    s.get(),
343                    "\"rustev.serve-decision/1\"" | "\"rustev.serve-refusal/1\""
344                )
345            });
346            let mut out = vec![b'{'];
347            for (index, (key, value)) in fields.iter().enumerate() {
348                if index > 0 {
349                    out.push(b',');
350                }
351                out.extend(serde_json::to_vec(key).unwrap());
352                out.push(b':');
353                if evidence && matches!(key.as_str(), "judgment" | "run_record" | "compile_refusal")
354                {
355                    out.extend(value.get().as_bytes());
356                } else {
357                    out.extend(render(value)?);
358                }
359            }
360            out.push(b'}');
361            Ok(out)
362        } else if raw.get().starts_with('[') {
363            let values: Vec<Box<serde_json::value::RawValue>> = serde_json::from_str(raw.get())
364                .map_err(|e| CanonicalError::Serialize {
365                    message: e.to_string(),
366                })?;
367            let mut out = vec![b'['];
368            for (index, value) in values.iter().enumerate() {
369                if index > 0 {
370                    out.push(b',');
371                }
372                out.extend(render(value)?);
373            }
374            out.push(b']');
375            Ok(out)
376        } else {
377            let value: serde_json::Value =
378                serde_json::from_str(raw.get()).map_err(|e| CanonicalError::Serialize {
379                    message: e.to_string(),
380                })?;
381            crate::canonical::record_canonical_bytes(&value)
382        }
383    }
384    let raw = serde_json::value::to_raw_value(value).map_err(|e| CanonicalError::Serialize {
385        message: e.to_string(),
386    })?;
387    render(&raw)
388}
389
390service_document!(ServeRegister,RegisterSchema,"rustev.serve-register/1",BUNDLE_LIMITS,{
391    pub bundle:Embedded<PlanBundle>,pub signature:Maybe<Embedded<PlanBundleSignature>>,
392});
393impl Validate for ServeRegister {}
394#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
395#[serde(deny_unknown_fields)]
396pub struct Context {
397    pub source: Handle,
398}
399service_document!(ServeDecide,DecideSchema,"rustev.serve-decide/1",SERVICE_LIMITS,{
400    pub decision_id:Handle,pub evaluation_time:Timestamp,
401    # [serde(default,skip_serializing_if="Option::is_none",deserialize_with="present")] pub snapshot:Option<Embedded<crate::snapshot::Snapshot>>,
402    # [serde(default,skip_serializing_if="Option::is_none",deserialize_with="present")] pub context:Option<Context>,
403    // Extension point: spec 036 (mid-plan acquisition).
404});
405impl Validate for ServeDecide {
406    fn validate(&self) -> Result<(), String> {
407        if self.snapshot.is_some() == self.context.is_some() {
408            Err("exactly one of snapshot or context is required".into())
409        } else {
410            Ok(())
411        }
412    }
413}
414#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
415#[serde(rename_all = "snake_case")]
416pub enum DecisionStatus {
417    Judged,
418    Cancelled,
419    EvidenceNotDelivered,
420}
421#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
422#[serde(rename_all = "snake_case", deny_unknown_fields)]
423pub enum EvidenceDisposition {
424    Receipt {
425        receipt: Text<1, 4096>,
426    },
427    Ticket {
428        ticket: Handle,
429    },
430    Dropped {
431        count: Number<1, { u64::MAX }>,
432    },
433    NotDelivered {
434        error: Text<0, 256>,
435        may_have_received: bool,
436    },
437}
438service_document!(ServeDecision,DecisionSchema,"rustev.serve-decision/1",DECISION_LIMITS,{
439    pub status:DecisionStatus,pub decision_id:Handle,pub plan_id:PlanId,pub registration_id:RegistrationId,
440    pub signer:Signer,pub execution_policy_id:Maybe<ExecutionPolicyId>,pub service_build:Handle,
441    pub protocol:Protocol,pub configuration_digest:ContentDigest,
442    # [serde(default,skip_serializing_if="Option::is_none",deserialize_with="present")] pub judgment:Option<Embedded<crate::judgment::Judgment>>,
443    pub run_record_digest:ContentDigest,pub evidence:EvidenceDisposition,pub replayed:bool,
444    # [serde(default,skip_serializing_if="Option::is_none",deserialize_with="present")] pub run_record:Option<Embedded<crate::run::RunRecord>>,
445});
446impl Validate for ServeDecision {
447    fn validate(&self) -> Result<(), String> {
448        if (self.status == DecisionStatus::EvidenceNotDelivered) != self.run_record.is_some() {
449            return Err("run_record is required only for evidence_not_delivered".into());
450        }
451        // 042 3.4.3: the judgment is absent only when cancelled. Every
452        // `evidence_not_delivered` decision carries its run record (checked
453        // above; a cancelled run still has one), but its judgment may be
454        // present (judged, then delivery failed) or absent (cancelled, then
455        // delivery failed), so neither is refused there.
456        if (self.status == DecisionStatus::Judged && self.judgment.is_none())
457            || (self.status == DecisionStatus::Cancelled && self.judgment.is_some())
458        {
459            return Err("judgment does not match status".into());
460        }
461        if (self.status == DecisionStatus::EvidenceNotDelivered)
462            != matches!(self.evidence, EvidenceDisposition::NotDelivered { .. })
463        {
464            return Err("evidence disposition does not match status".into());
465        }
466        Ok(())
467    }
468}
469#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
470pub enum Protocol {
471    #[serde(rename = "rustev.serve/1")]
472    V1,
473}
474#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
475#[serde(rename_all = "snake_case")]
476pub enum DecisionState {
477    Queued,
478    Running,
479}
480service_document!(ServeDecisionState,DecisionStateSchema,"rustev.serve-decision-state/1",SERVICE_LIMITS,{
481    pub decision_id:Handle,pub plan_id:PlanId,pub state:DecisionState,
482});
483impl Validate for ServeDecisionState {}
484#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
485#[serde(rename_all = "snake_case")]
486pub enum CancelStatus {
487    Queued,
488    Requested,
489    AlreadyFinished,
490}
491#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
492#[serde(rename_all = "snake_case")]
493pub enum CancelAck {
494    NotRequested,
495    Stopped,
496    Unconfirmed,
497    NoAnswer,
498}
499impl From<crate::run::Cancellation> for CancelAck {
500    fn from(value: crate::run::Cancellation) -> Self {
501        use crate::run::{CancelAnswer, Cancellation};
502        match value {
503            Cancellation::NotRequested => Self::NotRequested,
504            Cancellation::Requested {
505                answer: CancelAnswer::Stopped,
506            } => Self::Stopped,
507            Cancellation::Requested {
508                answer: CancelAnswer::Unconfirmed,
509            } => Self::Unconfirmed,
510            Cancellation::Requested {
511                answer: CancelAnswer::NotObserved,
512            } => Self::NoAnswer,
513        }
514    }
515}
516#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
517#[serde(deny_unknown_fields)]
518pub struct AttemptCancellation {
519    pub attempt_id: Text<1, 4096>,
520    pub cancel_ack: CancelAck,
521    pub remote_end: crate::run::RemoteState,
522}
523service_document!(ServeCancel,CancelSchema,"rustev.serve-cancel/1",SERVICE_LIMITS,{
524    pub status:CancelStatus,
525    # [serde(default,skip_serializing_if="Option::is_none",deserialize_with="present")]pub attempts:Option<List<AttemptCancellation,0,10000>>,
526});
527impl Validate for ServeCancel {}
528/// Query/header bounds, shared with transport generators.
529pub type CancelWaitMs = Number<0, 60000>;
530pub type DeadlineMs = Number<1, { u64::MAX }>;
531
532#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
533#[serde(rename_all = "snake_case")]
534pub enum RefusalCode {
535    Overloaded,
536    IdempotencyWindowFull,
537    QueueDeadline,
538    ShuttingDown,
539    NotReady,
540    ContextUnavailable,
541    CancelledBeforeAdmission,
542    DecisionIdConflict,
543    RegistrationConflict,
544    DecisionInProgress,
545    RegistryFull,
546    InvalidRequest,
547    InvalidDocument,
548    PrincipalInvalid,
549    InvalidTraceContext,
550    InvalidDeadline,
551    Unauthenticated,
552    PlanNotRegistered,
553    DecisionNotFound,
554    EndpointUnavailable,
555    BodyTooLarge,
556    HeadersTooLarge,
557    UnsupportedMediaType,
558    CompileRefused,
559    PlanIdMismatch,
560    BindingUnknown,
561    ProfileUnsupported,
562    BundleTampered,
563    SignatureInvalid,
564    SignatureUntrusted,
565    UnsignedRefused,
566}
567#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
568#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
569pub enum GrpcCode {
570    ResourceExhausted,
571    Unavailable,
572    Aborted,
573    AlreadyExists,
574    FailedPrecondition,
575    InvalidArgument,
576    Unauthenticated,
577    NotFound,
578}
579impl RefusalCode {
580    pub fn http_status(self) -> u16 {
581        use RefusalCode::*;
582        match self {
583            Overloaded | IdempotencyWindowFull => 429,
584            QueueDeadline | ShuttingDown | NotReady => 503,
585            ContextUnavailable => 502,
586            CancelledBeforeAdmission
587            | DecisionIdConflict
588            | RegistrationConflict
589            | DecisionInProgress
590            | RegistryFull => 409,
591            InvalidRequest | InvalidDocument | PrincipalInvalid | InvalidTraceContext
592            | InvalidDeadline => 400,
593            Unauthenticated => 401,
594            PlanNotRegistered | DecisionNotFound | EndpointUnavailable => 404,
595            BodyTooLarge => 413,
596            HeadersTooLarge => 431,
597            UnsupportedMediaType => 415,
598            _ => 422,
599        }
600    }
601    pub fn grpc_code(self) -> GrpcCode {
602        use RefusalCode::*;
603        match self {
604            Overloaded | IdempotencyWindowFull | BodyTooLarge | HeadersTooLarge => {
605                GrpcCode::ResourceExhausted
606            }
607            QueueDeadline | ShuttingDown | NotReady | ContextUnavailable => GrpcCode::Unavailable,
608            CancelledBeforeAdmission | DecisionInProgress => GrpcCode::Aborted,
609            DecisionIdConflict | RegistrationConflict => GrpcCode::AlreadyExists,
610            InvalidRequest | InvalidDocument | PrincipalInvalid | InvalidTraceContext
611            | InvalidDeadline | UnsupportedMediaType => GrpcCode::InvalidArgument,
612            Unauthenticated => GrpcCode::Unauthenticated,
613            PlanNotRegistered | DecisionNotFound | EndpointUnavailable => GrpcCode::NotFound,
614            _ => GrpcCode::FailedPrecondition,
615        }
616    }
617}
618/// Only false is representable. A refusal never claims a decision ran.
619#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, JsonSchema)]
620#[serde(transparent)]
621pub struct DecisionDidNotRun(#[schemars(extend("const" = false))] bool);
622impl<'de> Deserialize<'de> for DecisionDidNotRun {
623    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
624        if bool::deserialize(d)? {
625            Err(serde::de::Error::custom("decision_ran must be false"))
626        } else {
627            Ok(Self(false))
628        }
629    }
630}
631/// An oversized compiler refusal is represented by its digest and byte length.
632#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
633#[serde(rename_all = "snake_case", deny_unknown_fields)]
634pub enum CompileRefusal {
635    Document(CompilerRefusal),
636    Digest {
637        digest: ContentDigest,
638        length: Number<2049, { u64::MAX }>,
639    },
640}
641/// Surface-owned compiler output, kept byte-for-byte. The contract does not
642/// duplicate the compiler's semantic refusal taxonomy. Its outer renderer
643/// fields are checked here, and the entire document is limited to 2 KiB.
644#[derive(Debug, Clone)]
645pub struct CompilerRefusal(Box<serde_json::value::RawValue>);
646impl PartialEq for CompilerRefusal {
647    fn eq(&self, other: &Self) -> bool {
648        self.0.get() == other.0.get()
649    }
650}
651impl Serialize for CompilerRefusal {
652    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
653        self.0.serialize(s)
654    }
655}
656impl<'de> Deserialize<'de> for CompilerRefusal {
657    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
658        let raw = Box::<serde_json::value::RawValue>::deserialize(d)?;
659        let _: CompilerRefusalFields = parse_bounded(
660            raw.get().as_bytes(),
661            &ParseLimits {
662                max_bytes: 2048,
663                ..REFUSAL_LIMITS
664            },
665        )
666        .map_err(serde::de::Error::custom)?;
667        Ok(Self(raw))
668    }
669}
670#[derive(Debug, Serialize, Deserialize, JsonSchema)]
671#[serde(deny_unknown_fields)]
672struct CompilerRefusalFields {
673    command: Text<1, 2048>,
674    status: Refused,
675    // Category numbers are fixed by the specs: 18 and 19 are spec 035's
676    // screening categories, 20 and 21 are spec 036 3.9.3's, and 22 to 24
677    // are spec 037 3.5's. This is a wire range check only; nothing here
678    // converts the number to a `Category`.
679    category: Number<1, 24>,
680    category_name: Text<1, 2048>,
681    subject: Text<0, 2048>,
682    detail: Text<0, 2048>,
683    shortfalls: List<CompilerShortfall, 0, 1024>,
684}
685#[derive(Debug, Serialize, Deserialize, JsonSchema)]
686enum Refused {
687    #[serde(rename = "refused")]
688    Refused,
689}
690#[derive(Debug, Serialize, Deserialize, JsonSchema)]
691#[serde(deny_unknown_fields)]
692struct CompilerShortfall {
693    backend_id: Handle,
694    reasons: List<CompilerReason, 0, 1024>,
695}
696#[derive(Debug, Serialize, Deserialize, JsonSchema)]
697#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
698enum CompilerReason {
699    UnknownBackend,
700    Operation {
701        operation: crate::definition::Operation,
702    },
703    OutputKind {
704        offered: crate::descriptor::OutputKind,
705        required: crate::definition::RequiredKind,
706    },
707    Options {
708        max: u64,
709        needed: u64,
710    },
711    InputLimit {
712        max: u64,
713        needed: u64,
714    },
715    Determinism {
716        offered: Text<1, 2048>,
717        required: Text<1, 2048>,
718    },
719    ArtifactPin {
720        pinned: ContentDigest,
721        offered: ContentDigest,
722    },
723    // Screener shortfalls (spec 035, 3.5.4), as rustev-surface emits them.
724    ScreenProfile,
725    ScreenTextLimit {
726        max: u64,
727        needed: u64,
728    },
729    ScreenInputExcess,
730}
731impl JsonSchema for CompilerRefusal {
732    fn schema_name() -> Cow<'static, str> {
733        "CompilerRefusal".into()
734    }
735    fn json_schema(g: &mut SchemaGenerator) -> Schema {
736        g.subschema_for::<CompilerRefusalFields>()
737    }
738}
739service_document!(ServeRefusal,RefusalSchema,"rustev.serve-refusal/1",REFUSAL_LIMITS,{
740    pub code:RefusalCode,pub detail:Text<0,256>,pub decision_ran:DecisionDidNotRun,
741    # [serde(default,skip_serializing_if="Option::is_none",deserialize_with="present")]pub compile_refusal:Option<CompileRefusal>,
742    // No retry_after_ms until a spec gives the runtime a basis for one.
743});
744impl Validate for ServeRefusal {
745    fn validate(&self) -> Result<(), String> {
746        if (self.code == RefusalCode::CompileRefused) != self.compile_refusal.is_some() {
747            Err("compile_refusal is required only for compile_refused".into())
748        } else {
749            Ok(())
750        }
751    }
752}
753
754#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
755#[serde(rename_all = "snake_case", deny_unknown_fields)]
756pub enum StepOutcome {
757    Supplied,
758    Unresolved { reason: crate::judgment::Unresolved },
759    // Extension point: spec 036 (skipped by tiered widening).
760}
761/// Spec 037 field events are described now; the service must advertise
762/// their absence until generation is implemented. Values remain provisional.
763#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
764#[serde(deny_unknown_fields)]
765pub struct GeneratedField {
766    pub step: Handle,
767    pub item: Maybe<Handle>,
768    pub field: GeneratedFieldName,
769    pub value: GeneratedValue,
770    pub attempt_id: Text<1, 4096>,
771    pub provisional: Provisional,
772}
773#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
774#[serde(rename_all = "snake_case", deny_unknown_fields)]
775pub enum GeneratedValue {
776    Text(Text<0, 16384>),
777    List(List<Text<1, 64>, 0, 64>),
778}
779#[derive(Debug, Clone, PartialEq, Serialize, JsonSchema)]
780#[serde(transparent)]
781pub struct GeneratedFieldName(#[schemars(regex(pattern = "^[a-z][a-z0-9_]{0,63}$"))] Text<1, 64>);
782impl<'de> Deserialize<'de> for GeneratedFieldName {
783    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
784        let value = Text::<1, 64>::deserialize(d)?;
785        let mut bytes = value.as_str().bytes();
786        if !bytes.next().is_some_and(|b| b.is_ascii_lowercase())
787            || !bytes.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_')
788        {
789            return Err(serde::de::Error::custom("invalid generated field name"));
790        }
791        Ok(Self(value))
792    }
793}
794#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, JsonSchema)]
795#[serde(transparent)]
796pub struct Provisional(#[schemars(extend("const" = true))] bool);
797impl<'de> Deserialize<'de> for Provisional {
798    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
799        if bool::deserialize(d)? {
800            Ok(Self(true))
801        } else {
802            Err(serde::de::Error::custom("field must be provisional"))
803        }
804    }
805}
806#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
807#[serde(rename_all = "snake_case", deny_unknown_fields)]
808pub enum FinalResult {
809    Decision(Embedded<ServeDecision>),
810    Refusal(Embedded<ServeRefusal>),
811}
812#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
813#[serde(rename_all = "snake_case", deny_unknown_fields)]
814pub enum EventKind {
815    Admitted,
816    Step {
817        step_id: Handle,
818        instance_ids: List<Handle, 0, 4096>,
819        outcome: StepOutcome,
820        derivation: crate::judgment::Derivation,
821    },
822    Field(GeneratedField),
823    Final {
824        result: FinalResult,
825        events_dropped: u64,
826    },
827}
828service_document!(ServeEvent,EventSchema,"rustev.serve-event/1",ParseLimits{max_bytes:10*MIB,..SERVICE_LIMITS},{pub sequence:u64,pub kind:EventKind,});
829impl Validate for ServeEvent {}
830
831/// Resolver references are prefix:relative/path, never URLs or absolute paths.
832/// Filesystem containment and scope are checked by the host resolver.
833#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)]
834#[serde(transparent)]
835pub struct Reference(Text<1, 4096>);
836impl Reference {
837    pub fn as_str(&self) -> &str {
838        self.0.as_str()
839    }
840}
841impl<'de> Deserialize<'de> for Reference {
842    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
843        let value = Text::<1, 4096>::deserialize(d)?;
844        let valid = value
845            .as_str()
846            .split_once(':')
847            .is_some_and(|(prefix, path)| {
848                !prefix.is_empty()
849                    && prefix
850                        .bytes()
851                        .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-')
852                    && !path.contains([':', '\\', '\0'])
853                    && path
854                        .split('/')
855                        .all(|s| !s.is_empty() && s != "." && s != "..")
856            });
857        if !valid {
858            return Err(serde::de::Error::custom(
859                "reference must be prefix:relative/path without empty, dot or parent components",
860            ));
861        }
862        Ok(Self(value))
863    }
864}
865#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
866#[serde(rename_all = "snake_case", deny_unknown_fields)]
867#[serde(bound(deserialize = "T: Document", serialize = ""))]
868pub enum DocumentInput<T: Document> {
869    Document(Embedded<T>),
870    Reference(Reference),
871}
872#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
873#[serde(deny_unknown_fields)]
874pub struct ReplayOptions {
875    pub scope: crate::scope::Scope,
876    pub now_ms: Timestamp,
877    pub host_cap_ms: Maybe<Number<1, 2592000000>>,
878}
879service_document!(ServeReplay,ReplaySchema,"rustev.serve-replay/1",SERVICE_LIMITS,{
880    pub bundle:DocumentInput<crate::replay::ReplayBundle>,pub inputs:List<ReferencedInput,0,1024>,pub config:ReplayOptions,
881});
882impl Validate for ServeReplay {}
883/// A resolver binding for a typed embedded replay dependency.
884#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
885#[serde(deny_unknown_fields)]
886pub struct ReferencedInput {
887    pub reference: Reference,
888    pub input: ReplayInput,
889}
890#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
891#[serde(rename_all = "snake_case", deny_unknown_fields)]
892pub enum ReplayInput {
893    Plan(DocumentInput<crate::plan::Plan>),
894    Snapshot(DocumentInput<crate::snapshot::Snapshot>),
895    Run(DocumentInput<crate::run::RunRecord>),
896    Evidence(DocumentInput<crate::evidence::EvidenceRecord>),
897    Judgment(DocumentInput<crate::judgment::Judgment>),
898    BackendOutput(DocumentInput<crate::output::BackendOutputDoc>),
899    ExecutionPolicy(DocumentInput<crate::execution::ExecutionPolicy>),
900    Calibration(DocumentInput<crate::calibration::CalibrationArtifact>),
901}
902#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
903#[serde(deny_unknown_fields)]
904pub struct EvaluationDependencies {
905    pub descriptors: List<Reference, 0, 1024>,
906    pub rules: List<Reference, 0, 1024>,
907    pub calibrations: List<Reference, 0, 1024>,
908}
909#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
910#[serde(deny_unknown_fields)]
911pub struct RepeatInputs {
912    pub source: RepeatSource,
913    pub config: Reference,
914    pub manifest: Reference,
915    pub runs: List<RepeatRun, 1, 64>,
916}
917#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
918#[serde(rename_all = "snake_case")]
919pub enum RepeatSource {
920    Replay,
921    Captured,
922    Live,
923}
924#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
925#[serde(deny_unknown_fields)]
926pub struct RepeatRun {
927    pub evaluated: Reference,
928    pub cases: Reference,
929}
930service_document!(ServeEvaluate,EvaluateSchema,"rustev.serve-evaluate/1",SERVICE_LIMITS,{
931    pub split:crate::eval_report::Split,pub replay:ReplayOptions,pub dataset:Reference,pub config:Reference,pub adapter:Reference,pub bundles:Reference,pub candidate_plan:Maybe<Reference>,pub deps:EvaluationDependencies,pub repeat:Maybe<RepeatInputs>,
932});
933impl Validate for ServeEvaluate {}
934
935#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
936#[serde(rename_all = "snake_case")]
937pub enum RouteGroup {
938    Decide,
939    Admin,
940    Ops,
941}
942#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
943pub enum HttpMethod {
944    GET,
945    PUT,
946    POST,
947}
948#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
949#[serde(rename_all = "snake_case")]
950pub enum Operation {
951    Describe,
952    Register,
953    Registration,
954    Decide,
955    Stream,
956    Lookup,
957    Cancel,
958    Replay,
959    Evaluate,
960    Explain,
961    Invalidations,
962    Health,
963    Readiness,
964    Metrics,
965}
966#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
967#[serde(deny_unknown_fields)]
968pub struct OperationDescription {
969    pub operation: Operation,
970    pub method: HttpMethod,
971    pub route: Text<1, 256>,
972    pub group: RouteGroup,
973    pub request_schema: Maybe<Text<1, 128>>,
974    pub response_schemas: List<Text<1, 128>, 0, 4>,
975    pub streaming: bool,
976    pub response_format: ResponseFormat,
977}
978#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
979#[serde(rename_all = "snake_case")]
980pub enum ResponseFormat {
981    Document,
982    SurfaceOutput,
983    Explanation,
984    Empty,
985    Openmetrics,
986}
987/// Single operation source for later deterministic HTTP and gRPC generators.
988pub fn operation_table() -> Vec<OperationDescription> {
989    use HttpMethod::*;
990    use Operation::{
991        Cancel, Describe, Evaluate, Explain, Health, Invalidations, Lookup, Metrics, Readiness,
992        Register, Registration, Replay, Stream,
993    };
994    use ResponseFormat::*;
995    use RouteGroup::*;
996    let rows = [
997        (
998            Describe,
999            GET,
1000            "/v1/service",
1001            Decide,
1002            None,
1003            &["rustev.serve-service/1"][..],
1004            false,
1005            Document,
1006        ),
1007        (
1008            Register,
1009            PUT,
1010            "/v1/plans/{planId}",
1011            Admin,
1012            Some("rustev.serve-register/1"),
1013            &["rustev.plan-registration/1"],
1014            false,
1015            Document,
1016        ),
1017        (
1018            Registration,
1019            GET,
1020            "/v1/plans/{planId}",
1021            Decide,
1022            None,
1023            &["rustev.plan-registration/1"],
1024            false,
1025            Document,
1026        ),
1027        (
1028            Operation::Decide,
1029            POST,
1030            "/v1/plans/{planId}/decisions",
1031            Decide,
1032            Some("rustev.serve-decide/1"),
1033            &["rustev.serve-decision/1"],
1034            false,
1035            Document,
1036        ),
1037        (
1038            Stream,
1039            POST,
1040            "/v1/plans/{planId}/decisions?stream=1",
1041            Decide,
1042            Some("rustev.serve-decide/1"),
1043            &["rustev.serve-event/1"],
1044            true,
1045            Document,
1046        ),
1047        (
1048            Lookup,
1049            GET,
1050            "/v1/decisions/{decisionId}",
1051            Decide,
1052            None,
1053            &["rustev.serve-decision/1", "rustev.serve-decision-state/1"],
1054            false,
1055            Document,
1056        ),
1057        (
1058            Cancel,
1059            POST,
1060            "/v1/decisions/{decisionId}/cancel",
1061            Decide,
1062            None,
1063            &["rustev.serve-cancel/1"],
1064            false,
1065            Document,
1066        ),
1067        (
1068            Replay,
1069            POST,
1070            "/v1/replay",
1071            Decide,
1072            Some("rustev.serve-replay/1"),
1073            &[],
1074            false,
1075            SurfaceOutput,
1076        ),
1077        (
1078            Evaluate,
1079            POST,
1080            "/v1/evaluate",
1081            Decide,
1082            Some("rustev.serve-evaluate/1"),
1083            &[],
1084            false,
1085            SurfaceOutput,
1086        ),
1087        (
1088            Explain,
1089            POST,
1090            "/v1/explain",
1091            Decide,
1092            None,
1093            &["rustev.explanation/1"],
1094            false,
1095            Explanation,
1096        ),
1097        (
1098            Invalidations,
1099            POST,
1100            "/v1/context/invalidations",
1101            Admin,
1102            Some("rustev.serve-invalidations/1"),
1103            &["rustev.serve-invalidations-ack/1"],
1104            false,
1105            Document,
1106        ),
1107        (Health, GET, "/healthz", Ops, None, &[], false, Empty),
1108        (
1109            Readiness,
1110            GET,
1111            "/readyz",
1112            Ops,
1113            None,
1114            &["rustev.serve-readiness/1"],
1115            false,
1116            Document,
1117        ),
1118        (Metrics, GET, "/metrics", Ops, None, &[], false, Openmetrics),
1119    ];
1120    rows.into_iter()
1121        .map(
1122            |(operation, method, route, group, request, response, streaming, response_format)| {
1123                OperationDescription {
1124                    operation,
1125                    method,
1126                    route: Text::new(route).unwrap(),
1127                    group,
1128                    request_schema: request
1129                        .map_or(Maybe::None, |s| Maybe::Some(Text::new(s).unwrap())),
1130                    response_schemas: List::new(
1131                        response.iter().map(|s| Text::new(*s).unwrap()).collect(),
1132                    )
1133                    .unwrap(),
1134                    streaming,
1135                    response_format,
1136                }
1137            },
1138        )
1139        .collect()
1140}
1141service_document!(Serve,ServeSchema,"rustev.serve/1",SERVICE_LIMITS,{pub operations:List<OperationDescription,14,14>,});
1142impl Validate for Serve {
1143    fn validate(&self) -> Result<(), String> {
1144        if self.operations.as_slice() != operation_table() {
1145            Err("operation table differs from rustev.serve/1".into())
1146        } else {
1147            Ok(())
1148        }
1149    }
1150}
1151#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1152#[serde(rename_all = "snake_case")]
1153pub enum Retention {
1154    MemoryLostOnRestart,
1155}
1156service_document!(ServeService,ServiceSchema,"rustev.serve-service/1",SERVICE_LIMITS,{
1157    pub service_build:Handle,pub configuration_digest:ContentDigest,pub protocol:Protocol,pub available_endpoints:List<Operation,0,14>,pub signing_mode:SigningMode,pub development_bearer_listeners:List<Handle,0,64>,pub idempotency_window_ms:Number<60000,86400000>,pub retention:Retention,
1158});
1159impl Validate for ServeService {}
1160#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1161#[serde(deny_unknown_fields)]
1162pub struct ReadinessFailure {
1163    pub item: Handle,
1164    pub reason: ReadinessReason,
1165}
1166#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1167#[serde(rename_all = "snake_case")]
1168pub enum ReadinessReason {
1169    PreloadUnregistered,
1170    DescribeIncomplete,
1171    Degraded,
1172    InvalidationDegraded,
1173    ShuttingDown,
1174}
1175service_document!(ServeReadiness,ReadinessSchema,"rustev.serve-readiness/1",SERVICE_LIMITS,{
1176    pub failing_items:List<ReadinessFailure,0,4096>,pub signing_mode:SigningMode,pub development_bearer_listeners:List<Handle,0,64>,
1177});
1178impl Validate for ServeReadiness {}
1179#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1180#[serde(deny_unknown_fields)]
1181pub struct DecisionIdList {
1182    pub ids: List<Handle, 0, 10000>,
1183    pub total: u64,
1184    pub truncated: bool,
1185}
1186impl DecisionIdList {
1187    fn validate(&self) -> Result<(), String> {
1188        let length = self.ids.as_slice().len() as u64;
1189        if self.total < length || self.truncated != (self.total > length) {
1190            Err("id list total/truncated contradict its length".into())
1191        } else {
1192            Ok(())
1193        }
1194    }
1195}
1196service_document!(ServeShutdown,ShutdownSchema,"rustev.serve-shutdown/1",ParseLimits{max_bytes:8*MIB,..SERVICE_LIMITS},{
1197    pub drained:u64,pub cancelled:u64,pub delivered:u64,pub failed:u64,pub dropped:u64,pub unflushed:u64,pub failed_ids:DecisionIdList,pub unflushed_ids:DecisionIdList,
1198});
1199impl Validate for ServeShutdown {
1200    fn validate(&self) -> Result<(), String> {
1201        self.failed_ids.validate()?;
1202        self.unflushed_ids.validate()?;
1203        if self.failed != self.failed_ids.total || self.unflushed != self.unflushed_ids.total {
1204            Err("shutdown counts must equal id-list totals".into())
1205        } else {
1206            Ok(())
1207        }
1208    }
1209}
1210#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1211#[serde(rename_all = "snake_case")]
1212pub enum InvalidationKind {
1213    Correction,
1214    Revocation,
1215    Erasure,
1216}
1217#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1218#[serde(rename_all = "snake_case", deny_unknown_fields)]
1219pub enum InvalidationScope {
1220    Principal(List<u8, 1, 1024>),
1221    AllScopes,
1222}
1223/// The embedded remote-context item wire shape. The remote lane can reuse
1224/// this payload; it is not a second remote protocol implementation.
1225#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1226#[serde(deny_unknown_fields)]
1227pub struct InvalidationItem {
1228    pub schema: InvalidationItemSchema,
1229    pub source_id: Handle,
1230    pub sequence: u64,
1231    pub kind: InvalidationKind,
1232    pub fields: List<Handle, 1, 4096>,
1233    pub scope: InvalidationScope,
1234}
1235#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1236pub enum InvalidationItemSchema {
1237    #[serde(rename = "rustev.remote-context-invalidation/1")]
1238    V1,
1239}
1240service_document!(ServeInvalidations,InvalidationsSchema,"rustev.serve-invalidations/1",SERVICE_LIMITS,{pub source_id:Handle,pub items:List<InvalidationItem,0,1024>,});
1241impl Validate for ServeInvalidations {
1242    fn validate(&self) -> Result<(), String> {
1243        if self
1244            .items
1245            .as_slice()
1246            .iter()
1247            .any(|i| i.source_id != self.source_id)
1248            || self
1249                .items
1250                .as_slice()
1251                .windows(2)
1252                .any(|p| p[0].sequence >= p[1].sequence)
1253        {
1254            Err("invalidations must name the batch source and increase in sequence".into())
1255        } else {
1256            Ok(())
1257        }
1258    }
1259}
1260service_document!(ServeInvalidationsAck,InvalidationsAckSchema,"rustev.serve-invalidations-ack/1",SERVICE_LIMITS,{pub highest_sequence:u64,});
1261impl Validate for ServeInvalidationsAck {}
1262
1263#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1264#[serde(deny_unknown_fields)]
1265pub struct ListenerLimits {
1266    pub max_header_bytes: Number<4096, 65536>,
1267    pub registration_body_bytes: Number<1, 16777216>,
1268    pub other_body_bytes: Number<1, 4194304>,
1269    pub max_connections: Number<1, 65536>,
1270    pub max_http2_streams: Number<1, 1024>,
1271    pub requests_per_second: Number<1, 10000>,
1272}
1273#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1274#[serde(deny_unknown_fields)]
1275pub struct Tls {
1276    pub certificate_file: Text<1, 4096>,
1277    pub private_key_file: Text<1, 4096>,
1278    pub client_ca_files: List<Text<1, 4096>, 1, 64>,
1279    pub subject_allowlist: List<Text<1, 4096>, 0, 1024>,
1280}
1281#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, JsonSchema)]
1282#[serde(transparent)]
1283pub struct SocketMode(#[schemars(extend("enum" = [384,432]))] u16);
1284impl<'de> Deserialize<'de> for SocketMode {
1285    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1286        match u16::deserialize(d)? {
1287            value @ (0o600 | 0o660) => Ok(Self(value)),
1288            _ => Err(serde::de::Error::custom("socket mode must be 0600 or 0660")),
1289        }
1290    }
1291}
1292#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1293#[serde(rename_all = "snake_case", deny_unknown_fields)]
1294pub enum UnixAuth {
1295    Filesystem,
1296    DevelopmentBearer { token_file: Text<1, 4096> },
1297}
1298#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1299#[serde(rename_all = "snake_case", deny_unknown_fields)]
1300pub enum ListenerTransport {
1301    Tcp {
1302        address: Text<1, 4096>,
1303        tls: Tls,
1304    },
1305    Unix {
1306        path: Text<1, 4096>,
1307        mode: SocketMode,
1308        auth: UnixAuth,
1309    },
1310}
1311#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1312#[serde(deny_unknown_fields)]
1313pub struct Listener {
1314    pub name: Handle,
1315    pub groups: List<RouteGroup, 1, 3>,
1316    pub transport: ListenerTransport,
1317    pub limits: ListenerLimits,
1318    pub redaction_policy: Maybe<Embedded<crate::redaction::RedactionPolicy>>,
1319}
1320#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1321#[serde(deny_unknown_fields)]
1322pub struct RuntimeConfig {
1323    pub max_in_flight: Number<1, 65536>,
1324    pub max_queued: Number<0, 65536>,
1325    #[schemars(extend("maxProperties" = 1024))]
1326    pub per_backend_concurrency: BTreeMap<Handle, Number<1, 65536>>,
1327}
1328#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1329#[serde(rename_all = "snake_case", deny_unknown_fields)]
1330pub enum SinkPolicy {
1331    FailDecision {
1332        timeout_ms: Number<1, 600000>,
1333    },
1334    Backpressure {
1335        max_pending: Number<1, 65536>,
1336        enqueue_timeout_ms: Number<1, 600000>,
1337        delivery_timeout_ms: Number<1, 600000>,
1338    },
1339    DropCounted {
1340        max_pending: Number<1, 65536>,
1341        delivery_timeout_ms: Number<1, 600000>,
1342    },
1343}
1344#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1345pub enum RemoteProtocol {
1346    #[serde(rename = "rustev.remote/1")]
1347    V1,
1348    #[serde(rename = "rustev.remote/2")]
1349    V2,
1350}
1351#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1352#[serde(deny_unknown_fields)]
1353pub struct RemoteEndpoint {
1354    pub address: Text<1, 4096>,
1355    pub credentials_file: Maybe<Text<1, 4096>>,
1356    pub max_in_flight: Number<1, 65536>,
1357    pub queue_capacity: Number<1, 65536>,
1358    pub readiness_affecting: bool,
1359}
1360#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1361#[serde(rename_all = "snake_case", deny_unknown_fields)]
1362pub enum BackendKind {
1363    Compiled {
1364        feature: Handle,
1365    },
1366    Remote {
1367        protocol: RemoteProtocol,
1368        endpoint: RemoteEndpoint,
1369    },
1370}
1371#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1372#[serde(deny_unknown_fields)]
1373pub struct BackendBinding {
1374    pub expected_descriptor_id: DescriptorId,
1375    pub backend: BackendKind,
1376}
1377#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1378#[serde(rename_all = "snake_case", deny_unknown_fields)]
1379pub enum Sink {
1380    Remote(RemoteEndpoint),
1381    Opentelemetry {
1382        endpoint: RemoteEndpoint,
1383        redaction: Embedded<crate::redaction::RedactionPolicy>,
1384    },
1385}
1386#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1387#[serde(deny_unknown_fields)]
1388pub struct SigstoreTrust {
1389    pub trusted_root_file: Text<1, 4096>,
1390    pub certificate_identity: Text<1, 4096>,
1391    pub issuer: Text<1, 4096>,
1392}
1393#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1394#[serde(deny_unknown_fields)]
1395pub struct SigningKeys {
1396    #[schemars(extend("maxProperties" = 1024))]
1397    pub ed25519: BTreeMap<Handle, Text<1, 4096>>,
1398    pub sigstore: List<SigstoreTrust, 0, 1024>,
1399}
1400service_document!(ServeConfig,ConfigSchema,"rustev.serve-config/1",SERVICE_LIMITS,{
1401    pub listeners:List<Listener,1,64>,pub max_plans:Number<1,4096>,pub runtime:RuntimeConfig,
1402    #[schemars(extend("maxProperties" = 1024))] pub backend_bindings:BTreeMap<Handle,BackendBinding>,#[schemars(extend("maxProperties" = 1024))] pub context_sources:BTreeMap<Handle,RemoteEndpoint>,#[schemars(extend("maxProperties" = 1024))] pub sinks:BTreeMap<Handle,Sink>,pub sink_policy:SinkPolicy,
1403    pub signing_mode:SigningMode,pub signing_keys:SigningKeys,pub preload_bundles:List<Text<1,4096>,0,4096>,#[schemars(extend("maxProperties" = 1024))] pub resolver_roots:BTreeMap<Handle,Text<1,4096>>,
1404    pub idempotency_window_ms:Number<60000,86400000>,pub max_retained:Number<1,1000000>,pub max_retained_bytes:Number<16777216,17179869184>,pub stream_queue:Number<16,65536>,pub blocking_threads:Number<1,64>,pub blocking_queue:Number<1,4096>,pub drain_ms:Number<0,600000>,pub flush_ms:Number<0,600000>,
1405    // Extension point: spec 034 (tenant profiles and profile signing mode).
1406    // Extension point: spec 036 (acquisition source configuration).
1407});
1408impl Validate for ServeConfig {
1409    fn validate(&self) -> Result<(), String> {
1410        for len in [
1411            self.backend_bindings.len(),
1412            self.context_sources.len(),
1413            self.sinks.len(),
1414            self.resolver_roots.len(),
1415            self.runtime.per_backend_concurrency.len(),
1416            self.signing_keys.ed25519.len(),
1417        ] {
1418            if len > 1024 {
1419                return Err("configuration map exceeds 1024 entries".into());
1420            }
1421        }
1422        let mut names = std::collections::BTreeSet::new();
1423        for listener in self.listeners.as_slice() {
1424            if !names.insert(listener.name.as_str()) {
1425                return Err("duplicate listener name".into());
1426            }
1427            let groups = listener.groups.as_slice();
1428            for (i, group) in groups.iter().enumerate() {
1429                if groups[..i].contains(group) {
1430                    return Err("duplicate route group".into());
1431                }
1432            }
1433        }
1434        Ok(())
1435    }
1436}
1437impl ServeConfig {
1438    /// Address-free identity. Secret file contents are never document members.
1439    /// Socket paths, endpoint addresses and resolver directory locations are
1440    /// deployment locations, not configuration identity inputs.
1441    pub fn configuration_digest(&self) -> Result<ContentDigest, CanonicalError> {
1442        let mut value = serde_json::to_value(self).map_err(|e| CanonicalError::Serialize {
1443            message: e.to_string(),
1444        })?;
1445        for listener in value["listeners"].as_array_mut().unwrap() {
1446            if let Some(tcp) = listener["transport"].get_mut("tcp") {
1447                tcp.as_object_mut().unwrap().remove("address");
1448            }
1449            if let Some(unix) = listener["transport"].get_mut("unix") {
1450                unix.as_object_mut().unwrap().remove("path");
1451            }
1452        }
1453        fn strip_addresses(value: &mut serde_json::Value) {
1454            match value {
1455                serde_json::Value::Object(map) => {
1456                    map.remove("address");
1457                    for value in map.values_mut() {
1458                        strip_addresses(value);
1459                    }
1460                }
1461                serde_json::Value::Array(values) => values.iter_mut().for_each(strip_addresses),
1462                _ => {}
1463            }
1464        }
1465        for key in ["backend_bindings", "context_sources", "sinks"] {
1466            strip_addresses(&mut value[key]);
1467        }
1468        for root in value["resolver_roots"]
1469            .as_object_mut()
1470            .unwrap()
1471            .values_mut()
1472        {
1473            *root = serde_json::Value::String("configured".into());
1474        }
1475        Ok(ContentDigest::from_digest(tagged_digest(
1476            Self::SCHEMA,
1477            &crate::canonical::canonical_value_bytes(&value)?,
1478        )))
1479    }
1480}