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(default, skip_serializing_if = "std::ops::Not::not")]
79 #[cfg_attr(feature = "openapi", schema(example = false))]
80 pub is_guardrail: bool,
81 #[serde(skip_serializing_if = "Vec::is_empty", default)]
84 #[cfg_attr(feature = "openapi", schema(example = json!(["approval"])))]
85 pub dependencies: Vec<String>,
86 #[serde(skip_serializing_if = "Vec::is_empty", default)]
89 #[cfg_attr(feature = "openapi", schema(example = json!(["file_browser"])))]
90 pub features: Vec<String>,
91 #[serde(skip_serializing_if = "Option::is_none")]
93 #[cfg_attr(
94 feature = "openapi",
95 schema(
96 value_type = Object,
97 example = json!({
98 "type": "object",
99 "properties": {"max_file_bytes": {"type": "integer", "default": 1048576}}
100 })
101 )
102 )]
103 pub config_schema: Option<serde_json::Value>,
104 #[serde(skip_serializing_if = "Option::is_none")]
106 #[cfg_attr(
107 feature = "openapi",
108 schema(
109 value_type = Object,
110 example = json!({"max_file_bytes": {"ui:widget": "updown"}})
111 )
112 )]
113 pub config_ui_schema: Option<serde_json::Value>,
114 #[serde(skip_serializing_if = "is_low_risk", default = "default_risk_level")]
116 #[cfg_attr(feature = "openapi", schema(example = "low"))]
117 pub risk_level: RiskLevel,
118 #[serde(default, skip_serializing_if = "is_zero_u64")]
120 #[cfg_attr(feature = "openapi", schema(example = 42u64))]
121 pub agent_count: u64,
122 #[serde(default, skip_serializing_if = "is_zero_u64")]
124 #[cfg_attr(feature = "openapi", schema(example = 7u64))]
125 pub harness_count: u64,
126 #[allow(rustdoc::bare_urls)]
127 #[serde(default, skip_serializing_if = "Option::is_none")]
129 #[cfg_attr(feature = "openapi", schema(example = "session_file_system"))]
130 pub docs_slug: Option<String>,
131 #[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
135 #[cfg_attr(
136 feature = "openapi",
137 schema(
138 example = json!({
139 "uk": {"name": "Пам'ять", "description": "Монтує спільні файли пам'яті в сесії."}
140 })
141 )
142 )]
143 pub localizations: std::collections::BTreeMap<String, CapabilityLocalizationInfo>,
144}
145
146#[derive(Debug, Clone, Default, Serialize, Deserialize)]
148#[cfg_attr(feature = "openapi", derive(ToSchema))]
149pub struct CapabilityLocalizationInfo {
150 #[serde(default, skip_serializing_if = "Option::is_none")]
152 pub name: Option<String>,
153 #[serde(default, skip_serializing_if = "Option::is_none")]
155 pub description: Option<String>,
156 #[serde(default, skip_serializing_if = "Option::is_none")]
158 pub config_description: Option<String>,
159 #[serde(default, skip_serializing_if = "Option::is_none")]
162 #[cfg_attr(feature = "openapi", schema(value_type = Object))]
163 pub config_overlay: Option<serde_json::Value>,
164}
165
166fn is_low_risk(r: &RiskLevel) -> bool {
167 *r == RiskLevel::Low
168}
169fn default_risk_level() -> RiskLevel {
170 RiskLevel::Low
171}
172fn is_zero_u64(v: &u64) -> bool {
173 *v == 0
174}
175
176#[allow(rustdoc::bare_urls)]
177pub fn builtin_capability_docs_slug(id: &str) -> Option<&'static str> {
181 match id {
182 "agent_instructions" => Some("agent-instructions"),
183 "skills" => Some("agent-skills"),
184 "browserless" => Some("browserless"),
185 "budgeting" => Some("budgeting"),
186 "current_time" => Some("current-time"),
187 "daytona" => Some("daytona"),
188 "fake_aws" => Some("fake-aws"),
189 "fake_crm" => Some("fake-crm"),
190 "fake_warehouse" => Some("fake-warehouse"),
191 "github_scout" => Some("github-scout"),
192 "session_file_system" => Some("file-system"),
193 "infinity_context" => Some("infinity-context"),
194 "openai_image_generation" => Some("openai-image-generation"),
195 "openai_tool_search" => Some("openai-tool-search"),
196 "tool_search" => Some("tool-search"),
197 "auto_tool_search" => Some("auto-tool-search"),
198 "platform_management" => Some("platform-management"),
199 "prompt_canary_guardrail" => Some("prompt-canary-guardrail"),
200 "self_budget" => Some("self-budget"),
201 "session_schedule" => Some("session-schedules"),
202 "session_storage" => Some("session-storage"),
203 "session_sandbox" => Some("session"),
204 "session_sql_database" => Some("sql-database"),
205 "subagents" => Some("sub-agents"),
206 "stateless_todo_list" => Some("task-management"),
207 "bashkit_shell" => Some("bashkit-shell"),
208 "web_fetch" => Some("web-fetch"),
209 _ => None,
210 }
211}
212
213impl CapabilityInfo {
214 pub fn matches_search(&self, query: &str) -> bool {
216 let q = query.to_lowercase();
217 self.name.to_lowercase().contains(&q)
218 || self.description.to_lowercase().contains(&q)
219 || self.id.as_str().to_lowercase().contains(&q)
220 || self
221 .category
222 .as_deref()
223 .is_some_and(|cat| cat.to_lowercase().contains(&q))
224 }
225
226 pub fn from_core(cap: &dyn crate::capabilities::Capability) -> Self {
228 let id_str = cap.id();
230 let is_mcp = id_str.starts_with("mcp:");
231 let is_skill =
232 id_str.starts_with("skill:") || id_str == "skills" || cap.category() == Some("Skills");
233
234 Self {
235 id: CapabilityId::new(id_str),
236 name: cap.name().to_string(),
237 description: cap.description().to_string(),
238 status: cap.status(),
239 icon: cap.icon().map(|s| s.to_string()),
240 category: cap.category().map(|s| s.to_string()),
241 system_prompt: cap.system_prompt_preview(),
242 tool_definitions: cap.tool_definitions(),
243 is_mcp,
244 is_skill,
245 is_guardrail: cap.is_guardrail(),
246 dependencies: cap.dependencies().iter().map(|s| s.to_string()).collect(),
247 features: cap.features().iter().map(|s| s.to_string()).collect(),
248 config_schema: cap.config_schema(),
249 config_ui_schema: cap.config_ui_schema(),
250 risk_level: cap.risk_level(),
251 agent_count: 0,
252 harness_count: 0,
253 docs_slug: builtin_capability_docs_slug(id_str).map(|s| s.to_string()),
254 localizations: cap
255 .localizations()
256 .into_iter()
257 .map(|entry| {
258 (
259 entry.locale.to_lowercase(),
260 CapabilityLocalizationInfo {
261 name: entry.name.map(str::to_string),
262 description: entry.description.map(str::to_string),
263 config_description: entry.config_description.map(str::to_string),
264 config_overlay: entry.config_overlay,
265 },
266 )
267 })
268 .collect(),
269 }
270 }
271}
272
273#[derive(Debug, Clone, Serialize, Deserialize)]
275#[cfg_attr(feature = "openapi", derive(ToSchema))]
276pub struct AgentCapability {
277 #[cfg_attr(feature = "openapi", schema(value_type = String))]
279 pub capability_id: CapabilityId,
280 pub position: i32,
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287
288 #[test]
289 fn test_capability_info_serialization() {
290 let cap = CapabilityInfo {
291 id: CapabilityId::new("research"),
292 name: "Research".to_string(),
293 description: "Deep research capability".to_string(),
294 status: CapabilityStatus::Available,
295 icon: Some("search".to_string()),
296 category: Some("AI".to_string()),
297 system_prompt: Some("You have research capabilities.".to_string()),
298 tool_definitions: vec![],
299 is_mcp: false,
300 is_skill: false,
301 is_guardrail: false,
302 dependencies: vec![],
303 features: vec![],
304 config_schema: None,
305 config_ui_schema: None,
306 risk_level: RiskLevel::Low,
307 agent_count: 0,
308 harness_count: 0,
309 docs_slug: None,
310 localizations: Default::default(),
311 };
312
313 let json = serde_json::to_string(&cap).unwrap();
314 assert!(json.contains("\"id\":\"research\""));
315 assert!(json.contains("\"status\":\"available\""));
316 assert!(json.contains("\"system_prompt\":\"You have research capabilities.\""));
317 assert!(!json.contains("\"is_mcp\""));
319 assert!(!json.contains("\"is_skill\""));
321 assert!(!json.contains("\"dependencies\""));
323 assert!(!json.contains("\"features\""));
325 }
326
327 #[test]
328 fn test_mcp_capability_info_serialization() {
329 let cap = CapabilityInfo {
330 id: CapabilityId::new("mcp:550e8400-e29b-41d4-a716-446655440000"),
331 name: "Microsoft Learn".to_string(),
332 description: "MCP Server for Microsoft documentation".to_string(),
333 status: CapabilityStatus::Available,
334 icon: Some("plug".to_string()),
335 category: Some("MCP Servers".to_string()),
336 system_prompt: None,
337 tool_definitions: vec![],
338 is_mcp: true,
339 is_skill: false,
340 is_guardrail: false,
341 dependencies: vec![],
342 features: vec![],
343 config_schema: None,
344 config_ui_schema: None,
345 risk_level: RiskLevel::Low,
346 agent_count: 0,
347 harness_count: 0,
348 docs_slug: None,
349 localizations: Default::default(),
350 };
351
352 let json = serde_json::to_string(&cap).unwrap();
353 assert!(json.contains("\"is_mcp\":true"));
354 }
355
356 #[test]
357 fn test_capability_with_dependencies_serialization() {
358 let cap = CapabilityInfo {
359 id: CapabilityId::new("sample_data"),
360 name: "Sample Data".to_string(),
361 description: "Sample data for testing".to_string(),
362 status: CapabilityStatus::Available,
363 icon: None,
364 category: None,
365 system_prompt: None,
366 tool_definitions: vec![],
367 is_mcp: false,
368 is_skill: false,
369 is_guardrail: false,
370 dependencies: vec!["session_file_system".to_string()],
371 features: vec![],
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 localizations: Default::default(),
379 };
380
381 let json = serde_json::to_string(&cap).unwrap();
382 assert!(json.contains("\"dependencies\":[\"session_file_system\"]"));
383 }
384
385 #[test]
386 fn test_agent_capability_serialization() {
387 let agent_cap = AgentCapability {
388 capability_id: CapabilityId::new("test_math"),
389 position: 1,
390 };
391
392 let json = serde_json::to_string(&agent_cap).unwrap();
393 assert!(json.contains("\"capability_id\":\"test_math\""));
394 assert!(json.contains("\"position\":1"));
395 }
396
397 #[test]
398 fn test_test_capabilities() {
399 assert_eq!(CapabilityId::new("test_math").to_string(), "test_math");
401 assert_eq!(
402 CapabilityId::new("test_weather").to_string(),
403 "test_weather"
404 );
405 }
406
407 #[test]
408 fn test_custom_capability_id() {
409 let custom = CapabilityId::new("my_custom_capability");
411 assert_eq!(custom.to_string(), "my_custom_capability");
412
413 let json = serde_json::to_string(&custom).unwrap();
414 assert_eq!(json, "\"my_custom_capability\"");
415 }
416
417 #[test]
418 fn test_capability_with_features_serialization() {
419 let cap = CapabilityInfo {
420 id: CapabilityId::new("session_storage"),
421 name: "Storage".to_string(),
422 description: "Storage capability".to_string(),
423 status: CapabilityStatus::Available,
424 icon: None,
425 category: None,
426 system_prompt: None,
427 tool_definitions: vec![],
428 is_mcp: false,
429 is_skill: false,
430 is_guardrail: false,
431 dependencies: vec![],
432 features: vec!["secrets".to_string(), "key_value".to_string()],
433 config_schema: None,
434 config_ui_schema: None,
435 risk_level: RiskLevel::Low,
436 agent_count: 0,
437 harness_count: 0,
438 docs_slug: None,
439 localizations: Default::default(),
440 };
441
442 let json = serde_json::to_string(&cap).unwrap();
443 assert!(json.contains("\"features\":[\"secrets\",\"key_value\"]"));
444 }
445
446 #[test]
447 fn test_from_core_populates_features() {
448 let registry = crate::capabilities::CapabilityRegistry::with_builtins();
449
450 let schedule_cap = registry.get("session_schedule").unwrap();
451 let info = CapabilityInfo::from_core(schedule_cap.as_ref());
452 assert_eq!(info.features, vec!["schedules"]);
453
454 let storage_cap = registry.get("session_storage").unwrap();
455 let info = CapabilityInfo::from_core(storage_cap.as_ref());
456 assert!(info.features.contains(&"secrets".to_string()));
457 assert!(info.features.contains(&"key_value".to_string()));
458
459 let noop_cap = registry.get("noop").unwrap();
461 let info = CapabilityInfo::from_core(noop_cap.as_ref());
462 assert!(info.features.is_empty());
463 }
464
465 #[test]
466 fn test_risk_level_serialization() {
467 let cap = CapabilityInfo {
469 id: CapabilityId::new("safe"),
470 name: "Safe".to_string(),
471 description: "Low risk".to_string(),
472 status: CapabilityStatus::Available,
473 icon: None,
474 category: None,
475 system_prompt: None,
476 tool_definitions: vec![],
477 is_mcp: false,
478 is_skill: false,
479 is_guardrail: false,
480 dependencies: vec![],
481 features: vec![],
482 config_schema: None,
483 config_ui_schema: None,
484 risk_level: RiskLevel::Low,
485 agent_count: 0,
486 harness_count: 0,
487 docs_slug: None,
488 localizations: Default::default(),
489 };
490 let json = serde_json::to_string(&cap).unwrap();
491 assert!(
492 !json.contains("\"risk_level\""),
493 "Low risk should be omitted"
494 );
495
496 let cap_high = CapabilityInfo {
498 risk_level: RiskLevel::High,
499 ..cap
500 };
501 let json = serde_json::to_string(&cap_high).unwrap();
502 assert!(json.contains("\"risk_level\":\"high\""));
503 }
504
505 #[test]
506 fn test_from_core_populates_risk_level() {
507 let registry = crate::capabilities::CapabilityRegistry::with_builtins();
508
509 let bash_cap = registry.get("bashkit_shell").unwrap();
511 let info = CapabilityInfo::from_core(bash_cap.as_ref());
512 assert_eq!(info.risk_level, RiskLevel::High);
513
514 let fetch_cap = registry.get("web_fetch").unwrap();
516 let info = CapabilityInfo::from_core(fetch_cap.as_ref());
517 assert_eq!(info.risk_level, RiskLevel::High);
518
519 let noop_cap = registry.get("noop").unwrap();
521 let info = CapabilityInfo::from_core(noop_cap.as_ref());
522 assert_eq!(info.risk_level, RiskLevel::Low);
523 }
524
525 #[test]
526 fn test_matches_search() {
527 let cap = CapabilityInfo {
528 id: CapabilityId::new("web_fetch"),
529 name: "Web Fetch".to_string(),
530 description: "Fetch content from URLs".to_string(),
531 status: CapabilityStatus::Available,
532 icon: None,
533 category: Some("Network".to_string()),
534 system_prompt: None,
535 tool_definitions: vec![],
536 is_mcp: false,
537 is_skill: false,
538 is_guardrail: false,
539 dependencies: vec![],
540 features: vec![],
541 config_schema: None,
542 config_ui_schema: None,
543 risk_level: RiskLevel::Low,
544 agent_count: 0,
545 harness_count: 0,
546 docs_slug: None,
547 localizations: Default::default(),
548 };
549
550 assert!(cap.matches_search("web"));
552 assert!(cap.matches_search("WEB FETCH"));
553 assert!(cap.matches_search("urls"));
555 assert!(cap.matches_search("web_fetch"));
557 assert!(cap.matches_search("network"));
559 assert!(!cap.matches_search("zzz_nonexistent"));
561 }
562}