Skip to main content

kindly_guard_server/
versioning.rs

1// Copyright 2025 Kindly Software Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14//! API versioning for `KindlyGuard`
15//! Provides version management and stability guarantees
16#![allow(missing_docs)] // Simple DTOs with self-explanatory fields
17
18use serde::{Deserialize, Serialize};
19
20/// Current API version
21pub const API_VERSION: &str = "v1-beta";
22
23/// Server version (from Cargo.toml)
24pub const SERVER_VERSION: &str = env!("CARGO_PKG_VERSION");
25
26/// MCP protocol version supported
27pub const MCP_PROTOCOL_VERSION: &str = "2024-11-05";
28
29/// API stability levels
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
31#[serde(rename_all = "lowercase")]
32pub enum ApiStability {
33    /// Experimental features that may change or be removed
34    Experimental,
35    /// Beta features that are mostly stable but may have minor changes
36    Beta,
37    /// Stable features with backward compatibility guarantees
38    Stable,
39    /// Deprecated features that will be removed in future versions
40    Deprecated,
41}
42
43/// API endpoint metadata
44#[derive(Debug, Clone)]
45pub struct ApiEndpoint {
46    pub method: &'static str,
47    pub stability: ApiStability,
48    pub since_version: &'static str,
49    pub deprecated_in: Option<&'static str>,
50    pub removed_in: Option<&'static str>,
51}
52
53/// Version information for responses
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct VersionInfo {
56    pub api_version: String,
57    pub server_version: String,
58    pub protocol_version: String,
59    pub stability: String,
60}
61
62impl Default for VersionInfo {
63    fn default() -> Self {
64        Self {
65            api_version: API_VERSION.to_string(),
66            server_version: SERVER_VERSION.to_string(),
67            protocol_version: MCP_PROTOCOL_VERSION.to_string(),
68            stability: "beta".to_string(),
69        }
70    }
71}
72
73/// Registry of API endpoints and their stability
74pub struct ApiRegistry;
75
76impl ApiRegistry {
77    /// Get all registered API endpoints
78    pub fn endpoints() -> Vec<ApiEndpoint> {
79        vec![
80            // Core MCP methods (stable)
81            ApiEndpoint {
82                method: "initialize",
83                stability: ApiStability::Stable,
84                since_version: "0.9.1",
85                deprecated_in: None,
86                removed_in: None,
87            },
88            ApiEndpoint {
89                method: "initialized",
90                stability: ApiStability::Stable,
91                since_version: "0.9.1",
92                deprecated_in: None,
93                removed_in: None,
94            },
95            ApiEndpoint {
96                method: "shutdown",
97                stability: ApiStability::Stable,
98                since_version: "0.9.1",
99                deprecated_in: None,
100                removed_in: None,
101            },
102            ApiEndpoint {
103                method: "tools/list",
104                stability: ApiStability::Stable,
105                since_version: "0.9.1",
106                deprecated_in: None,
107                removed_in: None,
108            },
109            ApiEndpoint {
110                method: "tools/call",
111                stability: ApiStability::Stable,
112                since_version: "0.9.1",
113                deprecated_in: None,
114                removed_in: None,
115            },
116            ApiEndpoint {
117                method: "resources/list",
118                stability: ApiStability::Stable,
119                since_version: "0.9.1",
120                deprecated_in: None,
121                removed_in: None,
122            },
123            ApiEndpoint {
124                method: "resources/read",
125                stability: ApiStability::Stable,
126                since_version: "0.9.1",
127                deprecated_in: None,
128                removed_in: None,
129            },
130            ApiEndpoint {
131                method: "prompts/list",
132                stability: ApiStability::Beta,
133                since_version: "0.9.1",
134                deprecated_in: None,
135                removed_in: None,
136            },
137            // Security extensions (experimental)
138            ApiEndpoint {
139                method: "security/status",
140                stability: ApiStability::Experimental,
141                since_version: "0.9.1",
142                deprecated_in: None,
143                removed_in: None,
144            },
145            ApiEndpoint {
146                method: "security/threats",
147                stability: ApiStability::Experimental,
148                since_version: "0.9.1",
149                deprecated_in: None,
150                removed_in: None,
151            },
152            ApiEndpoint {
153                method: "security/rate_limit_status",
154                stability: ApiStability::Experimental,
155                since_version: "0.9.1",
156                deprecated_in: None,
157                removed_in: None,
158            },
159            // Admin methods (experimental)
160            ApiEndpoint {
161                method: "admin/update_config",
162                stability: ApiStability::Experimental,
163                since_version: "0.9.1",
164                deprecated_in: None,
165                removed_in: None,
166            },
167        ]
168    }
169
170    /// Check if a method is stable
171    pub fn is_stable(method: &str) -> bool {
172        Self::endpoints()
173            .iter()
174            .find(|e| e.method == method)
175            .is_some_and(|e| e.stability == ApiStability::Stable)
176    }
177
178    /// Get stability for a method
179    pub fn get_stability(method: &str) -> Option<ApiStability> {
180        Self::endpoints()
181            .iter()
182            .find(|e| e.method == method)
183            .map(|e| e.stability)
184    }
185
186    /// Check if experimental features are enabled
187    pub fn experimental_enabled() -> bool {
188        // Could be controlled by environment variable or config
189        std::env::var("KINDLYGUARD_EXPERIMENTAL")
190            .map(|v| v == "1" || v.to_lowercase() == "true")
191            .unwrap_or(false)
192    }
193}
194
195/// Add version metadata to responses
196pub fn add_version_metadata(response: &mut serde_json::Value) {
197    if let Some(obj) = response.as_object_mut() {
198        let version_info = VersionInfo::default();
199        obj.insert(
200            "_meta".to_string(),
201            serde_json::json!({
202                "api_version": version_info.api_version,
203                "server_version": version_info.server_version,
204                "timestamp": chrono::Utc::now().to_rfc3339(),
205            }),
206        );
207    }
208}
209
210/// Version negotiation for protocol compatibility
211pub struct VersionNegotiator;
212
213impl VersionNegotiator {
214    /// Supported protocol versions in order of preference
215    pub const SUPPORTED_PROTOCOLS: &'static [&'static str] = &[
216        "2024-11-05", // Current
217        "2024-10-01", // Previous (if we supported it)
218    ];
219
220    /// Check if a protocol version is supported
221    pub fn is_supported(version: &str) -> bool {
222        Self::SUPPORTED_PROTOCOLS.contains(&version)
223    }
224
225    /// Get the best matching version
226    pub fn negotiate(requested: &str) -> Option<&'static str> {
227        // Find exact match in our supported versions
228        Self::SUPPORTED_PROTOCOLS
229            .iter()
230            .find(|&&v| v == requested)
231            .copied()
232    }
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238
239    #[test]
240    fn test_api_registry() {
241        assert!(ApiRegistry::is_stable("initialize"));
242        assert!(ApiRegistry::is_stable("tools/list"));
243        assert!(!ApiRegistry::is_stable("security/status"));
244
245        assert_eq!(
246            ApiRegistry::get_stability("security/status"),
247            Some(ApiStability::Experimental)
248        );
249    }
250
251    #[test]
252    fn test_version_negotiation() {
253        assert!(VersionNegotiator::is_supported("2024-11-05"));
254        assert!(!VersionNegotiator::is_supported("2023-01-01"));
255
256        assert_eq!(
257            VersionNegotiator::negotiate("2024-11-05"),
258            Some("2024-11-05")
259        );
260    }
261
262    #[test]
263    fn test_version_metadata() {
264        let mut response = serde_json::json!({
265            "result": "test"
266        });
267
268        add_version_metadata(&mut response);
269
270        assert!(response.get("_meta").is_some());
271        let meta = response.get("_meta").unwrap();
272        assert!(meta.get("api_version").is_some());
273        assert!(meta.get("server_version").is_some());
274    }
275}