Skip to main content

cookie_agent_protocol/
agent.rs

1use std::{borrow::Cow, collections::BTreeSet, fmt};
2
3use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
4use serde::{Deserialize, Serialize};
5
6use crate::{AgentId, FrozenModelBinding, ModelKey, ModelSelection, Sha256Digest, WildcardPattern};
7
8#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
9pub struct AgentSchemaVersion(());
10impl AgentSchemaVersion {
11    #[must_use]
12    pub const fn current() -> Self {
13        Self(())
14    }
15    #[must_use]
16    pub const fn value(self) -> u32 {
17        7
18    }
19}
20impl Serialize for AgentSchemaVersion {
21    fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
22    where
23        S: serde::Serializer,
24    {
25        s.serialize_u32(7)
26    }
27}
28impl<'de> Deserialize<'de> for AgentSchemaVersion {
29    fn deserialize<D>(d: D) -> Result<Self, D::Error>
30    where
31        D: serde::Deserializer<'de>,
32    {
33        let v = u32::deserialize(d)?;
34        if v == 7 {
35            Ok(Self::current())
36        } else {
37            Err(serde::de::Error::custom(format!(
38                "unsupported exact agent schema {v}; expected 7"
39            )))
40        }
41    }
42}
43impl JsonSchema for AgentSchemaVersion {
44    fn inline_schema() -> bool {
45        true
46    }
47    fn schema_name() -> Cow<'static, str> {
48        Cow::Borrowed("AgentSchemaVersion")
49    }
50    fn json_schema(_: &mut SchemaGenerator) -> Schema {
51        json_schema!({"type":"integer","const":7})
52    }
53}
54
55#[derive(Clone, Copy, Debug, Eq, PartialEq)]
56struct PreviousAgentSchemaVersion;
57
58#[derive(Clone, Copy, Debug, Eq, PartialEq)]
59struct OlderAgentSchemaVersion;
60
61impl<'de> Deserialize<'de> for PreviousAgentSchemaVersion {
62    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
63    where
64        D: serde::Deserializer<'de>,
65    {
66        let value = u32::deserialize(deserializer)?;
67        if value == 6 {
68            Ok(Self)
69        } else {
70            Err(serde::de::Error::custom(format!(
71                "unsupported previous agent schema {value}; expected 6"
72            )))
73        }
74    }
75}
76
77impl<'de> Deserialize<'de> for OlderAgentSchemaVersion {
78    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
79    where
80        D: serde::Deserializer<'de>,
81    {
82        let value = u32::deserialize(deserializer)?;
83        if value == 5 {
84            Ok(Self)
85        } else {
86            Err(serde::de::Error::custom(format!(
87                "unsupported older agent schema {value}; expected 5"
88            )))
89        }
90    }
91}
92
93#[derive(
94    Clone, Copy, Debug, Deserialize, Eq, Hash, JsonSchema, Ord, PartialEq, PartialOrd, Serialize,
95)]
96#[serde(rename_all = "snake_case")]
97pub enum AgentMode {
98    Primary,
99    Subagent,
100    All,
101    Internal,
102}
103
104#[derive(
105    Clone, Copy, Debug, Deserialize, Eq, Hash, JsonSchema, Ord, PartialEq, PartialOrd, Serialize,
106)]
107#[serde(rename_all = "snake_case")]
108pub enum PermissionAction {
109    Read,
110    Write,
111    Bash,
112    Delegate,
113    Message,
114    Mcp,
115    Plugin,
116    Skill,
117    Webfetch,
118}
119
120#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, JsonSchema, PartialEq, Serialize)]
121#[serde(rename_all = "snake_case")]
122pub enum PermissionEffect {
123    Allow,
124    Ask,
125    Deny,
126}
127
128#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
129#[serde(deny_unknown_fields)]
130pub struct PermissionRule {
131    pub action: PermissionAction,
132    pub resource: WildcardPattern,
133    pub effect: PermissionEffect,
134}
135
136#[derive(Clone, Debug, Eq, JsonSchema, PartialEq, Serialize)]
137#[serde(deny_unknown_fields)]
138pub struct SessionPermissionOverlay {
139    #[schemars(length(max = 256))]
140    pub rules: Vec<PermissionRule>,
141}
142
143impl SessionPermissionOverlay {
144    #[must_use]
145    pub const fn empty() -> Self {
146        Self { rules: Vec::new() }
147    }
148
149    pub fn validate(&self) -> Result<(), AgentSchemaError> {
150        if self.rules.len() > 256 {
151            return Err(AgentSchemaError::InvalidListBounds);
152        }
153        let mut keys = BTreeSet::new();
154        if !self
155            .rules
156            .iter()
157            .all(|rule| keys.insert((rule.action, rule.resource.as_str().to_owned())))
158        {
159            return Err(AgentSchemaError::DuplicatePermissionRule);
160        }
161        Ok(())
162    }
163}
164
165impl Default for SessionPermissionOverlay {
166    fn default() -> Self {
167        Self::empty()
168    }
169}
170
171impl<'de> Deserialize<'de> for SessionPermissionOverlay {
172    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
173    where
174        D: serde::Deserializer<'de>,
175    {
176        #[derive(Deserialize)]
177        #[serde(deny_unknown_fields)]
178        struct Wire {
179            rules: Vec<PermissionRule>,
180        }
181        let value = Self {
182            rules: Wire::deserialize(deserializer)?.rules,
183        };
184        value.validate().map_err(serde::de::Error::custom)?;
185        Ok(value)
186    }
187}
188
189#[derive(Clone, Copy, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
190#[serde(rename_all = "snake_case")]
191pub enum AgentDocumentSource {
192    BuiltIn,
193    User,
194    Workspace,
195}
196
197#[derive(Clone, Debug, Eq, JsonSchema, PartialEq, Serialize)]
198#[serde(deny_unknown_fields)]
199pub struct FrozenDelegationPolicy {
200    #[schemars(length(min = 1, max = 256))]
201    pub targets: Vec<AgentId>,
202    pub effective_depth_ceiling: u32,
203}
204impl FrozenDelegationPolicy {
205    pub fn validate(&self) -> Result<(), AgentSchemaError> {
206        if self.targets.is_empty() || self.targets.len() > 256 {
207            return Err(AgentSchemaError::InvalidDelegationTargets);
208        }
209        if self.targets.windows(2).any(|pair| pair[0] >= pair[1]) {
210            return Err(AgentSchemaError::InvalidDelegationTargets);
211        }
212        Ok(())
213    }
214}
215impl<'de> Deserialize<'de> for FrozenDelegationPolicy {
216    fn deserialize<D>(d: D) -> Result<Self, D::Error>
217    where
218        D: serde::Deserializer<'de>,
219    {
220        #[derive(Deserialize)]
221        #[serde(deny_unknown_fields)]
222        struct Wire {
223            targets: Vec<AgentId>,
224            effective_depth_ceiling: u32,
225        }
226        let w = Wire::deserialize(d)?;
227        let value = Self {
228            targets: w.targets,
229            effective_depth_ceiling: w.effective_depth_ceiling,
230        };
231        value.validate().map_err(serde::de::Error::custom)?;
232        Ok(value)
233    }
234}
235
236#[derive(Clone, Debug, JsonSchema, PartialEq, Serialize)]
237#[serde(deny_unknown_fields)]
238pub struct AgentSnapshot {
239    pub agent: AgentId,
240    pub schema: AgentSchemaVersion,
241    pub mode: AgentMode,
242    #[schemars(with = "AgentDescriptionSchema")]
243    pub description: String,
244    pub document_source: AgentDocumentSource,
245    pub document_fingerprint: Sha256Digest,
246    #[schemars(with = "AgentPromptSchema")]
247    pub composed_prompt: String,
248    pub prompt_fingerprint: Sha256Digest,
249    pub max_output_tokens: u64,
250    #[schemars(length(max = 256))]
251    pub permissions: Vec<PermissionRule>,
252    #[serde(deserialize_with = "crate::deserialize_required_option")]
253    #[schemars(with = "crate::NullableSchema<FrozenDelegationPolicy>", required)]
254    pub delegation: Option<FrozenDelegationPolicy>,
255    #[schemars(length(min = 1, max = 256))]
256    pub fallback_chain: Vec<FrozenModelBinding>,
257    pub selected_suffix_start: u32,
258}
259
260impl AgentSnapshot {
261    pub const MAX_PROMPT_BYTES: usize = 128 * 1024;
262    pub fn validate(&self) -> Result<(), AgentSchemaError> {
263        if self.description.trim().is_empty()
264            || self.description.len() > 512
265            || self.description.chars().any(char::is_control)
266        {
267            return Err(AgentSchemaError::InvalidDescription);
268        }
269        if self.composed_prompt.trim().is_empty()
270            || self.composed_prompt.len() > Self::MAX_PROMPT_BYTES
271        {
272            return Err(AgentSchemaError::InvalidPrompt);
273        }
274        if self.permissions.len() > 256
275            || self.fallback_chain.is_empty()
276            || self.fallback_chain.len() > 256
277        {
278            return Err(AgentSchemaError::InvalidListBounds);
279        }
280        let mut models = BTreeSet::<ModelKey>::new();
281        if !self
282            .fallback_chain
283            .iter()
284            .all(|binding| models.insert(binding.selection.model.clone()))
285        {
286            return Err(AgentSchemaError::DuplicateFallbackModel);
287        }
288        for binding in &self.fallback_chain {
289            binding
290                .validate()
291                .map_err(|_| AgentSchemaError::InvalidModelBinding)?;
292        }
293        if self.selected_suffix_start as usize >= self.fallback_chain.len() {
294            return Err(AgentSchemaError::InvalidSuffixStart);
295        }
296        if let Some(delegation) = &self.delegation {
297            delegation.validate()?;
298        }
299        Ok(())
300    }
301
302    pub fn validate_selection(&self, selection: &RunSelection) -> Result<(), AgentSchemaError> {
303        self.validate()?;
304        let start = self.selected_suffix_start as usize;
305        if selection.agent != self.agent
306            || self.fallback_chain[start].selection.model != selection.model.model
307        {
308            return Err(AgentSchemaError::SelectionMismatch);
309        }
310        Ok(())
311    }
312
313    pub fn validate_selected_suffix(
314        &self,
315        selection: &RunSelection,
316        selected_suffix: &[FrozenModelBinding],
317    ) -> Result<(), AgentSchemaError> {
318        self.validate_selection(selection)?;
319        let start = self.selected_suffix_start as usize;
320        let expected = &self.fallback_chain[start..];
321        if selected_suffix.len() != expected.len() || selected_suffix.is_empty() {
322            return Err(AgentSchemaError::SelectedSuffixMismatch);
323        }
324        let selected_head = &selected_suffix[0];
325        let fallback_head = &expected[0];
326        if selected_head.selection != selection.model
327            || selected_head.descriptor != fallback_head.descriptor
328        {
329            return Err(AgentSchemaError::SelectedSuffixMismatch);
330        }
331        if selection.model.variant == fallback_head.selection.variant
332            && selected_head != fallback_head
333        {
334            return Err(AgentSchemaError::SelectedSuffixMismatch);
335        }
336        if selected_suffix[1..] != expected[1..] {
337            return Err(AgentSchemaError::SelectedSuffixMismatch);
338        }
339        Ok(())
340    }
341}
342impl<'de> Deserialize<'de> for AgentSnapshot {
343    fn deserialize<D>(d: D) -> Result<Self, D::Error>
344    where
345        D: serde::Deserializer<'de>,
346    {
347        #[derive(Deserialize)]
348        #[serde(deny_unknown_fields)]
349        struct CurrentWire {
350            agent: AgentId,
351            schema: AgentSchemaVersion,
352            mode: AgentMode,
353            description: String,
354            document_source: AgentDocumentSource,
355            document_fingerprint: Sha256Digest,
356            composed_prompt: String,
357            prompt_fingerprint: Sha256Digest,
358            max_output_tokens: u64,
359            permissions: Vec<PermissionRule>,
360            #[serde(deserialize_with = "crate::deserialize_required_option")]
361            delegation: Option<FrozenDelegationPolicy>,
362            fallback_chain: Vec<FrozenModelBinding>,
363            selected_suffix_start: u32,
364        }
365
366        #[derive(Deserialize)]
367        #[serde(deny_unknown_fields)]
368        struct PreviousWire {
369            agent: AgentId,
370            #[serde(rename = "schema")]
371            _schema: PreviousAgentSchemaVersion,
372            mode: AgentMode,
373            description: String,
374            document_source: AgentDocumentSource,
375            document_fingerprint: Sha256Digest,
376            composed_prompt: String,
377            prompt_fingerprint: Sha256Digest,
378            max_output_tokens: u64,
379            permissions: Vec<PermissionRule>,
380            #[serde(deserialize_with = "crate::deserialize_required_option")]
381            delegation: Option<FrozenDelegationPolicy>,
382            fallback_chain: Vec<FrozenModelBinding>,
383            selected_suffix_start: u32,
384        }
385
386        #[derive(Deserialize)]
387        #[serde(deny_unknown_fields)]
388        struct OlderWire {
389            agent: AgentId,
390            #[serde(rename = "schema")]
391            _schema: OlderAgentSchemaVersion,
392            mode: AgentMode,
393            description: String,
394            document_source: AgentDocumentSource,
395            document_fingerprint: Sha256Digest,
396            composed_prompt: String,
397            prompt_fingerprint: Sha256Digest,
398            permissions: Vec<PermissionRule>,
399            #[serde(deserialize_with = "crate::deserialize_required_option")]
400            delegation: Option<FrozenDelegationPolicy>,
401            fallback_chain: Vec<FrozenModelBinding>,
402            selected_suffix_start: u32,
403        }
404
405        #[derive(Deserialize)]
406        #[serde(untagged)]
407        enum Wire {
408            Current(CurrentWire),
409            Previous(PreviousWire),
410            Older(OlderWire),
411        }
412
413        let value = match Wire::deserialize(d)? {
414            Wire::Current(w) => Self {
415                agent: w.agent,
416                schema: w.schema,
417                mode: w.mode,
418                description: w.description,
419                document_source: w.document_source,
420                document_fingerprint: w.document_fingerprint,
421                composed_prompt: w.composed_prompt,
422                prompt_fingerprint: w.prompt_fingerprint,
423                max_output_tokens: w.max_output_tokens,
424                permissions: w.permissions,
425                delegation: w.delegation,
426                fallback_chain: w.fallback_chain,
427                selected_suffix_start: w.selected_suffix_start,
428            },
429            Wire::Previous(w) => Self {
430                agent: w.agent,
431                schema: AgentSchemaVersion::current(),
432                mode: w.mode,
433                description: w.description,
434                document_source: w.document_source,
435                document_fingerprint: w.document_fingerprint,
436                composed_prompt: w.composed_prompt,
437                prompt_fingerprint: w.prompt_fingerprint,
438                max_output_tokens: w.max_output_tokens,
439                permissions: w.permissions,
440                delegation: w.delegation,
441                fallback_chain: w.fallback_chain,
442                selected_suffix_start: w.selected_suffix_start,
443            },
444            Wire::Older(w) => Self {
445                agent: w.agent,
446                schema: AgentSchemaVersion::current(),
447                mode: w.mode,
448                description: w.description,
449                document_source: w.document_source,
450                document_fingerprint: w.document_fingerprint,
451                composed_prompt: w.composed_prompt,
452                prompt_fingerprint: w.prompt_fingerprint,
453                max_output_tokens: 0,
454                permissions: w.permissions,
455                delegation: w.delegation,
456                fallback_chain: w.fallback_chain,
457                selected_suffix_start: w.selected_suffix_start,
458            },
459        };
460        value.validate().map_err(serde::de::Error::custom)?;
461        Ok(value)
462    }
463}
464
465#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
466#[serde(deny_unknown_fields)]
467pub struct RunSelection {
468    pub agent: AgentId,
469    #[serde(deserialize_with = "crate::deserialize_required_model_selection")]
470    #[schemars(with = "crate::RequiredModelSelectionSchema")]
471    pub model: ModelSelection,
472    #[serde(
473        default,
474        deserialize_with = "deserialize_optional_preset",
475        skip_serializing_if = "Option::is_none"
476    )]
477    #[schemars(with = "Option<AgentPresetNameSchema>")]
478    pub preset: Option<String>,
479}
480
481#[derive(Clone, Debug, Eq, JsonSchema, PartialEq, Serialize)]
482#[serde(deny_unknown_fields)]
483pub struct AgentDescriptor {
484    pub id: AgentId,
485    #[serde(default, skip_serializing_if = "Option::is_none")]
486    #[schemars(with = "Option<AgentPresetNameSchema>")]
487    pub preset: Option<String>,
488    #[schemars(with = "AgentDescriptionSchema")]
489    pub description: String,
490    pub mode: AgentMode,
491    pub enabled: bool,
492    pub runnable_as_root: bool,
493    #[schemars(with = "Vec<crate::RequiredModelSelectionSchema>")]
494    #[schemars(length(max = 256))]
495    pub resolved_fallback: Vec<ModelSelection>,
496    #[schemars(length(max = 256))]
497    pub delegation_targets: Vec<AgentId>,
498}
499
500struct AgentDescriptionSchema;
501struct AgentPresetNameSchema;
502struct AgentPromptSchema;
503impl JsonSchema for AgentDescriptionSchema {
504    fn inline_schema() -> bool {
505        true
506    }
507    fn schema_name() -> Cow<'static, str> {
508        Cow::Borrowed("AgentDescription")
509    }
510    fn json_schema(_: &mut SchemaGenerator) -> Schema {
511        json_schema!({"type":"string","minLength":1,"maxLength":512,"pattern":"^(?=.*\\S)[^\\p{Cc}\\p{Cf}]+$"})
512    }
513}
514impl JsonSchema for AgentPresetNameSchema {
515    fn inline_schema() -> bool {
516        true
517    }
518    fn schema_name() -> Cow<'static, str> {
519        Cow::Borrowed("AgentPresetName")
520    }
521    fn json_schema(_: &mut SchemaGenerator) -> Schema {
522        json_schema!({"type":"string","minLength":1,"maxLength":64,"pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$"})
523    }
524}
525impl JsonSchema for AgentPromptSchema {
526    fn inline_schema() -> bool {
527        true
528    }
529    fn schema_name() -> Cow<'static, str> {
530        Cow::Borrowed("AgentPrompt")
531    }
532    fn json_schema(_: &mut SchemaGenerator) -> Schema {
533        json_schema!({"type":"string","minLength":1,"maxLength":131072,"pattern":"(?s)^(?=.*\\S).+$"})
534    }
535}
536impl AgentDescriptor {
537    pub fn validate(&self) -> Result<(), AgentSchemaError> {
538        if self
539            .preset
540            .as_deref()
541            .is_some_and(|name| !valid_preset_name(name))
542        {
543            return Err(AgentSchemaError::InvalidPreset);
544        }
545        if self.description.trim().is_empty()
546            || self.description.len() > 512
547            || self.description.chars().any(char::is_control)
548        {
549            return Err(AgentSchemaError::InvalidDescription);
550        }
551        if self.resolved_fallback.len() > 256 || self.delegation_targets.len() > 256 {
552            return Err(AgentSchemaError::InvalidListBounds);
553        }
554        let mut models = BTreeSet::new();
555        if !self
556            .resolved_fallback
557            .iter()
558            .all(|selection| models.insert(selection.model.clone()))
559        {
560            return Err(AgentSchemaError::DuplicateFallbackModel);
561        }
562        if self
563            .delegation_targets
564            .windows(2)
565            .any(|pair| pair[0] >= pair[1])
566        {
567            return Err(AgentSchemaError::InvalidDelegationTargets);
568        }
569        if self.runnable_as_root
570            && (!self.enabled
571                || matches!(self.mode, AgentMode::Subagent | AgentMode::Internal)
572                || self.resolved_fallback.is_empty())
573        {
574            return Err(AgentSchemaError::InvalidRootRunnable);
575        }
576        Ok(())
577    }
578}
579
580fn valid_preset_name(name: &str) -> bool {
581    !name.is_empty()
582        && name.len() <= 64
583        && name.split('-').all(|part| {
584            !part.is_empty()
585                && part
586                    .bytes()
587                    .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit())
588        })
589}
590
591fn deserialize_optional_preset<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
592where
593    D: serde::Deserializer<'de>,
594{
595    let preset = Option::<String>::deserialize(deserializer)?;
596    if preset
597        .as_deref()
598        .is_some_and(|name| !valid_preset_name(name))
599    {
600        return Err(serde::de::Error::custom(AgentSchemaError::InvalidPreset));
601    }
602    Ok(preset)
603}
604impl<'de> Deserialize<'de> for AgentDescriptor {
605    fn deserialize<D>(d: D) -> Result<Self, D::Error>
606    where
607        D: serde::Deserializer<'de>,
608    {
609        #[derive(Deserialize)]
610        #[serde(deny_unknown_fields)]
611        struct Wire {
612            id: AgentId,
613            #[serde(default)]
614            preset: Option<String>,
615            description: String,
616            mode: AgentMode,
617            enabled: bool,
618            runnable_as_root: bool,
619            #[serde(deserialize_with = "crate::deserialize_required_model_selections")]
620            resolved_fallback: Vec<ModelSelection>,
621            delegation_targets: Vec<AgentId>,
622        }
623        let w = Wire::deserialize(d)?;
624        let value = Self {
625            id: w.id,
626            preset: w.preset,
627            description: w.description,
628            mode: w.mode,
629            enabled: w.enabled,
630            runnable_as_root: w.runnable_as_root,
631            resolved_fallback: w.resolved_fallback,
632            delegation_targets: w.delegation_targets,
633        };
634        value.validate().map_err(serde::de::Error::custom)?;
635        Ok(value)
636    }
637}
638
639#[derive(Clone, Copy, Debug, Eq, PartialEq)]
640pub enum AgentSchemaError {
641    InvalidDelegationTargets,
642    InvalidDescription,
643    InvalidPreset,
644    InvalidPrompt,
645    InvalidListBounds,
646    DuplicatePermissionRule,
647    DuplicateFallbackModel,
648    InvalidModelBinding,
649    InvalidSuffixStart,
650    SelectionMismatch,
651    SelectedSuffixMismatch,
652    InvalidRootRunnable,
653}
654impl fmt::Display for AgentSchemaError {
655    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
656        f.write_str(match self{Self::InvalidDelegationTargets=>"delegation targets must be a nonempty, strictly sorted unique list of at most 256 IDs",Self::InvalidDescription=>"agent description must be nonblank, control-free, and 1..=512 bytes",Self::InvalidPreset=>"agent preset must use lowercase alphanumeric segments separated by hyphens and contain at most 64 bytes",Self::InvalidPrompt=>"composed prompt must be nonblank and at most 128 KiB",Self::InvalidListBounds=>"agent list exceeds bounds or frozen fallback is empty",Self::DuplicatePermissionRule=>"session permission overlay rules must be unique by action and resource",Self::DuplicateFallbackModel=>"fallback chains may contain each model key at most once",Self::InvalidModelBinding=>"agent snapshot contains an invalid frozen model binding",Self::InvalidSuffixStart=>"selected_suffix_start must index the frozen fallback chain",Self::SelectionMismatch=>"run selection does not match the snapshot agent and selected fallback start",Self::SelectedSuffixMismatch=>"selected suffix does not exactly match the frozen fallback order and selected head",Self::InvalidRootRunnable=>"runnable_as_root contradicts enabled mode or fallback state"})
657    }
658}
659impl std::error::Error for AgentSchemaError {}