Skip to main content

lean_ctx/core/
server_capabilities.rs

1//! `GET /v1/capabilities` — runtime discovery of what this lean-ctx instance
2//! supports, so any client (any language) can branch on real features instead
3//! of trial calls. The HTTP route lives in `http_server`; the payload builder
4//! lives here so it stays compiled (and drift-tested) without the
5//! `http-server` feature.
6//!
7//! Contract: `docs/contracts/capabilities-contract-v1.md`. The set of
8//! [`TOP_LEVEL_KEYS`] is the stable contract surface and is bound to that doc
9//! by `tests/capabilities_contract_up_to_date.rs`.
10//!
11//! Not to be confused with [`crate::core::capabilities`], which models RBAC
12//! permissions (`fs:read`, …). This module describes *server* capabilities.
13
14use serde_json::{Value, json};
15
16use crate::core::contracts::{CAPABILITIES_CONTRACT_VERSION, status_kv, versions_kv};
17
18/// Stable, documented top-level keys of the capabilities document.
19pub const TOP_LEVEL_KEYS: [&str; 11] = [
20    "contract_version",
21    "server",
22    "plane",
23    "transports",
24    "presets",
25    "read_modes",
26    "tools",
27    "features",
28    "extensions",
29    "contracts",
30    "contract_status",
31];
32
33/// Build the capabilities document for this running instance.
34pub fn capabilities_value() -> Value {
35    let manifest = crate::core::mcp_manifest::manifest_value();
36    let tool_names = tool_names(&manifest);
37    let read_modes = manifest.get("read_modes").cloned().unwrap_or(Value::Null);
38    let active_persona =
39        crate::core::persona::Persona::resolve(&crate::core::config::Config::load());
40
41    json!({
42        "contract_version": CAPABILITIES_CONTRACT_VERSION,
43        "server": {
44            "name": "lean-ctx",
45            "version": env!("CARGO_PKG_VERSION"),
46            // Additive: which pipeline built this binary, from which commit.
47            "build": crate::core::integrity::build().to_json(),
48            "persona": active_persona.name,
49        },
50        "plane": "personal",
51        "transports": ["stdio-mcp", "http-mcp", "rest", "sse"],
52        "presets": crate::core::persona::Persona::builtin_names(),
53        "read_modes": read_modes,
54        "tools": {
55            "total": tool_names.len(),
56            "names": tool_names,
57        },
58        "features": features(),
59        "extensions": extensions(),
60        "contracts": versions_kv(),
61        // Stability per contract document (frozen|stable|experimental) so
62        // clients can check compatibility before building against a surface
63        // (GL #394). Additive: existing consumers are unaffected.
64        "contract_status": status_kv(),
65    })
66}
67
68fn tool_names(manifest: &Value) -> Vec<String> {
69    manifest
70        .get("tools")
71        .and_then(|t| t.get("granular"))
72        .and_then(|g| g.as_array())
73        .map(|arr| {
74            arr.iter()
75                .filter_map(|t| t.get("name").and_then(|n| n.as_str()).map(String::from))
76                .collect()
77        })
78        .unwrap_or_default()
79}
80
81/// Always-on Community capabilities. They are explicit Trust Core product
82/// records rather than capabilities inferred to be free because they are local.
83pub const COMMUNITY_ALWAYS_ON_FEATURES: &[&str] = &[
84    "compression",
85    "caching",
86    "knowledge",
87    "session",
88    "gateway",
89    "sensitivity_floor",
90    "savings_ledger",
91    "audit_trail",
92    "routing",
93    "team_seat_value_v1",
94];
95
96/// Community capabilities gated only by compilation.
97pub const COMMUNITY_OPTIONAL_FEATURES: &[&str] = &[
98    "ast_compression",
99    "semantic_search",
100    "http_server",
101    "wasm_runtime",
102    // Cross-shape routing Anthropic→OpenAI (enterprise#16, `shape-xlat`).
103    "shape_translation",
104];
105
106/// Always-on capabilities plus compiled-in feature flags. Booleans reflect what
107/// this binary can actually do.
108fn features() -> Value {
109    json!({
110        "compression": true,
111        "caching": true,
112        "knowledge": true,
113        "session": true,
114        "gateway": true,
115        "sensitivity_floor": true,
116        "savings_ledger": true,
117        "audit_trail": true,
118        // The advertised primitive is deterministic Community routing.
119        // Personalized adaptive routing has its own Pro capability ID.
120        "routing": true,
121        "team_seat_value_v1": true,
122        "ast_compression": cfg!(feature = "tree-sitter"),
123        "semantic_search": cfg!(feature = "embeddings"),
124        "http_server": cfg!(feature = "http-server"),
125        "wasm_runtime": cfg!(feature = "wasm"),
126        "shape_translation": cfg!(feature = "shape-xlat"),
127    })
128}
129
130/// Runtime-discovered extensions: installed plugins plus the registered
131/// read-modes / compressors / chunkers (EPIC 12.9). The sandboxed extension
132/// runtime (EPIC 12.8) expands what registers here.
133fn extensions() -> Value {
134    let (read_modes, compressors, chunkers) = crate::core::extension_registry::global()
135        .read()
136        .map(|r| (r.read_mode_names(), r.compressor_names(), r.chunker_names()))
137        .unwrap_or_default();
138
139    json!({
140        "plugins": [],
141        "tools": [],
142        "read_modes": read_modes,
143        "compressors": compressors,
144        "chunkers": chunkers,
145    })
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn payload_has_exactly_documented_top_level_keys() {
154        let v = capabilities_value();
155        let obj = v.as_object().expect("capabilities is an object");
156        let mut keys: Vec<&str> = obj.keys().map(String::as_str).collect();
157        keys.sort_unstable();
158        let mut expected: Vec<&str> = TOP_LEVEL_KEYS.to_vec();
159        expected.sort_unstable();
160        assert_eq!(keys, expected, "top-level keys drifted from TOP_LEVEL_KEYS");
161    }
162
163    #[test]
164    fn contract_version_matches_constant() {
165        let v = capabilities_value();
166        assert_eq!(v["contract_version"], json!(CAPABILITIES_CONTRACT_VERSION));
167    }
168
169    #[test]
170    fn extensions_expose_registry_builtins() {
171        let v = capabilities_value();
172        let ext = &v["extensions"];
173        assert!(ext["plugins"].is_array());
174        let compressors = ext["compressors"].as_array().expect("compressors array");
175        assert!(compressors.iter().any(|c| c == "identity"));
176        assert!(
177            ext["read_modes"]
178                .as_array()
179                .is_some_and(|a| a.iter().any(|m| m == "full"))
180        );
181        assert!(
182            ext["chunkers"]
183                .as_array()
184                .is_some_and(|a| a.iter().any(|c| c == "lines"))
185        );
186    }
187
188    #[test]
189    fn every_advertised_feature_has_an_explicit_product_record() {
190        let v = capabilities_value();
191        for key in v["features"].as_object().expect("features object").keys() {
192            assert!(
193                crate::core::product_capabilities::registry()
194                    .find(key)
195                    .is_some(),
196                "server feature '{key}' lacks product classification"
197            );
198        }
199    }
200
201    #[test]
202    fn community_always_on_features_are_available_without_an_account() {
203        let v = capabilities_value();
204        for key in COMMUNITY_ALWAYS_ON_FEATURES {
205            assert_eq!(
206                v["features"][key],
207                json!(true),
208                "Community capability '{key}' must be available"
209            );
210            let capability = crate::core::product_capabilities::registry()
211                .find(key)
212                .expect("feature classified");
213            assert_eq!(
214                capability.minimum_plan(),
215                crate::core::product_capabilities::Plan::Community
216            );
217            assert!(!capability.account_required);
218        }
219    }
220
221    #[test]
222    fn reports_compiled_features() {
223        let v = capabilities_value();
224        // Always-on capabilities are unconditionally true.
225        assert_eq!(v["features"]["compression"], json!(true));
226        assert_eq!(v["features"]["savings_ledger"], json!(true));
227        assert_eq!(v["features"]["team_seat_value_v1"], json!(true));
228        assert_eq!(
229            v["contracts"]["leanctx.contract.team_seat_value_v1.schema_version"],
230            json!(1)
231        );
232        // Feature-gated flags mirror the compile-time cfg.
233        assert_eq!(
234            v["features"]["semantic_search"],
235            json!(cfg!(feature = "embeddings"))
236        );
237    }
238}