1use serde::{Deserialize, Serialize};
7
8#[cfg(feature = "openapi")]
9use utoipa::ToSchema;
10
11use crate::capabilities::RiskLevel;
12use crate::capability_types::{CapabilityId, CapabilityStatus};
13use crate::tool_types::ToolDefinition;
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
19#[cfg_attr(feature = "openapi", derive(ToSchema))]
20pub struct CapabilityInfo {
21 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "session_file_system"))]
23 pub id: CapabilityId,
24 #[cfg_attr(feature = "openapi", schema(example = "Session File System"))]
26 pub name: String,
27 #[cfg_attr(
29 feature = "openapi",
30 schema(
31 example = "Read, write, edit, list, grep, delete, and stat files in the session workspace."
32 )
33 )]
34 pub description: String,
35 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "active"))]
37 pub status: CapabilityStatus,
38 #[serde(skip_serializing_if = "Option::is_none")]
40 #[cfg_attr(feature = "openapi", schema(example = "Folder"))]
41 pub icon: Option<String>,
42 #[serde(skip_serializing_if = "Option::is_none")]
44 #[cfg_attr(feature = "openapi", schema(example = "filesystem"))]
45 pub category: Option<String>,
46 #[serde(skip_serializing_if = "Option::is_none")]
48 #[cfg_attr(
49 feature = "openapi",
50 schema(
51 example = "You can read and write files in /workspace via the session_file_system tools."
52 )
53 )]
54 pub system_prompt: Option<String>,
55 #[serde(skip_serializing_if = "Vec::is_empty", default)]
57 #[cfg_attr(
58 feature = "openapi",
59 schema(
60 value_type = Vec<Object>,
61 example = json!([
62 {"name": "read_file", "description": "Read a file from the session workspace."},
63 {"name": "write_file", "description": "Write or overwrite a file in the session workspace."}
64 ])
65 )
66 )]
67 pub tool_definitions: Vec<ToolDefinition>,
68 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
70 #[cfg_attr(feature = "openapi", schema(example = false))]
71 pub is_mcp: bool,
72 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
74 #[cfg_attr(feature = "openapi", schema(example = false))]
75 pub is_skill: bool,
76 #[serde(skip_serializing_if = "Vec::is_empty", default)]
79 #[cfg_attr(feature = "openapi", schema(example = json!(["approval"])))]
80 pub dependencies: Vec<String>,
81 #[serde(skip_serializing_if = "Vec::is_empty", default)]
84 #[cfg_attr(feature = "openapi", schema(example = json!(["file_browser"])))]
85 pub features: Vec<String>,
86 #[serde(skip_serializing_if = "Option::is_none")]
88 #[cfg_attr(
89 feature = "openapi",
90 schema(
91 value_type = Object,
92 example = json!({
93 "type": "object",
94 "properties": {"max_file_bytes": {"type": "integer", "default": 1048576}}
95 })
96 )
97 )]
98 pub config_schema: Option<serde_json::Value>,
99 #[serde(skip_serializing_if = "Option::is_none")]
101 #[cfg_attr(
102 feature = "openapi",
103 schema(
104 value_type = Object,
105 example = json!({"max_file_bytes": {"ui:widget": "updown"}})
106 )
107 )]
108 pub config_ui_schema: Option<serde_json::Value>,
109 #[serde(skip_serializing_if = "is_low_risk", default = "default_risk_level")]
111 #[cfg_attr(feature = "openapi", schema(example = "low"))]
112 pub risk_level: RiskLevel,
113 #[serde(default, skip_serializing_if = "is_zero_u64")]
115 #[cfg_attr(feature = "openapi", schema(example = 42u64))]
116 pub agent_count: u64,
117 #[serde(default, skip_serializing_if = "is_zero_u64")]
119 #[cfg_attr(feature = "openapi", schema(example = 7u64))]
120 pub harness_count: u64,
121 #[allow(rustdoc::bare_urls)]
122 #[serde(default, skip_serializing_if = "Option::is_none")]
124 #[cfg_attr(feature = "openapi", schema(example = "session_file_system"))]
125 pub docs_slug: Option<String>,
126}
127
128fn is_low_risk(r: &RiskLevel) -> bool {
129 *r == RiskLevel::Low
130}
131fn default_risk_level() -> RiskLevel {
132 RiskLevel::Low
133}
134fn is_zero_u64(v: &u64) -> bool {
135 *v == 0
136}
137
138#[allow(rustdoc::bare_urls)]
139pub fn builtin_capability_docs_slug(id: &str) -> Option<&'static str> {
143 match id {
144 "agent_instructions" => Some("agent-instructions"),
145 "skills" => Some("agent-skills"),
146 "browserless" => Some("browserless"),
147 "budgeting" => Some("budgeting"),
148 "current_time" => Some("current-time"),
149 "daytona" => Some("daytona"),
150 "fake_aws" => Some("fake-aws"),
151 "fake_crm" => Some("fake-crm"),
152 "fake_warehouse" => Some("fake-warehouse"),
153 "github_scout" => Some("github-scout"),
154 "session_file_system" => Some("file-system"),
155 "infinity_context" => Some("infinity-context"),
156 "openai_image_generation" => Some("openai-image-generation"),
157 "openai_tool_search" => Some("openai-tool-search"),
158 "tool_search" => Some("generic-tool-search"),
159 "auto_tool_search" => Some("auto-tool-search"),
160 "platform_management" => Some("platform-management"),
161 "prompt_canary_guardrail" => Some("prompt-canary-guardrail"),
162 "self_budget" => Some("self-budget"),
163 "session_schedule" => Some("session-schedules"),
164 "session_storage" => Some("session-storage"),
165 "session_sandbox" => Some("session"),
166 "session_sql_database" => Some("sql-database"),
167 "subagents" => Some("sub-agents"),
168 "stateless_todo_list" => Some("task-management"),
169 "virtual_bash" => Some("virtual-bash"),
170 "web_fetch" => Some("web-fetch"),
171 _ => None,
172 }
173}
174
175impl CapabilityInfo {
176 pub fn matches_search(&self, query: &str) -> bool {
178 let q = query.to_lowercase();
179 self.name.to_lowercase().contains(&q)
180 || self.description.to_lowercase().contains(&q)
181 || self.id.as_str().to_lowercase().contains(&q)
182 || self
183 .category
184 .as_deref()
185 .is_some_and(|cat| cat.to_lowercase().contains(&q))
186 }
187
188 pub fn from_core(cap: &dyn crate::capabilities::Capability) -> Self {
190 let id_str = cap.id();
192 let is_mcp = id_str.starts_with("mcp:");
193 let is_skill =
194 id_str.starts_with("skill:") || id_str == "skills" || cap.category() == Some("Skills");
195
196 Self {
197 id: CapabilityId::new(id_str),
198 name: cap.name().to_string(),
199 description: cap.description().to_string(),
200 status: cap.status(),
201 icon: cap.icon().map(|s| s.to_string()),
202 category: cap.category().map(|s| s.to_string()),
203 system_prompt: cap.system_prompt_preview(),
204 tool_definitions: cap.tool_definitions(),
205 is_mcp,
206 is_skill,
207 dependencies: cap.dependencies().iter().map(|s| s.to_string()).collect(),
208 features: cap.features().iter().map(|s| s.to_string()).collect(),
209 config_schema: cap.config_schema(),
210 config_ui_schema: cap.config_ui_schema(),
211 risk_level: cap.risk_level(),
212 agent_count: 0,
213 harness_count: 0,
214 docs_slug: builtin_capability_docs_slug(id_str).map(|s| s.to_string()),
215 }
216 }
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize)]
221#[cfg_attr(feature = "openapi", derive(ToSchema))]
222pub struct AgentCapability {
223 #[cfg_attr(feature = "openapi", schema(value_type = String))]
225 pub capability_id: CapabilityId,
226 pub position: i32,
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233
234 #[test]
235 fn test_capability_info_serialization() {
236 let cap = CapabilityInfo {
237 id: CapabilityId::new("research"),
238 name: "Research".to_string(),
239 description: "Deep research capability".to_string(),
240 status: CapabilityStatus::Available,
241 icon: Some("search".to_string()),
242 category: Some("AI".to_string()),
243 system_prompt: Some("You have research capabilities.".to_string()),
244 tool_definitions: vec![],
245 is_mcp: false,
246 is_skill: false,
247 dependencies: vec![],
248 features: vec![],
249 config_schema: None,
250 config_ui_schema: None,
251 risk_level: RiskLevel::Low,
252 agent_count: 0,
253 harness_count: 0,
254 docs_slug: None,
255 };
256
257 let json = serde_json::to_string(&cap).unwrap();
258 assert!(json.contains("\"id\":\"research\""));
259 assert!(json.contains("\"status\":\"available\""));
260 assert!(json.contains("\"system_prompt\":\"You have research capabilities.\""));
261 assert!(!json.contains("\"is_mcp\""));
263 assert!(!json.contains("\"is_skill\""));
265 assert!(!json.contains("\"dependencies\""));
267 assert!(!json.contains("\"features\""));
269 }
270
271 #[test]
272 fn test_mcp_capability_info_serialization() {
273 let cap = CapabilityInfo {
274 id: CapabilityId::new("mcp:550e8400-e29b-41d4-a716-446655440000"),
275 name: "Microsoft Learn".to_string(),
276 description: "MCP Server for Microsoft documentation".to_string(),
277 status: CapabilityStatus::Available,
278 icon: Some("plug".to_string()),
279 category: Some("MCP Servers".to_string()),
280 system_prompt: None,
281 tool_definitions: vec![],
282 is_mcp: true,
283 is_skill: false,
284 dependencies: vec![],
285 features: vec![],
286 config_schema: None,
287 config_ui_schema: None,
288 risk_level: RiskLevel::Low,
289 agent_count: 0,
290 harness_count: 0,
291 docs_slug: None,
292 };
293
294 let json = serde_json::to_string(&cap).unwrap();
295 assert!(json.contains("\"is_mcp\":true"));
296 }
297
298 #[test]
299 fn test_capability_with_dependencies_serialization() {
300 let cap = CapabilityInfo {
301 id: CapabilityId::new("sample_data"),
302 name: "Sample Data".to_string(),
303 description: "Sample data for testing".to_string(),
304 status: CapabilityStatus::Available,
305 icon: None,
306 category: None,
307 system_prompt: None,
308 tool_definitions: vec![],
309 is_mcp: false,
310 is_skill: false,
311 dependencies: vec!["session_file_system".to_string()],
312 features: vec![],
313 config_schema: None,
314 config_ui_schema: None,
315 risk_level: RiskLevel::Low,
316 agent_count: 0,
317 harness_count: 0,
318 docs_slug: None,
319 };
320
321 let json = serde_json::to_string(&cap).unwrap();
322 assert!(json.contains("\"dependencies\":[\"session_file_system\"]"));
323 }
324
325 #[test]
326 fn test_agent_capability_serialization() {
327 let agent_cap = AgentCapability {
328 capability_id: CapabilityId::new("test_math"),
329 position: 1,
330 };
331
332 let json = serde_json::to_string(&agent_cap).unwrap();
333 assert!(json.contains("\"capability_id\":\"test_math\""));
334 assert!(json.contains("\"position\":1"));
335 }
336
337 #[test]
338 fn test_test_capabilities() {
339 assert_eq!(CapabilityId::new("test_math").to_string(), "test_math");
341 assert_eq!(
342 CapabilityId::new("test_weather").to_string(),
343 "test_weather"
344 );
345 }
346
347 #[test]
348 fn test_custom_capability_id() {
349 let custom = CapabilityId::new("my_custom_capability");
351 assert_eq!(custom.to_string(), "my_custom_capability");
352
353 let json = serde_json::to_string(&custom).unwrap();
354 assert_eq!(json, "\"my_custom_capability\"");
355 }
356
357 #[test]
358 fn test_capability_with_features_serialization() {
359 let cap = CapabilityInfo {
360 id: CapabilityId::new("session_storage"),
361 name: "Storage".to_string(),
362 description: "Storage capability".to_string(),
363 status: CapabilityStatus::Available,
364 icon: None,
365 category: None,
366 system_prompt: None,
367 tool_definitions: vec![],
368 is_mcp: false,
369 is_skill: false,
370 dependencies: vec![],
371 features: vec!["secrets".to_string(), "key_value".to_string()],
372 config_schema: None,
373 config_ui_schema: None,
374 risk_level: RiskLevel::Low,
375 agent_count: 0,
376 harness_count: 0,
377 docs_slug: None,
378 };
379
380 let json = serde_json::to_string(&cap).unwrap();
381 assert!(json.contains("\"features\":[\"secrets\",\"key_value\"]"));
382 }
383
384 #[test]
385 fn test_from_core_populates_features() {
386 let registry = crate::capabilities::CapabilityRegistry::with_builtins();
387
388 let schedule_cap = registry.get("session_schedule").unwrap();
389 let info = CapabilityInfo::from_core(schedule_cap.as_ref());
390 assert_eq!(info.features, vec!["schedules"]);
391
392 let storage_cap = registry.get("session_storage").unwrap();
393 let info = CapabilityInfo::from_core(storage_cap.as_ref());
394 assert!(info.features.contains(&"secrets".to_string()));
395 assert!(info.features.contains(&"key_value".to_string()));
396
397 let noop_cap = registry.get("noop").unwrap();
399 let info = CapabilityInfo::from_core(noop_cap.as_ref());
400 assert!(info.features.is_empty());
401 }
402
403 #[test]
404 fn test_risk_level_serialization() {
405 let cap = CapabilityInfo {
407 id: CapabilityId::new("safe"),
408 name: "Safe".to_string(),
409 description: "Low risk".to_string(),
410 status: CapabilityStatus::Available,
411 icon: None,
412 category: None,
413 system_prompt: None,
414 tool_definitions: vec![],
415 is_mcp: false,
416 is_skill: false,
417 dependencies: vec![],
418 features: vec![],
419 config_schema: None,
420 config_ui_schema: None,
421 risk_level: RiskLevel::Low,
422 agent_count: 0,
423 harness_count: 0,
424 docs_slug: None,
425 };
426 let json = serde_json::to_string(&cap).unwrap();
427 assert!(
428 !json.contains("\"risk_level\""),
429 "Low risk should be omitted"
430 );
431
432 let cap_high = CapabilityInfo {
434 risk_level: RiskLevel::High,
435 ..cap
436 };
437 let json = serde_json::to_string(&cap_high).unwrap();
438 assert!(json.contains("\"risk_level\":\"high\""));
439 }
440
441 #[test]
442 fn test_from_core_populates_risk_level() {
443 let registry = crate::capabilities::CapabilityRegistry::with_builtins();
444
445 let bash_cap = registry.get("virtual_bash").unwrap();
447 let info = CapabilityInfo::from_core(bash_cap.as_ref());
448 assert_eq!(info.risk_level, RiskLevel::High);
449
450 let fetch_cap = registry.get("web_fetch").unwrap();
452 let info = CapabilityInfo::from_core(fetch_cap.as_ref());
453 assert_eq!(info.risk_level, RiskLevel::High);
454
455 let noop_cap = registry.get("noop").unwrap();
457 let info = CapabilityInfo::from_core(noop_cap.as_ref());
458 assert_eq!(info.risk_level, RiskLevel::Low);
459 }
460
461 #[test]
462 fn test_matches_search() {
463 let cap = CapabilityInfo {
464 id: CapabilityId::new("web_fetch"),
465 name: "Web Fetch".to_string(),
466 description: "Fetch content from URLs".to_string(),
467 status: CapabilityStatus::Available,
468 icon: None,
469 category: Some("Network".to_string()),
470 system_prompt: None,
471 tool_definitions: vec![],
472 is_mcp: false,
473 is_skill: false,
474 dependencies: vec![],
475 features: vec![],
476 config_schema: None,
477 config_ui_schema: None,
478 risk_level: RiskLevel::Low,
479 agent_count: 0,
480 harness_count: 0,
481 docs_slug: None,
482 };
483
484 assert!(cap.matches_search("web"));
486 assert!(cap.matches_search("WEB FETCH"));
487 assert!(cap.matches_search("urls"));
489 assert!(cap.matches_search("web_fetch"));
491 assert!(cap.matches_search("network"));
493 assert!(!cap.matches_search("zzz_nonexistent"));
495 }
496}