everruns_core/capabilities/
error_disclosure.rs1use crate::capabilities::{Capability, CapabilityLocalization};
23use crate::capability_types::AgentCapabilityConfig;
24use crate::user_facing_error::ErrorDisclosure;
25
26pub const ERROR_DISCLOSURE_CAPABILITY_ID: &str = "error_disclosure";
27
28pub struct ErrorDisclosureCapability;
29
30impl Capability for ErrorDisclosureCapability {
31 fn id(&self) -> &str {
32 ERROR_DISCLOSURE_CAPABILITY_ID
33 }
34
35 fn name(&self) -> &str {
36 "Error Disclosure"
37 }
38
39 fn description(&self) -> &str {
40 "Controls how much detail about run-blocking errors is shown in the session: \
41 a single generic message (public agents), stable error codes (default), \
42 or full provider error details (trusted surfaces)."
43 }
44
45 fn config_schema(&self) -> Option<serde_json::Value> {
46 Some(serde_json::json!({
47 "type": "object",
48 "properties": {
49 "mode": {
50 "type": "string",
51 "title": "Disclosure mode",
52 "description": "generic: one generic localized message; standard: stable error code + fields; detailed: standard plus the underlying provider error text.",
53 "enum": ["generic", "standard", "detailed"],
54 "default": "standard"
55 }
56 }
57 }))
58 }
59
60 fn validate_config(&self, config: &serde_json::Value) -> Result<(), String> {
61 if config.is_null() {
62 return Ok(());
63 }
64 if !config.is_object() {
65 return Err("error_disclosure config must be an object".to_string());
66 }
67 match config.get("mode") {
68 None => Ok(()),
69 Some(serde_json::Value::String(mode)) if ErrorDisclosure::parse(mode).is_some() => {
70 Ok(())
71 }
72 Some(value) => Err(format!(
73 "mode must be one of \"generic\", \"standard\", \"detailed\", got {value}"
74 )),
75 }
76 }
77
78 fn localizations(&self) -> Vec<CapabilityLocalization> {
79 vec![
80 CapabilityLocalization {
81 locale: "en",
82 name: None,
83 description: None,
84 config_description: Some(
85 "Chooses how much error detail session viewers see when a turn fails.",
86 ),
87 config_overlay: None,
88 },
89 CapabilityLocalization {
90 locale: "uk",
91 name: Some("Розкриття помилок"),
92 description: Some(
93 "Визначає, скільки деталей про блокуючі помилки показувати в сесії: \
94 одне загальне повідомлення (публічні агенти), стабільні коди помилок \
95 (за замовчуванням) або повні деталі помилки провайдера (довірені середовища).",
96 ),
97 config_description: Some(
98 "Визначає, скільки деталей про помилку бачать користувачі сесії, коли хід завершується невдало.",
99 ),
100 config_overlay: Some(serde_json::json!({
101 "properties": {
102 "mode": {
103 "title": "Режим розкриття",
104 "description": "generic: одне загальне локалізоване повідомлення; standard: стабільний код помилки з полями; detailed: standard плюс текст помилки провайдера."
105 }
106 }
107 })),
108 },
109 ]
110 }
111}
112
113fn configured_mode(configs: &[AgentCapabilityConfig]) -> Option<ErrorDisclosure> {
116 let config = configs
117 .iter()
118 .find(|config| config.capability_id() == ERROR_DISCLOSURE_CAPABILITY_ID)?;
119 Some(
120 config
121 .config
122 .get("mode")
123 .and_then(|mode| mode.as_str())
124 .and_then(ErrorDisclosure::parse)
125 .unwrap_or_default(),
126 )
127}
128
129pub fn resolve_error_disclosure(
138 configs: &[AgentCapabilityConfig],
139 requested: Option<&str>,
140) -> ErrorDisclosure {
141 let ceiling = configured_mode(configs).unwrap_or_default();
142 match requested.and_then(ErrorDisclosure::parse) {
143 Some(requested) => requested.min(ceiling),
144 None => ceiling,
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 fn cap_config(mode: &str) -> AgentCapabilityConfig {
153 AgentCapabilityConfig {
154 capability_ref: ERROR_DISCLOSURE_CAPABILITY_ID.into(),
155 config: serde_json::json!({ "mode": mode }),
156 }
157 }
158
159 #[test]
160 fn resolve_defaults_to_standard_without_capability() {
161 assert_eq!(
162 resolve_error_disclosure(&[], None),
163 ErrorDisclosure::Standard
164 );
165 }
166
167 #[test]
168 fn resolve_uses_capability_mode() {
169 assert_eq!(
170 resolve_error_disclosure(&[cap_config("detailed")], None),
171 ErrorDisclosure::Detailed
172 );
173 assert_eq!(
174 resolve_error_disclosure(&[cap_config("generic")], None),
175 ErrorDisclosure::Generic
176 );
177 }
178
179 #[test]
180 fn resolve_capability_without_mode_defaults_to_standard() {
181 let config = AgentCapabilityConfig::new(ERROR_DISCLOSURE_CAPABILITY_ID);
182 assert_eq!(
183 resolve_error_disclosure(&[config], None),
184 ErrorDisclosure::Standard
185 );
186 }
187
188 #[test]
189 fn controls_can_narrow_but_not_widen() {
190 assert_eq!(
192 resolve_error_disclosure(&[cap_config("detailed")], Some("generic")),
193 ErrorDisclosure::Generic
194 );
195 assert_eq!(
197 resolve_error_disclosure(&[cap_config("generic")], Some("detailed")),
198 ErrorDisclosure::Generic
199 );
200 assert_eq!(
202 resolve_error_disclosure(&[], Some("detailed")),
203 ErrorDisclosure::Standard
204 );
205 assert_eq!(
207 resolve_error_disclosure(&[cap_config("detailed")], Some("everything")),
208 ErrorDisclosure::Detailed
209 );
210 }
211
212 #[test]
213 fn validate_config_accepts_known_modes_only() {
214 let cap = ErrorDisclosureCapability;
215 assert!(cap.validate_config(&serde_json::Value::Null).is_ok());
216 assert!(cap.validate_config(&serde_json::json!({})).is_ok());
217 assert!(
218 cap.validate_config(&serde_json::json!({"mode": "generic"}))
219 .is_ok()
220 );
221 assert!(
222 cap.validate_config(&serde_json::json!({"mode": "loud"}))
223 .is_err()
224 );
225 assert!(cap.validate_config(&serde_json::json!([])).is_err());
226 }
227
228 #[test]
229 fn localizations_resolve_uk() {
230 let cap = ErrorDisclosureCapability;
231 assert_eq!(cap.localized_name(Some("uk-UA")), "Розкриття помилок");
232 }
233}