use everruns_core::capabilities::Capability;
use everruns_core::{AgentCapabilityConfig, CapabilityInfo};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use std::collections::HashMap;
use std::sync::Arc;
use toml::Table;
use toml::Value as TomlValue;
pub struct CapabilityCatalog {
capabilities: HashMap<String, Arc<dyn Capability>>,
}
impl CapabilityCatalog {
pub fn new() -> Self {
Self {
capabilities: HashMap::new(),
}
}
pub fn register_arc(&mut self, capability: Arc<dyn Capability>) {
self.capabilities
.insert(capability.id().to_string(), capability);
}
pub fn get(&self, id: &str) -> Option<&Arc<dyn Capability>> {
self.capabilities.get(id)
}
pub fn has(&self, id: &str) -> bool {
self.capabilities.contains_key(id)
}
pub fn ids(&self) -> Vec<String> {
let mut ids: Vec<_> = self.capabilities.keys().cloned().collect();
ids.sort();
ids
}
pub fn validate(&self, id: &str, config: &Value) -> Result<(), String> {
let cap = self
.get(id)
.ok_or_else(|| format!("unknown capability `{id}`; not registered in yolop"))?;
cap.validate_config(config)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CapabilityOverride {
#[serde(rename = "ref")]
pub capability_ref: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub append: bool,
#[serde(default)]
pub config: Value,
}
impl CapabilityOverride {
pub fn remove(capability_ref: impl Into<String>) -> Self {
Self {
capability_ref: capability_ref.into(),
enabled: Some(false),
append: false,
config: Value::Null,
}
}
pub fn is_remove(&self) -> bool {
self.enabled == Some(false)
}
}
pub fn parse_capabilities_table(table: &Table) -> Vec<CapabilityOverride> {
let Some(raw) = table.get("capabilities") else {
return Vec::new();
};
match raw {
TomlValue::Array(items) => items.iter().filter_map(parse_capability_entry).collect(),
_ => Vec::new(),
}
}
fn parse_capability_entry(value: &TomlValue) -> Option<CapabilityOverride> {
let entry = value.as_table()?;
let capability_ref = entry
.get("ref")
.or_else(|| entry.get("id"))
.and_then(TomlValue::as_str)?
.to_string();
let enabled = entry.get("enabled").and_then(TomlValue::as_bool);
let append = entry
.get("append")
.and_then(TomlValue::as_bool)
.unwrap_or(false);
let mut config_table = entry.clone();
for key in ["ref", "id", "enabled", "append"] {
config_table.remove(key);
}
let config = if config_table.is_empty() {
Value::Null
} else {
toml_value_to_json(&TomlValue::Table(config_table))
};
Some(CapabilityOverride {
capability_ref,
enabled,
append,
config,
})
}
pub fn overrides_to_json(overrides: &[CapabilityOverride]) -> Value {
Value::Array(
overrides
.iter()
.enumerate()
.map(|(index, entry)| stored_override_json(index, entry))
.collect(),
)
}
pub fn parse_override_from_json(value: &Value) -> Result<CapabilityOverride, String> {
let obj = value
.as_object()
.ok_or_else(|| "capabilities override must be a JSON object".to_string())?;
let capability_ref = obj
.get("ref")
.or_else(|| obj.get("id"))
.and_then(Value::as_str)
.ok_or_else(|| "override object requires `ref`".to_string())?
.trim()
.to_string();
if capability_ref.is_empty() {
return Err("override `ref` must not be empty".to_string());
}
let enabled = obj.get("enabled").and_then(Value::as_bool);
let append = obj.get("append").and_then(Value::as_bool).unwrap_or(false);
let mut config = obj.clone();
for key in ["ref", "id", "enabled", "append"] {
config.remove(key);
}
let config = if config.is_empty() {
Value::Null
} else {
Value::Object(config)
};
Ok(CapabilityOverride {
capability_ref,
enabled,
append,
config,
})
}
pub fn capabilities_to_toml(overrides: &[CapabilityOverride]) -> TomlValue {
let items: Vec<TomlValue> = overrides.iter().map(override_to_toml).collect();
TomlValue::Array(items)
}
fn override_to_toml(entry: &CapabilityOverride) -> TomlValue {
let mut table = Table::new();
table.insert(
"ref".to_string(),
TomlValue::String(entry.capability_ref.clone()),
);
if let Some(enabled) = entry.enabled {
table.insert("enabled".to_string(), TomlValue::Boolean(enabled));
}
if entry.append {
table.insert("append".to_string(), TomlValue::Boolean(true));
}
if let TomlValue::Table(config) = json_to_toml(&entry.config) {
for (k, v) in config {
table.insert(k, v);
}
}
TomlValue::Table(table)
}
fn toml_value_to_json(value: &TomlValue) -> Value {
match value {
TomlValue::String(s) => Value::String(s.clone()),
TomlValue::Integer(i) => json!(*i),
TomlValue::Float(f) => json!(*f),
TomlValue::Boolean(b) => Value::Bool(*b),
TomlValue::Datetime(dt) => Value::String(dt.to_string()),
TomlValue::Array(items) => Value::Array(items.iter().map(toml_value_to_json).collect()),
TomlValue::Table(table) => {
let mut map = serde_json::Map::new();
for (k, v) in table {
map.insert(k.clone(), toml_value_to_json(v));
}
Value::Object(map)
}
}
}
fn json_to_toml(value: &Value) -> TomlValue {
match value {
Value::Null => TomlValue::Table(Table::new()),
Value::Bool(b) => TomlValue::Boolean(*b),
Value::Number(n) => {
if let Some(i) = n.as_i64() {
TomlValue::Integer(i)
} else if let Some(f) = n.as_f64() {
TomlValue::Float(f)
} else {
TomlValue::String(n.to_string())
}
}
Value::String(s) => TomlValue::String(s.clone()),
Value::Array(items) => TomlValue::Array(items.iter().map(json_to_toml).collect()),
Value::Object(map) => {
let mut table = Table::new();
for (k, v) in map {
if v.is_null() {
continue;
}
table.insert(k.clone(), json_to_toml(v));
}
TomlValue::Table(table)
}
}
}
pub fn apply_capability_settings(
defaults: Vec<AgentCapabilityConfig>,
overrides: &[CapabilityOverride],
) -> Vec<AgentCapabilityConfig> {
let mut caps = defaults;
for entry in overrides {
if entry.is_remove() {
caps.retain(|cap| cap.capability_id() != entry.capability_ref);
continue;
}
if entry.append {
caps.push(AgentCapabilityConfig::with_config(
entry.capability_ref.clone(),
entry.config.clone(),
));
continue;
}
if let Some(existing) = caps
.iter_mut()
.find(|cap| cap.capability_id() == entry.capability_ref)
{
existing.config = merge_config(&existing.config, &entry.config);
} else {
caps.push(AgentCapabilityConfig::with_config(
entry.capability_ref.clone(),
entry.config.clone(),
));
}
}
caps
}
fn merge_config(default: &Value, override_config: &Value) -> Value {
if override_config.is_null() {
return default.clone();
}
match (default, override_config) {
(Value::Object(base), Value::Object(over)) => {
let mut merged = base.clone();
for (k, v) in over {
merged.insert(k.clone(), v.clone());
}
Value::Object(merged)
}
(_, over) => over.clone(),
}
}
pub fn capability_catalog_list(catalog: &CapabilityCatalog) -> Vec<Value> {
catalog
.ids()
.into_iter()
.filter_map(|id| capability_catalog_json(catalog, &id).ok())
.collect()
}
pub fn capability_catalog_json(catalog: &CapabilityCatalog, id: &str) -> Result<Value, String> {
let cap = catalog
.get(id)
.ok_or_else(|| format!("unknown capability `{id}`"))?;
let info = CapabilityInfo::from_core(cap.as_ref());
Ok(json!({
"id": id,
"name": info.name,
"description": info.description,
"category": info.category,
"config_schema": info.config_schema,
"config_ui_schema": info.config_ui_schema,
"config_description": cap.describe_schema(None),
}))
}
pub fn stored_override_json(index: usize, entry: &CapabilityOverride) -> Value {
json!({
"index": index,
"ref": entry.capability_ref,
"enabled": entry.enabled,
"append": entry.append,
"config": entry.config,
})
}
pub fn effective_harness_json(caps: &[AgentCapabilityConfig]) -> Vec<Value> {
caps.iter()
.enumerate()
.map(|(index, cap)| {
json!({
"index": index,
"ref": cap.capability_id(),
"config": cap.config,
})
})
.collect()
}
pub fn build_capability_override(
catalog: &CapabilityCatalog,
capability_ref: &str,
enabled: Option<bool>,
append: bool,
config: Option<&Value>,
) -> Result<CapabilityOverride, String> {
if !catalog.has(capability_ref) {
return Err(format!(
"unknown capability `{capability_ref}`; call `get_config key=capabilities` for registered ids"
));
}
if enabled == Some(false) {
return Ok(CapabilityOverride::remove(capability_ref));
}
let config = config.cloned().unwrap_or(Value::Null);
if !config.is_null() {
catalog.validate(capability_ref, &config)?;
}
Ok(CapabilityOverride {
capability_ref: capability_ref.to_string(),
enabled,
append,
config,
})
}
#[cfg(test)]
mod tests {
use super::*;
use everruns_core::capabilities::{MESSAGE_METADATA_CAPABILITY_ID, MessageMetadataCapability};
fn defaults() -> Vec<AgentCapabilityConfig> {
vec![
AgentCapabilityConfig::new("duckduckgo"),
AgentCapabilityConfig::with_config(
"web_fetch",
json!({ "enable_file_download": true }),
),
]
}
#[test]
fn capability_catalog_list_returns_sorted_entries() {
let mut catalog = CapabilityCatalog::new();
catalog.register_arc(Arc::new(MessageMetadataCapability));
let list = capability_catalog_list(&catalog);
assert_eq!(list.len(), 1);
assert_eq!(list[0]["id"], MESSAGE_METADATA_CAPABILITY_ID);
assert!(list[0]["config_schema"].is_object());
}
#[test]
fn apply_adds_optional_capability() {
let overrides = vec![CapabilityOverride {
capability_ref: MESSAGE_METADATA_CAPABILITY_ID.to_string(),
enabled: Some(true),
append: false,
config: json!({ "fields": ["timestamp"] }),
}];
let resolved = apply_capability_settings(defaults(), &overrides);
assert!(
resolved
.iter()
.any(|c| c.capability_id() == MESSAGE_METADATA_CAPABILITY_ID)
);
}
#[test]
fn apply_removes_all_instances_with_ref() {
let mut base = defaults();
base.push(AgentCapabilityConfig::new("duckduckgo"));
let overrides = vec![CapabilityOverride::remove("duckduckgo")];
let resolved = apply_capability_settings(base, &overrides);
assert!(!resolved.iter().any(|c| c.capability_id() == "duckduckgo"));
}
#[test]
fn apply_merges_config_into_first_match() {
let overrides = vec![CapabilityOverride {
capability_ref: "web_fetch".to_string(),
enabled: None,
append: false,
config: json!({ "enable_file_download": false }),
}];
let resolved = apply_capability_settings(defaults(), &overrides);
assert_eq!(
resolved
.iter()
.filter(|c| c.capability_id() == "web_fetch")
.count(),
1
);
let cap = resolved
.iter()
.find(|c| c.capability_id() == "web_fetch")
.expect("web_fetch still enabled");
assert_eq!(cap.config["enable_file_download"], false);
}
#[test]
fn apply_append_allows_duplicate_refs() {
let overrides = vec![
CapabilityOverride {
capability_ref: "duckduckgo".to_string(),
enabled: None,
append: true,
config: json!({}),
},
CapabilityOverride {
capability_ref: "duckduckgo".to_string(),
enabled: None,
append: true,
config: json!({}),
},
];
let resolved = apply_capability_settings(defaults(), &overrides);
assert_eq!(
resolved
.iter()
.filter(|c| c.capability_id() == "duckduckgo")
.count(),
3
);
}
#[test]
fn capabilities_array_roundtrip() {
let overrides = vec![
CapabilityOverride {
capability_ref: MESSAGE_METADATA_CAPABILITY_ID.to_string(),
enabled: Some(true),
append: false,
config: json!({ "fields": ["timestamp"] }),
},
CapabilityOverride::remove("duckduckgo"),
];
let mut table = Table::new();
table.insert("capabilities".to_string(), capabilities_to_toml(&overrides));
let parsed = parse_capabilities_table(&table);
assert_eq!(parsed, overrides);
}
#[test]
fn validate_rejects_bad_message_metadata_config() {
let mut catalog = CapabilityCatalog::new();
catalog.register_arc(Arc::new(MessageMetadataCapability));
let err = catalog
.validate(
MESSAGE_METADATA_CAPABILITY_ID,
&json!({ "fields": ["llm_model"] }),
)
.unwrap_err();
assert!(err.contains("invalid message_metadata config"), "{err}");
}
}