1use crate::common::{
4 CapabilityId, ValidationError, deserialize_schema_version, validate_schema_version,
5};
6use crate::experiment::DataClassification;
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use std::collections::BTreeMap;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum CapabilityKind {
15 Tool,
16 Model,
17 Provider,
18 ContextSource,
19 Validator,
20 Scheduler,
21 ShellOutputOptimization,
22 Other,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(rename_all = "snake_case")]
28pub enum Reversibility {
29 Reversible,
30 Irreversible,
31 Conditional,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(rename_all = "snake_case")]
37pub enum Determinism {
38 Deterministic,
39 Seeded,
40 NonDeterministic,
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
45#[serde(rename_all = "snake_case")]
46pub enum DataMovement {
47 None,
48 LocalOnly,
49 Remote,
50 CrossRegion,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
55#[serde(deny_unknown_fields)]
56pub struct MeasurementSupportV1 {
57 pub latency: bool,
58 pub tokens: bool,
59 pub quality: bool,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
64#[serde(deny_unknown_fields)]
65pub struct SurfaceSupportV1 {
66 pub supported: bool,
67 #[serde(default, skip_serializing_if = "Option::is_none")]
68 pub input_schema_ref: Option<String>,
69 #[serde(default, skip_serializing_if = "Option::is_none")]
70 pub output_schema_ref: Option<String>,
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
75pub struct CapabilityManifestV1 {
76 #[serde(deserialize_with = "deserialize_schema_version")]
77 pub schema_version: u32,
78 pub capability_id: CapabilityId,
79 pub provider: String,
80 pub kind: CapabilityKind,
81 pub version: String,
82 pub surfaces: Vec<String>,
83 pub support_matrix: BTreeMap<String, SurfaceSupportV1>,
84 pub local: bool,
85 pub remote: bool,
86 pub reversibility: Reversibility,
87 pub determinism: Determinism,
88 pub data_movement: DataMovement,
89 pub supported_classifications: Vec<DataClassification>,
90 pub measurement_support: MeasurementSupportV1,
91 #[serde(default, skip_serializing_if = "Option::is_none")]
92 pub input_schema_ref: Option<String>,
93 #[serde(default, skip_serializing_if = "Option::is_none")]
94 pub output_schema_ref: Option<String>,
95 pub conformance_version: u32,
96 #[serde(flatten)]
97 pub extra: BTreeMap<String, Value>,
98}
99
100impl CapabilityManifestV1 {
101 pub fn validate(&self) -> Result<(), ValidationError> {
103 validate_schema_version(self.schema_version)?;
104 if !self.local && !self.remote {
105 return Err(ValidationError::new(
106 "capability must support local or remote execution",
107 ));
108 }
109 Ok(())
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116
117 fn id<T>(value: &str) -> T
118 where
119 T: TryFrom<String>,
120 <T as TryFrom<String>>::Error: std::fmt::Debug,
121 {
122 T::try_from(value.to_owned()).expect("identifier should be valid")
123 }
124
125 #[test]
126 fn serialization_round_trip() {
127 let manifest = CapabilityManifestV1 {
128 schema_version: 1,
129 capability_id: id("capability:search"),
130 provider: "provider-1".to_owned(),
131 kind: CapabilityKind::Tool,
132 version: "1.0.0".to_owned(),
133 surfaces: vec!["mcp".to_owned(), "cli".to_owned()],
134 support_matrix: BTreeMap::from([(
135 "mcp".to_owned(),
136 SurfaceSupportV1 {
137 supported: true,
138 input_schema_ref: Some("schema:input".to_owned()),
139 output_schema_ref: Some("schema:output".to_owned()),
140 },
141 )]),
142 local: true,
143 remote: true,
144 reversibility: Reversibility::Reversible,
145 determinism: Determinism::Deterministic,
146 data_movement: DataMovement::LocalOnly,
147 supported_classifications: vec![DataClassification::Public],
148 measurement_support: MeasurementSupportV1 {
149 latency: true,
150 tokens: true,
151 quality: false,
152 },
153 input_schema_ref: Some("schema:input".to_owned()),
154 output_schema_ref: Some("schema:output".to_owned()),
155 conformance_version: 1,
156 extra: BTreeMap::from([("future_field".to_owned(), Value::from(true))]),
157 };
158 let json = serde_json::to_string(&manifest).expect("manifest should serialize");
159 let decoded: CapabilityManifestV1 =
160 serde_json::from_str(&json).expect("manifest should deserialize");
161 assert_eq!(manifest, decoded);
162 manifest
163 .validate()
164 .expect("manifest should satisfy invariants");
165 }
166}