1use crate::common::{
4 CapabilityId, ExtensionsV1, ValidationError, deserialize_schema_version,
5 validate_bounded_string, validate_schema_version, validate_unique_strings,
6};
7use crate::experiment::DataClassification;
8use serde::{Deserialize, Serialize};
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 AgentConnector,
21 ModelProvider,
22 KnowledgeProvider,
23 OutcomeEvaluator,
24 CostEstimator,
25 QualityEstimator,
26 PolicyGate,
27 Scheduler,
28 ReadCompressionStrategy,
29 SearchRetrieval,
30 AgentRuntime,
31 Addon,
32 RemoteCapability,
33 ShellOutputOptimization,
34 Other,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
39#[serde(rename_all = "snake_case")]
40pub enum Reversibility {
41 Reversible,
42 Irreversible,
43 Conditional,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
48#[serde(rename_all = "snake_case")]
49pub enum Determinism {
50 Deterministic,
51 Seeded,
52 NonDeterministic,
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
57#[serde(rename_all = "snake_case")]
58pub enum DataMovement {
59 None,
60 LocalOnly,
61 Remote,
62 CrossRegion,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67#[serde(deny_unknown_fields)]
68pub struct MeasurementSupportV1 {
69 pub latency: bool,
70 pub tokens: bool,
71 pub quality: bool,
72}
73
74#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
76#[serde(deny_unknown_fields)]
77pub struct SurfaceSupportV1 {
78 pub supported: bool,
79 #[serde(default, skip_serializing_if = "Option::is_none")]
80 pub input_schema_ref: Option<String>,
81 #[serde(default, skip_serializing_if = "Option::is_none")]
82 pub output_schema_ref: Option<String>,
83}
84
85#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
87pub struct CapabilityManifestV1 {
88 #[serde(deserialize_with = "deserialize_schema_version")]
89 pub schema_version: u32,
90 pub capability_id: CapabilityId,
91 pub provider: String,
92 pub kind: CapabilityKind,
93 pub version: String,
94 pub surfaces: Vec<String>,
95 pub support_matrix: BTreeMap<String, SurfaceSupportV1>,
96 pub local: bool,
97 pub remote: bool,
98 pub reversibility: Reversibility,
99 pub determinism: Determinism,
100 pub data_movement: DataMovement,
101 pub supported_classifications: Vec<DataClassification>,
102 pub measurement_support: MeasurementSupportV1,
103 #[serde(default, skip_serializing_if = "Option::is_none")]
104 pub input_schema_ref: Option<String>,
105 #[serde(default, skip_serializing_if = "Option::is_none")]
106 pub output_schema_ref: Option<String>,
107 pub conformance_version: u32,
108 #[serde(default, flatten)]
109 pub extra: ExtensionsV1,
110}
111
112const CAPABILITY_RESERVED_FIELDS: &[&str] = &[
113 "schema_version",
114 "capability_id",
115 "provider",
116 "kind",
117 "version",
118 "surfaces",
119 "support_matrix",
120 "local",
121 "remote",
122 "reversibility",
123 "determinism",
124 "data_movement",
125 "supported_classifications",
126 "measurement_support",
127 "input_schema_ref",
128 "output_schema_ref",
129 "conformance_version",
130];
131
132impl CapabilityManifestV1 {
133 pub fn validate(&self) -> Result<(), ValidationError> {
135 self.extra.validate_reserved(CAPABILITY_RESERVED_FIELDS)?;
136 validate_schema_version(self.schema_version)?;
137 if !self.local && !self.remote {
138 return Err(ValidationError::new(
139 "capability must support local or remote execution",
140 ));
141 }
142 validate_bounded_string(&self.provider, "provider")?;
143 validate_bounded_string(&self.version, "capability version")?;
144 validate_unique_strings(&self.surfaces, "surfaces")?;
145 if self.surfaces.is_empty() {
146 return Err(ValidationError::new(
147 "capability must declare at least one surface",
148 ));
149 }
150 let supported = self
151 .support_matrix
152 .keys()
153 .map(String::as_str)
154 .collect::<Vec<_>>();
155 validate_unique_strings(&supported, "support_matrix")?;
156 if self
157 .support_matrix
158 .keys()
159 .any(|surface| !self.surfaces.contains(surface))
160 {
161 return Err(ValidationError::new(
162 "support_matrix contains a surface not declared in surfaces",
163 ));
164 }
165 if self.supported_classifications.len() > 4
166 || self
167 .supported_classifications
168 .iter()
169 .enumerate()
170 .any(|(index, value)| self.supported_classifications[index + 1..].contains(value))
171 {
172 return Err(ValidationError::new(
173 "supported_classifications exceeds the enum or contains duplicates",
174 ));
175 }
176 if self.remote
177 && matches!(
178 self.data_movement,
179 DataMovement::None | DataMovement::LocalOnly
180 )
181 {
182 return Err(ValidationError::new(
183 "remote capability must declare remote or cross-region data movement",
184 ));
185 }
186 if !self.remote
187 && matches!(
188 self.data_movement,
189 DataMovement::Remote | DataMovement::CrossRegion
190 )
191 {
192 return Err(ValidationError::new(
193 "remote or cross-region data movement requires remote=true",
194 ));
195 }
196 if self.remote
197 && self.supported_classifications.iter().any(|classification| {
198 matches!(
199 classification,
200 DataClassification::Confidential | DataClassification::Restricted
201 )
202 })
203 {
204 return Err(ValidationError::new(
205 "remote capability cannot accept confidential or restricted data",
206 ));
207 }
208 Ok(())
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215
216 fn id<T>(value: &str) -> T
217 where
218 T: TryFrom<String>,
219 <T as TryFrom<String>>::Error: std::fmt::Debug,
220 {
221 T::try_from(value.to_owned()).expect("identifier should be valid")
222 }
223
224 #[test]
225 fn serialization_round_trip() {
226 let manifest = CapabilityManifestV1 {
227 schema_version: 1,
228 capability_id: id("capability:search"),
229 provider: "provider-1".to_owned(),
230 kind: CapabilityKind::Tool,
231 version: "1.0.0".to_owned(),
232 surfaces: vec!["mcp".to_owned(), "cli".to_owned()],
233 support_matrix: BTreeMap::from([(
234 "mcp".to_owned(),
235 SurfaceSupportV1 {
236 supported: true,
237 input_schema_ref: Some("schema:input".to_owned()),
238 output_schema_ref: Some("schema:output".to_owned()),
239 },
240 )]),
241 local: true,
242 remote: true,
243 reversibility: Reversibility::Reversible,
244 determinism: Determinism::Deterministic,
245 data_movement: DataMovement::Remote,
246 supported_classifications: vec![DataClassification::Public],
247 measurement_support: MeasurementSupportV1 {
248 latency: true,
249 tokens: true,
250 quality: false,
251 },
252 input_schema_ref: Some("schema:input".to_owned()),
253 output_schema_ref: Some("schema:output".to_owned()),
254 conformance_version: 1,
255 extra: {
256 let mut extra = ExtensionsV1::default();
257 extra
258 .insert("future_field", serde_json::Value::from(true))
259 .expect("extension should be valid");
260 extra
261 },
262 };
263 let json = serde_json::to_string(&manifest).expect("manifest should serialize");
264 let decoded: CapabilityManifestV1 =
265 serde_json::from_str(&json).expect("manifest should deserialize");
266 assert_eq!(manifest, decoded);
267 manifest
268 .validate()
269 .expect("manifest should satisfy invariants");
270 }
271
272 #[test]
273 fn capability_kind_wire_names_cover_protocol_roles() {
274 let kinds = [
275 (CapabilityKind::Tool, "tool"),
276 (CapabilityKind::Model, "model"),
277 (CapabilityKind::Provider, "provider"),
278 (CapabilityKind::ContextSource, "context_source"),
279 (CapabilityKind::Validator, "validator"),
280 (CapabilityKind::AgentConnector, "agent_connector"),
281 (CapabilityKind::ModelProvider, "model_provider"),
282 (CapabilityKind::KnowledgeProvider, "knowledge_provider"),
283 (CapabilityKind::OutcomeEvaluator, "outcome_evaluator"),
284 (CapabilityKind::CostEstimator, "cost_estimator"),
285 (CapabilityKind::QualityEstimator, "quality_estimator"),
286 (CapabilityKind::PolicyGate, "policy_gate"),
287 (CapabilityKind::Scheduler, "scheduler"),
288 (
289 CapabilityKind::ReadCompressionStrategy,
290 "read_compression_strategy",
291 ),
292 (CapabilityKind::SearchRetrieval, "search_retrieval"),
293 (CapabilityKind::AgentRuntime, "agent_runtime"),
294 (CapabilityKind::Addon, "addon"),
295 (CapabilityKind::RemoteCapability, "remote_capability"),
296 (
297 CapabilityKind::ShellOutputOptimization,
298 "shell_output_optimization",
299 ),
300 (CapabilityKind::Other, "other"),
301 ];
302 for (kind, expected) in kinds {
303 assert_eq!(
304 serde_json::to_string(&kind).expect("serialize kind"),
305 serde_json::to_string(expected).expect("serialize expected"),
306 );
307 }
308 }
309
310 #[test]
311 fn remote_local_only_is_rejected_fail_closed() {
312 let mut manifest: CapabilityManifestV1 = serde_json::from_str(
313 r#"{
314 "schema_version":1,
315 "capability_id":"capability:test",
316 "provider":"provider",
317 "kind":"tool",
318 "version":"1.0.0",
319 "surfaces":["mcp"],
320 "support_matrix":{"mcp":{"supported":true}},
321 "local":true,
322 "remote":true,
323 "reversibility":"reversible",
324 "determinism":"deterministic",
325 "data_movement":"local_only",
326 "supported_classifications":["Public"],
327 "measurement_support":{"latency":true,"tokens":true,"quality":true},
328 "conformance_version":1
329 }"#,
330 )
331 .expect("manifest should deserialize");
332 assert!(manifest.validate().is_err());
333 manifest.remote = false;
334 manifest
335 .validate()
336 .expect("local-only capability should be valid");
337 }
338}