use serde::{Deserialize, Serialize};
#[cfg(feature = "openapi")]
use utoipa::ToSchema;
use crate::capabilities::RiskLevel;
use crate::capability_types::{CapabilityId, CapabilityStatus};
use crate::tool_types::ToolDefinition;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct CapabilityInfo {
#[cfg_attr(feature = "openapi", schema(value_type = String))]
pub id: CapabilityId,
pub name: String,
pub description: String,
#[cfg_attr(feature = "openapi", schema(value_type = String))]
pub status: CapabilityStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub icon: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub category: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub system_prompt: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
#[cfg_attr(feature = "openapi", schema(value_type = Vec<Object>))]
pub tool_definitions: Vec<ToolDefinition>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub is_mcp: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub is_skill: bool,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub dependencies: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub features: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(value_type = Object))]
pub config_schema: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(value_type = Object))]
pub config_ui_schema: Option<serde_json::Value>,
#[serde(skip_serializing_if = "is_low_risk", default = "default_risk_level")]
pub risk_level: RiskLevel,
#[serde(default, skip_serializing_if = "is_zero_u64")]
pub agent_count: u64,
#[serde(default, skip_serializing_if = "is_zero_u64")]
pub harness_count: u64,
#[allow(rustdoc::bare_urls)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub docs_slug: Option<String>,
}
fn is_low_risk(r: &RiskLevel) -> bool {
*r == RiskLevel::Low
}
fn default_risk_level() -> RiskLevel {
RiskLevel::Low
}
fn is_zero_u64(v: &u64) -> bool {
*v == 0
}
#[allow(rustdoc::bare_urls)]
pub fn builtin_capability_docs_slug(id: &str) -> Option<&'static str> {
match id {
"agent_instructions" => Some("agent-instructions"),
"skills" => Some("agent-skills"),
"browserless" => Some("browserless"),
"budgeting" => Some("budgeting"),
"current_time" => Some("current-time"),
"daytona" => Some("daytona"),
"fake_aws" => Some("fake-aws"),
"fake_crm" => Some("fake-crm"),
"fake_warehouse" => Some("fake-warehouse"),
"github_scout" => Some("github-scout"),
"session_file_system" => Some("file-system"),
"infinity_context" => Some("infinity-context"),
"openai_image_generation" => Some("openai-image-generation"),
"openai_tool_search" => Some("openai-tool-search"),
"platform_management" => Some("platform-management"),
"prompt_canary_guardrail" => Some("prompt-canary-guardrail"),
"self_budget" => Some("self-budget"),
"session_schedule" => Some("session-schedules"),
"session_storage" => Some("session-storage"),
"session_sandbox" => Some("session"),
"session_sql_database" => Some("sql-database"),
"subagents" => Some("sub-agents"),
"stateless_todo_list" => Some("task-management"),
"virtual_bash" => Some("virtual-bash"),
"web_fetch" => Some("web-fetch"),
_ => None,
}
}
impl CapabilityInfo {
pub fn matches_search(&self, query: &str) -> bool {
let q = query.to_lowercase();
self.name.to_lowercase().contains(&q)
|| self.description.to_lowercase().contains(&q)
|| self.id.as_str().to_lowercase().contains(&q)
|| self
.category
.as_deref()
.is_some_and(|cat| cat.to_lowercase().contains(&q))
}
pub fn from_core(cap: &dyn crate::capabilities::Capability) -> Self {
let id_str = cap.id();
let is_mcp = id_str.starts_with("mcp:");
let is_skill =
id_str.starts_with("skill:") || id_str == "skills" || cap.category() == Some("Skills");
Self {
id: CapabilityId::new(id_str),
name: cap.name().to_string(),
description: cap.description().to_string(),
status: cap.status(),
icon: cap.icon().map(|s| s.to_string()),
category: cap.category().map(|s| s.to_string()),
system_prompt: cap.system_prompt_preview(),
tool_definitions: cap.tool_definitions(),
is_mcp,
is_skill,
dependencies: cap.dependencies().iter().map(|s| s.to_string()).collect(),
features: cap.features().iter().map(|s| s.to_string()).collect(),
config_schema: cap.config_schema(),
config_ui_schema: cap.config_ui_schema(),
risk_level: cap.risk_level(),
agent_count: 0,
harness_count: 0,
docs_slug: builtin_capability_docs_slug(id_str).map(|s| s.to_string()),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct AgentCapability {
#[cfg_attr(feature = "openapi", schema(value_type = String))]
pub capability_id: CapabilityId,
pub position: i32,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_capability_info_serialization() {
let cap = CapabilityInfo {
id: CapabilityId::new("research"),
name: "Research".to_string(),
description: "Deep research capability".to_string(),
status: CapabilityStatus::Available,
icon: Some("search".to_string()),
category: Some("AI".to_string()),
system_prompt: Some("You have research capabilities.".to_string()),
tool_definitions: vec![],
is_mcp: false,
is_skill: false,
dependencies: vec![],
features: vec![],
config_schema: None,
config_ui_schema: None,
risk_level: RiskLevel::Low,
agent_count: 0,
harness_count: 0,
docs_slug: None,
};
let json = serde_json::to_string(&cap).unwrap();
assert!(json.contains("\"id\":\"research\""));
assert!(json.contains("\"status\":\"available\""));
assert!(json.contains("\"system_prompt\":\"You have research capabilities.\""));
assert!(!json.contains("\"is_mcp\""));
assert!(!json.contains("\"is_skill\""));
assert!(!json.contains("\"dependencies\""));
assert!(!json.contains("\"features\""));
}
#[test]
fn test_mcp_capability_info_serialization() {
let cap = CapabilityInfo {
id: CapabilityId::new("mcp:550e8400-e29b-41d4-a716-446655440000"),
name: "Microsoft Learn".to_string(),
description: "MCP Server for Microsoft documentation".to_string(),
status: CapabilityStatus::Available,
icon: Some("plug".to_string()),
category: Some("MCP Servers".to_string()),
system_prompt: None,
tool_definitions: vec![],
is_mcp: true,
is_skill: false,
dependencies: vec![],
features: vec![],
config_schema: None,
config_ui_schema: None,
risk_level: RiskLevel::Low,
agent_count: 0,
harness_count: 0,
docs_slug: None,
};
let json = serde_json::to_string(&cap).unwrap();
assert!(json.contains("\"is_mcp\":true"));
}
#[test]
fn test_capability_with_dependencies_serialization() {
let cap = CapabilityInfo {
id: CapabilityId::new("sample_data"),
name: "Sample Data".to_string(),
description: "Sample data for testing".to_string(),
status: CapabilityStatus::Available,
icon: None,
category: None,
system_prompt: None,
tool_definitions: vec![],
is_mcp: false,
is_skill: false,
dependencies: vec!["session_file_system".to_string()],
features: vec![],
config_schema: None,
config_ui_schema: None,
risk_level: RiskLevel::Low,
agent_count: 0,
harness_count: 0,
docs_slug: None,
};
let json = serde_json::to_string(&cap).unwrap();
assert!(json.contains("\"dependencies\":[\"session_file_system\"]"));
}
#[test]
fn test_agent_capability_serialization() {
let agent_cap = AgentCapability {
capability_id: CapabilityId::new("test_math"),
position: 1,
};
let json = serde_json::to_string(&agent_cap).unwrap();
assert!(json.contains("\"capability_id\":\"test_math\""));
assert!(json.contains("\"position\":1"));
}
#[test]
fn test_test_capabilities() {
assert_eq!(CapabilityId::new("test_math").to_string(), "test_math");
assert_eq!(
CapabilityId::new("test_weather").to_string(),
"test_weather"
);
}
#[test]
fn test_custom_capability_id() {
let custom = CapabilityId::new("my_custom_capability");
assert_eq!(custom.to_string(), "my_custom_capability");
let json = serde_json::to_string(&custom).unwrap();
assert_eq!(json, "\"my_custom_capability\"");
}
#[test]
fn test_capability_with_features_serialization() {
let cap = CapabilityInfo {
id: CapabilityId::new("session_storage"),
name: "Storage".to_string(),
description: "Storage capability".to_string(),
status: CapabilityStatus::Available,
icon: None,
category: None,
system_prompt: None,
tool_definitions: vec![],
is_mcp: false,
is_skill: false,
dependencies: vec![],
features: vec!["secrets".to_string(), "key_value".to_string()],
config_schema: None,
config_ui_schema: None,
risk_level: RiskLevel::Low,
agent_count: 0,
harness_count: 0,
docs_slug: None,
};
let json = serde_json::to_string(&cap).unwrap();
assert!(json.contains("\"features\":[\"secrets\",\"key_value\"]"));
}
#[test]
fn test_from_core_populates_features() {
let registry = crate::capabilities::CapabilityRegistry::with_builtins();
let schedule_cap = registry.get("session_schedule").unwrap();
let info = CapabilityInfo::from_core(schedule_cap.as_ref());
assert_eq!(info.features, vec!["schedules"]);
let storage_cap = registry.get("session_storage").unwrap();
let info = CapabilityInfo::from_core(storage_cap.as_ref());
assert!(info.features.contains(&"secrets".to_string()));
assert!(info.features.contains(&"key_value".to_string()));
let noop_cap = registry.get("noop").unwrap();
let info = CapabilityInfo::from_core(noop_cap.as_ref());
assert!(info.features.is_empty());
}
#[test]
fn test_risk_level_serialization() {
let cap = CapabilityInfo {
id: CapabilityId::new("safe"),
name: "Safe".to_string(),
description: "Low risk".to_string(),
status: CapabilityStatus::Available,
icon: None,
category: None,
system_prompt: None,
tool_definitions: vec![],
is_mcp: false,
is_skill: false,
dependencies: vec![],
features: vec![],
config_schema: None,
config_ui_schema: None,
risk_level: RiskLevel::Low,
agent_count: 0,
harness_count: 0,
docs_slug: None,
};
let json = serde_json::to_string(&cap).unwrap();
assert!(
!json.contains("\"risk_level\""),
"Low risk should be omitted"
);
let cap_high = CapabilityInfo {
risk_level: RiskLevel::High,
..cap
};
let json = serde_json::to_string(&cap_high).unwrap();
assert!(json.contains("\"risk_level\":\"high\""));
}
#[test]
fn test_from_core_populates_risk_level() {
let registry = crate::capabilities::CapabilityRegistry::with_builtins();
let bash_cap = registry.get("virtual_bash").unwrap();
let info = CapabilityInfo::from_core(bash_cap.as_ref());
assert_eq!(info.risk_level, RiskLevel::High);
let fetch_cap = registry.get("web_fetch").unwrap();
let info = CapabilityInfo::from_core(fetch_cap.as_ref());
assert_eq!(info.risk_level, RiskLevel::High);
let noop_cap = registry.get("noop").unwrap();
let info = CapabilityInfo::from_core(noop_cap.as_ref());
assert_eq!(info.risk_level, RiskLevel::Low);
}
#[test]
fn test_matches_search() {
let cap = CapabilityInfo {
id: CapabilityId::new("web_fetch"),
name: "Web Fetch".to_string(),
description: "Fetch content from URLs".to_string(),
status: CapabilityStatus::Available,
icon: None,
category: Some("Network".to_string()),
system_prompt: None,
tool_definitions: vec![],
is_mcp: false,
is_skill: false,
dependencies: vec![],
features: vec![],
config_schema: None,
config_ui_schema: None,
risk_level: RiskLevel::Low,
agent_count: 0,
harness_count: 0,
docs_slug: None,
};
assert!(cap.matches_search("web"));
assert!(cap.matches_search("WEB FETCH"));
assert!(cap.matches_search("urls"));
assert!(cap.matches_search("web_fetch"));
assert!(cap.matches_search("network"));
assert!(!cap.matches_search("zzz_nonexistent"));
}
}