Skip to main content

cookie_agent_protocol/
manifest.rs

1use std::{borrow::Cow, collections::BTreeMap, fmt};
2
3use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
4use serde::{Deserialize, Serialize};
5
6use crate::*;
7
8#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
9pub struct SafeEndpointIdentity(String);
10impl SafeEndpointIdentity {
11    pub const MAX_BYTES: usize = 2048;
12    pub fn new(value: impl Into<String>) -> Result<Self, ManifestSchemaError> {
13        let value = value.into();
14        if value.is_empty()
15            || value.len() > Self::MAX_BYTES
16            || value.chars().any(char::is_control)
17            || value.contains('@')
18            || value.contains('?')
19            || value.contains('#')
20        {
21            Err(ManifestSchemaError::InvalidEndpoint)
22        } else {
23            Ok(Self(value))
24        }
25    }
26    #[must_use]
27    pub fn as_str(&self) -> &str {
28        &self.0
29    }
30}
31impl Serialize for SafeEndpointIdentity {
32    fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
33    where
34        S: serde::Serializer,
35    {
36        s.serialize_str(&self.0)
37    }
38}
39impl<'de> Deserialize<'de> for SafeEndpointIdentity {
40    fn deserialize<D>(d: D) -> Result<Self, D::Error>
41    where
42        D: serde::Deserializer<'de>,
43    {
44        Self::new(String::deserialize(d)?).map_err(serde::de::Error::custom)
45    }
46}
47impl JsonSchema for SafeEndpointIdentity {
48    fn inline_schema() -> bool {
49        true
50    }
51    fn schema_name() -> Cow<'static, str> {
52        Cow::Borrowed("SafeEndpointIdentity")
53    }
54    fn json_schema(_: &mut SchemaGenerator) -> Schema {
55        json_schema!({"type":"string","minLength":1,"maxLength":2048})
56    }
57}
58
59#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
60pub struct HeaderName(String);
61impl HeaderName {
62    pub fn new(value: impl Into<String>) -> Result<Self, ManifestSchemaError> {
63        let value = value.into().to_ascii_lowercase();
64        if value.is_empty()
65            || value.len() > 128
66            || !value
67                .bytes()
68                .all(|b| b.is_ascii_alphanumeric() || b"!#$%&'*+-.^_`|~".contains(&b))
69        {
70            Err(ManifestSchemaError::InvalidHeader)
71        } else {
72            Ok(Self(value))
73        }
74    }
75    #[must_use]
76    pub fn as_str(&self) -> &str {
77        &self.0
78    }
79}
80impl Serialize for HeaderName {
81    fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
82    where
83        S: serde::Serializer,
84    {
85        s.serialize_str(&self.0)
86    }
87}
88impl<'de> Deserialize<'de> for HeaderName {
89    fn deserialize<D>(d: D) -> Result<Self, D::Error>
90    where
91        D: serde::Deserializer<'de>,
92    {
93        Self::new(String::deserialize(d)?).map_err(serde::de::Error::custom)
94    }
95}
96impl JsonSchema for HeaderName {
97    fn inline_schema() -> bool {
98        true
99    }
100    fn schema_name() -> Cow<'static, str> {
101        Cow::Borrowed("HeaderName")
102    }
103    fn json_schema(_: &mut SchemaGenerator) -> Schema {
104        json_schema!({"type":"string","minLength":1,"maxLength":128,"pattern":"^[!#$%&'*+.^_`|~0-9A-Za-z-]+$"})
105    }
106}
107
108#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
109pub struct SafeStaticHeaderValue(String);
110impl SafeStaticHeaderValue {
111    pub fn new(value: impl Into<String>) -> Result<Self, ManifestSchemaError> {
112        let value = value.into();
113        if value.len() > 8192 || value.chars().any(char::is_control) {
114            Err(ManifestSchemaError::InvalidHeader)
115        } else {
116            Ok(Self(value))
117        }
118    }
119    #[must_use]
120    pub fn as_str(&self) -> &str {
121        &self.0
122    }
123}
124impl Serialize for SafeStaticHeaderValue {
125    fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
126    where
127        S: serde::Serializer,
128    {
129        s.serialize_str(&self.0)
130    }
131}
132impl<'de> Deserialize<'de> for SafeStaticHeaderValue {
133    fn deserialize<D>(d: D) -> Result<Self, D::Error>
134    where
135        D: serde::Deserializer<'de>,
136    {
137        Self::new(String::deserialize(d)?).map_err(serde::de::Error::custom)
138    }
139}
140impl JsonSchema for SafeStaticHeaderValue {
141    fn inline_schema() -> bool {
142        true
143    }
144    fn schema_name() -> Cow<'static, str> {
145        Cow::Borrowed("SafeStaticHeaderValue")
146    }
147    fn json_schema(_: &mut SchemaGenerator) -> Schema {
148        json_schema!({"type":"string","maxLength":8192})
149    }
150}
151
152#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
153#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
154pub enum FrozenProviderSource {
155    Managed {
156        provider_recipe: ProviderRecipeId,
157        source_record_digest: Sha256Digest,
158        recipe_fingerprint: Sha256Digest,
159        package_claim: String,
160    },
161    Custom {
162        safe_definition_fingerprint: Sha256Digest,
163    },
164}
165
166#[derive(Clone, Copy, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
167#[serde(rename_all = "snake_case")]
168pub enum FrozenCredentialSource {
169    AuthoredApiKey,
170    AuthoredOverride,
171    ProviderStore,
172    NoAuth,
173}
174
175#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
176#[serde(deny_unknown_fields)]
177pub struct FrozenSetupBinding {
178    pub setup_recipe: ProviderSetupRecipeId,
179    pub values: BTreeMap<SetupFieldId, SafeSetupValue>,
180    pub setup_fingerprint: Sha256Digest,
181}
182
183#[derive(Clone, Debug, JsonSchema, PartialEq, Serialize)]
184#[serde(deny_unknown_fields)]
185pub struct FrozenCredentialBinding {
186    pub source: FrozenCredentialSource,
187    pub auth_method: AuthMethodId,
188    #[schemars(length(max = 32))]
189    pub fields: Vec<AuthFieldName>,
190    pub parameters: BTreeMap<AuthParameterId, FrozenAuthParameterValue>,
191    pub owned_headers: Vec<HeaderName>,
192}
193impl<'de> Deserialize<'de> for FrozenCredentialBinding {
194    fn deserialize<D>(d: D) -> Result<Self, D::Error>
195    where
196        D: serde::Deserializer<'de>,
197    {
198        #[derive(Deserialize)]
199        #[serde(deny_unknown_fields)]
200        struct Wire {
201            source: FrozenCredentialSource,
202            auth_method: AuthMethodId,
203            fields: Vec<AuthFieldName>,
204            parameters: BTreeMap<AuthParameterId, FrozenAuthParameterValue>,
205            owned_headers: Vec<HeaderName>,
206        }
207        let w = Wire::deserialize(d)?;
208        if w.fields.len() > 32
209            || w.fields.windows(2).any(|p| p[0] >= p[1])
210            || w.parameters.len() > 32
211            || w.owned_headers.len() > 32
212            || w.owned_headers.windows(2).any(|p| p[0] >= p[1])
213        {
214            return Err(serde::de::Error::custom(
215                "credential shape must be bounded, sorted, and unique",
216            ));
217        }
218        Ok(Self {
219            source: w.source,
220            auth_method: w.auth_method,
221            fields: w.fields,
222            parameters: w.parameters,
223            owned_headers: w.owned_headers,
224        })
225    }
226}
227
228pub type FrozenProviderOptions = ProviderOptions;
229
230#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
231pub struct FrozenAuthParameterValue(String);
232
233impl FrozenAuthParameterValue {
234    pub fn new(value: impl Into<String>) -> Result<Self, ManifestSchemaError> {
235        let value = value.into();
236        if value.is_empty()
237            || value.len() > 512
238            || value.chars().any(char::is_control)
239            || value.contains("${env:")
240        {
241            Err(ManifestSchemaError::InvalidAuthParameter)
242        } else {
243            Ok(Self(value))
244        }
245    }
246
247    #[must_use]
248    pub fn as_str(&self) -> &str {
249        &self.0
250    }
251}
252
253impl Serialize for FrozenAuthParameterValue {
254    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
255    where
256        S: serde::Serializer,
257    {
258        serializer.serialize_str(&self.0)
259    }
260}
261
262impl<'de> Deserialize<'de> for FrozenAuthParameterValue {
263    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
264    where
265        D: serde::Deserializer<'de>,
266    {
267        Self::new(String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
268    }
269}
270
271impl JsonSchema for FrozenAuthParameterValue {
272    fn inline_schema() -> bool {
273        true
274    }
275    fn schema_name() -> Cow<'static, str> {
276        Cow::Borrowed("FrozenAuthParameterValue")
277    }
278    fn json_schema(_: &mut SchemaGenerator) -> Schema {
279        json_schema!({"type":"string","minLength":1,"maxLength":512})
280    }
281}
282
283#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
284pub struct NormalizedDecimal(String);
285
286impl NormalizedDecimal {
287    pub fn from_f32(value: f32) -> Result<Self, ManifestSchemaError> {
288        if !value.is_finite() {
289            return Err(ManifestSchemaError::InvalidDecimal);
290        }
291        Ok(Self(value.to_string()))
292    }
293
294    pub fn parse(value: impl Into<String>) -> Result<Self, ManifestSchemaError> {
295        let value = value.into();
296        let parsed = value
297            .parse::<f32>()
298            .map_err(|_| ManifestSchemaError::InvalidDecimal)?;
299        if !parsed.is_finite() || parsed.to_string() != value {
300            return Err(ManifestSchemaError::InvalidDecimal);
301        }
302        Ok(Self(value))
303    }
304
305    #[must_use]
306    pub fn as_str(&self) -> &str {
307        &self.0
308    }
309
310    #[must_use]
311    pub fn get(&self) -> f32 {
312        self.0
313            .parse()
314            .expect("normalized decimal was validated at construction")
315    }
316}
317
318impl Serialize for NormalizedDecimal {
319    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
320    where
321        S: serde::Serializer,
322    {
323        serializer.serialize_str(&self.0)
324    }
325}
326
327impl<'de> Deserialize<'de> for NormalizedDecimal {
328    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
329    where
330        D: serde::Deserializer<'de>,
331    {
332        Self::parse(String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
333    }
334}
335
336impl JsonSchema for NormalizedDecimal {
337    fn inline_schema() -> bool {
338        true
339    }
340    fn schema_name() -> Cow<'static, str> {
341        Cow::Borrowed("NormalizedDecimal")
342    }
343    fn json_schema(_: &mut SchemaGenerator) -> Schema {
344        json_schema!({"type":"string","minLength":1,"maxLength":32})
345    }
346}
347
348#[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
349#[serde(deny_unknown_fields)]
350pub struct FrozenRequestDefaults {
351    pub temperature: Option<NormalizedDecimal>,
352    pub top_p: Option<NormalizedDecimal>,
353    pub max_output_tokens: Option<u64>,
354    pub stop: Vec<String>,
355    pub seed: Option<i64>,
356    pub tool_choice: Option<ToolChoice>,
357}
358
359impl FrozenRequestDefaults {
360    pub fn validate(&self) -> Result<(), ManifestSchemaError> {
361        if self
362            .temperature
363            .as_ref()
364            .is_some_and(|value| !(0.0..=2.0).contains(&value.get()))
365            || self
366                .top_p
367                .as_ref()
368                .is_some_and(|value| !(0.0..=1.0).contains(&value.get()))
369            || self.max_output_tokens == Some(0)
370            || self.stop.len() > 8
371            || self
372                .stop
373                .iter()
374                .any(|value| value.is_empty() || value.len() > 256)
375        {
376            Err(ManifestSchemaError::InvalidDefaults)
377        } else {
378            Ok(())
379        }
380    }
381}
382
383#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
384#[serde(deny_unknown_fields)]
385pub struct FrozenResolvedRequestDefaults {
386    pub request: FrozenRequestDefaults,
387    pub reasoning: Option<CompiledReasoningBehavior>,
388}
389
390impl FrozenResolvedRequestDefaults {
391    pub fn validate(&self) -> Result<(), ManifestSchemaError> {
392        self.request.validate()?;
393        if matches!(
394            self.reasoning,
395            Some(CompiledReasoningBehavior::BudgetTokens { value }) if value < -1
396        ) {
397            Err(ManifestSchemaError::InvalidDefaults)
398        } else {
399            Ok(())
400        }
401    }
402}
403
404#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
405#[serde(deny_unknown_fields)]
406pub struct FrozenVariantBlueprint {
407    pub id: VariantId,
408    #[schemars(with = "crate::LanguageModelDescriptorSchema")]
409    pub descriptor: oven_sdk::LanguageModelDescriptor,
410    pub defaults: FrozenResolvedRequestDefaults,
411    pub options: FrozenProviderOptions,
412    #[serde(default)]
413    pub static_headers: BTreeMap<HeaderName, SafeStaticHeaderValue>,
414    pub behavior_fingerprint: Sha256Digest,
415    pub selection_fingerprint: Sha256Digest,
416}
417
418#[derive(Clone, Debug, JsonSchema, PartialEq, Serialize)]
419#[serde(deny_unknown_fields)]
420pub struct CompiledSafeModelBlueprint {
421    pub blueprint_fingerprint: Sha256Digest,
422    pub selection: ModelSelection,
423    pub source: FrozenProviderSource,
424    pub config_override_fingerprint: Sha256Digest,
425    pub setup_binding: FrozenSetupBinding,
426    pub credential_binding: FrozenCredentialBinding,
427    pub endpoint_identity: SafeEndpointIdentity,
428    pub provider_recipe: ProviderRecipeId,
429    pub protocol_recipe: ProtocolRecipeId,
430    pub setup_recipe: ProviderSetupRecipeId,
431    pub auth_method: AuthMethodId,
432    pub compiler_version: RecipeCompilerVersion,
433    #[schemars(with = "crate::LanguageModelDescriptorSchema")]
434    pub descriptor: oven_sdk::LanguageModelDescriptor,
435    pub defaults: FrozenResolvedRequestDefaults,
436    pub options: FrozenProviderOptions,
437    pub static_headers: BTreeMap<HeaderName, SafeStaticHeaderValue>,
438    #[schemars(length(max = 256))]
439    pub variants: Vec<FrozenVariantBlueprint>,
440    pub behavior_fingerprint: Sha256Digest,
441    pub selection_fingerprint: Sha256Digest,
442}
443impl<'de> Deserialize<'de> for CompiledSafeModelBlueprint {
444    fn deserialize<D>(d: D) -> Result<Self, D::Error>
445    where
446        D: serde::Deserializer<'de>,
447    {
448        #[derive(Deserialize)]
449        #[serde(deny_unknown_fields)]
450        struct Wire {
451            blueprint_fingerprint: Sha256Digest,
452            selection: ModelSelection,
453            source: FrozenProviderSource,
454            config_override_fingerprint: Sha256Digest,
455            setup_binding: FrozenSetupBinding,
456            credential_binding: FrozenCredentialBinding,
457            endpoint_identity: SafeEndpointIdentity,
458            provider_recipe: ProviderRecipeId,
459            protocol_recipe: ProtocolRecipeId,
460            setup_recipe: ProviderSetupRecipeId,
461            auth_method: AuthMethodId,
462            compiler_version: RecipeCompilerVersion,
463            descriptor: oven_sdk::LanguageModelDescriptor,
464            defaults: FrozenResolvedRequestDefaults,
465            options: FrozenProviderOptions,
466            static_headers: BTreeMap<HeaderName, SafeStaticHeaderValue>,
467            variants: Vec<FrozenVariantBlueprint>,
468            behavior_fingerprint: Sha256Digest,
469            selection_fingerprint: Sha256Digest,
470        }
471        let w = Wire::deserialize(d)?;
472        if w.selection.variant.is_some()
473            || w.defaults.validate().is_err()
474            || w.variants.len() > 256
475            || w.variants.windows(2).any(|p| p[0].id >= p[1].id)
476            || w.variants.iter().any(|variant| {
477                variant.defaults.validate().is_err()
478                    || variant.descriptor.identity != w.descriptor.identity
479            })
480        {
481            return Err(serde::de::Error::custom(
482                "model blueprint behavior is invalid",
483            ));
484        }
485        Ok(Self {
486            blueprint_fingerprint: w.blueprint_fingerprint,
487            selection: w.selection,
488            source: w.source,
489            config_override_fingerprint: w.config_override_fingerprint,
490            setup_binding: w.setup_binding,
491            credential_binding: w.credential_binding,
492            endpoint_identity: w.endpoint_identity,
493            provider_recipe: w.provider_recipe,
494            protocol_recipe: w.protocol_recipe,
495            setup_recipe: w.setup_recipe,
496            auth_method: w.auth_method,
497            compiler_version: w.compiler_version,
498            descriptor: w.descriptor,
499            defaults: w.defaults,
500            options: w.options,
501            static_headers: w.static_headers,
502            variants: w.variants,
503            behavior_fingerprint: w.behavior_fingerprint,
504            selection_fingerprint: w.selection_fingerprint,
505        })
506    }
507}
508
509#[derive(Clone, Debug, JsonSchema, PartialEq, Serialize)]
510#[serde(deny_unknown_fields)]
511pub struct ModelSnapshotPayloadV1 {
512    pub catalog_revision: CatalogRevision,
513    pub recipe_registry_revision: RecipeRegistryRevision,
514    pub provider_state_revision: ProviderStateRevision,
515    pub model_revision: ModelRevision,
516    #[schemars(length(max = 4096))]
517    pub blueprints: Vec<CompiledSafeModelBlueprint>,
518}
519impl<'de> Deserialize<'de> for ModelSnapshotPayloadV1 {
520    fn deserialize<D>(d: D) -> Result<Self, D::Error>
521    where
522        D: serde::Deserializer<'de>,
523    {
524        #[derive(Deserialize)]
525        #[serde(deny_unknown_fields)]
526        struct Wire {
527            catalog_revision: CatalogRevision,
528            recipe_registry_revision: RecipeRegistryRevision,
529            provider_state_revision: ProviderStateRevision,
530            model_revision: ModelRevision,
531            blueprints: Vec<CompiledSafeModelBlueprint>,
532        }
533        let w = Wire::deserialize(d)?;
534        if w.blueprints.len() > 4096
535            || w.blueprints
536                .windows(2)
537                .any(|p| p[0].selection.model >= p[1].selection.model)
538        {
539            return Err(serde::de::Error::custom(
540                "blueprints must be strictly sorted by model key",
541            ));
542        }
543        Ok(Self {
544            catalog_revision: w.catalog_revision,
545            recipe_registry_revision: w.recipe_registry_revision,
546            provider_state_revision: w.provider_state_revision,
547            model_revision: w.model_revision,
548            blueprints: w.blueprints,
549        })
550    }
551}
552
553#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
554pub struct ModelSnapshotManifestSchemaVersion(());
555impl ModelSnapshotManifestSchemaVersion {
556    #[must_use]
557    pub const fn current() -> Self {
558        Self(())
559    }
560}
561impl Serialize for ModelSnapshotManifestSchemaVersion {
562    fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
563    where
564        S: serde::Serializer,
565    {
566        s.serialize_u32(1)
567    }
568}
569impl<'de> Deserialize<'de> for ModelSnapshotManifestSchemaVersion {
570    fn deserialize<D>(d: D) -> Result<Self, D::Error>
571    where
572        D: serde::Deserializer<'de>,
573    {
574        let value = u32::deserialize(d)?;
575        if value == 1 {
576            Ok(Self::current())
577        } else {
578            Err(serde::de::Error::custom(
579                "unsupported model-snapshot manifest schema; expected 1",
580            ))
581        }
582    }
583}
584impl JsonSchema for ModelSnapshotManifestSchemaVersion {
585    fn inline_schema() -> bool {
586        true
587    }
588    fn schema_name() -> Cow<'static, str> {
589        Cow::Borrowed("ModelSnapshotManifestSchemaVersion")
590    }
591    fn json_schema(_: &mut SchemaGenerator) -> Schema {
592        json_schema!({"type":"integer","const":1})
593    }
594}
595
596#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
597#[serde(deny_unknown_fields)]
598pub struct ModelSnapshotManifestV1 {
599    pub schema_version: ModelSnapshotManifestSchemaVersion,
600    pub revision: ModelSnapshotRevision,
601    pub payload: ModelSnapshotPayloadV1,
602}
603
604#[derive(Clone, Copy, Debug, Eq, PartialEq)]
605pub enum ManifestSchemaError {
606    InvalidEndpoint,
607    InvalidHeader,
608    InvalidAuthParameter,
609    InvalidDecimal,
610    InvalidDefaults,
611}
612impl fmt::Display for ManifestSchemaError {
613    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
614        f.write_str(match self {
615            Self::InvalidEndpoint => "invalid safe endpoint identity",
616            Self::InvalidHeader => "invalid safe static header",
617            Self::InvalidAuthParameter => "invalid frozen auth parameter",
618            Self::InvalidDecimal => "invalid normalized decimal",
619            Self::InvalidDefaults => "invalid frozen request defaults",
620        })
621    }
622}
623impl std::error::Error for ManifestSchemaError {}