Skip to main content

lean_ctx_protocol/
capability.rs

1//! Capability discovery and conformance contract.
2
3use 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/// Capability category exposed by a provider or runtime.
12#[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/// Whether a capability's effects can be undone.
26#[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/// Determinism guarantee made by a capability.
35#[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/// Boundary at which a capability moves data.
44#[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/// Measurement dimensions a capability can report.
54#[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/// Per-surface support details in a capability manifest.
63#[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/// Provider capability manifest. Unknown top-level fields are retained for additive evolution.
74#[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    /// Validate location and schema invariants.
102    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}