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 {}