use crate::capabilities::{Capability, CapabilityLocalization};
use crate::capability_types::AgentCapabilityConfig;
use crate::user_facing_error::ErrorDisclosure;
pub const ERROR_DISCLOSURE_CAPABILITY_ID: &str = "error_disclosure";
pub struct ErrorDisclosureCapability;
impl Capability for ErrorDisclosureCapability {
fn id(&self) -> &str {
ERROR_DISCLOSURE_CAPABILITY_ID
}
fn name(&self) -> &str {
"Error Disclosure"
}
fn description(&self) -> &str {
"Controls how much detail about run-blocking errors is shown in the session: \
a single generic message (public agents), stable error codes (default), \
or full provider error details (trusted surfaces)."
}
fn config_schema(&self) -> Option<serde_json::Value> {
Some(serde_json::json!({
"type": "object",
"properties": {
"mode": {
"type": "string",
"title": "Disclosure mode",
"description": "generic: one generic localized message; standard: stable error code + fields; detailed: standard plus the underlying provider error text.",
"enum": ["generic", "standard", "detailed"],
"default": "standard"
}
}
}))
}
fn validate_config(&self, config: &serde_json::Value) -> Result<(), String> {
if config.is_null() {
return Ok(());
}
if !config.is_object() {
return Err("error_disclosure config must be an object".to_string());
}
match config.get("mode") {
None => Ok(()),
Some(serde_json::Value::String(mode)) if ErrorDisclosure::parse(mode).is_some() => {
Ok(())
}
Some(value) => Err(format!(
"mode must be one of \"generic\", \"standard\", \"detailed\", got {value}"
)),
}
}
fn localizations(&self) -> Vec<CapabilityLocalization> {
vec![
CapabilityLocalization {
locale: "en",
name: None,
description: None,
config_description: Some(
"Chooses how much error detail session viewers see when a turn fails.",
),
config_overlay: None,
},
CapabilityLocalization {
locale: "uk",
name: Some("Розкриття помилок"),
description: Some(
"Визначає, скільки деталей про блокуючі помилки показувати в сесії: \
одне загальне повідомлення (публічні агенти), стабільні коди помилок \
(за замовчуванням) або повні деталі помилки провайдера (довірені середовища).",
),
config_description: Some(
"Визначає, скільки деталей про помилку бачать користувачі сесії, коли хід завершується невдало.",
),
config_overlay: Some(serde_json::json!({
"properties": {
"mode": {
"title": "Режим розкриття",
"description": "generic: одне загальне локалізоване повідомлення; standard: стабільний код помилки з полями; detailed: standard плюс текст помилки провайдера."
}
}
})),
},
]
}
}
fn configured_mode(configs: &[AgentCapabilityConfig]) -> Option<ErrorDisclosure> {
let config = configs
.iter()
.find(|config| config.capability_id() == ERROR_DISCLOSURE_CAPABILITY_ID)?;
Some(
config
.config
.get("mode")
.and_then(|mode| mode.as_str())
.and_then(ErrorDisclosure::parse)
.unwrap_or_default(),
)
}
pub fn resolve_error_disclosure(
configs: &[AgentCapabilityConfig],
requested: Option<&str>,
) -> ErrorDisclosure {
let ceiling = configured_mode(configs).unwrap_or_default();
match requested.and_then(ErrorDisclosure::parse) {
Some(requested) => requested.min(ceiling),
None => ceiling,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cap_config(mode: &str) -> AgentCapabilityConfig {
AgentCapabilityConfig {
capability_ref: ERROR_DISCLOSURE_CAPABILITY_ID.into(),
config: serde_json::json!({ "mode": mode }),
}
}
#[test]
fn resolve_defaults_to_standard_without_capability() {
assert_eq!(
resolve_error_disclosure(&[], None),
ErrorDisclosure::Standard
);
}
#[test]
fn resolve_uses_capability_mode() {
assert_eq!(
resolve_error_disclosure(&[cap_config("detailed")], None),
ErrorDisclosure::Detailed
);
assert_eq!(
resolve_error_disclosure(&[cap_config("generic")], None),
ErrorDisclosure::Generic
);
}
#[test]
fn resolve_capability_without_mode_defaults_to_standard() {
let config = AgentCapabilityConfig::new(ERROR_DISCLOSURE_CAPABILITY_ID);
assert_eq!(
resolve_error_disclosure(&[config], None),
ErrorDisclosure::Standard
);
}
#[test]
fn controls_can_narrow_but_not_widen() {
assert_eq!(
resolve_error_disclosure(&[cap_config("detailed")], Some("generic")),
ErrorDisclosure::Generic
);
assert_eq!(
resolve_error_disclosure(&[cap_config("generic")], Some("detailed")),
ErrorDisclosure::Generic
);
assert_eq!(
resolve_error_disclosure(&[], Some("detailed")),
ErrorDisclosure::Standard
);
assert_eq!(
resolve_error_disclosure(&[cap_config("detailed")], Some("everything")),
ErrorDisclosure::Detailed
);
}
#[test]
fn validate_config_accepts_known_modes_only() {
let cap = ErrorDisclosureCapability;
assert!(cap.validate_config(&serde_json::Value::Null).is_ok());
assert!(cap.validate_config(&serde_json::json!({})).is_ok());
assert!(
cap.validate_config(&serde_json::json!({"mode": "generic"}))
.is_ok()
);
assert!(
cap.validate_config(&serde_json::json!({"mode": "loud"}))
.is_err()
);
assert!(cap.validate_config(&serde_json::json!([])).is_err());
}
#[test]
fn localizations_resolve_uk() {
let cap = ErrorDisclosureCapability;
assert_eq!(cap.localized_name(Some("uk-UA")), "Розкриття помилок");
}
}