everruns_builtins/
error_disclosure.rs1use crate::capabilities::{Capability, CapabilityLocalization};
23use crate::user_facing_error::ErrorDisclosure;
24use everruns_capability::CapabilityRef as AgentCapabilityConfig;
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 fn error_disclosure(&self, config: &serde_json::Value) -> Option<ErrorDisclosure> {
113 Some(
114 config
115 .get("mode")
116 .and_then(serde_json::Value::as_str)
117 .and_then(ErrorDisclosure::parse)
118 .unwrap_or_default(),
119 )
120 }
121}
122
123fn configured_mode(configs: &[AgentCapabilityConfig]) -> Option<ErrorDisclosure> {
126 let config = configs
127 .iter()
128 .find(|config| config.capability_id() == ERROR_DISCLOSURE_CAPABILITY_ID)?;
129 Some(
130 config
131 .config_value()
132 .clone()
133 .get("mode")
134 .and_then(|mode| mode.as_str())
135 .and_then(ErrorDisclosure::parse)
136 .unwrap_or_default(),
137 )
138}
139
140pub fn resolve_error_disclosure(
149 configs: &[AgentCapabilityConfig],
150 requested: Option<&str>,
151) -> ErrorDisclosure {
152 let ceiling = configured_mode(configs).unwrap_or_default();
153 match requested.and_then(ErrorDisclosure::parse) {
154 Some(requested) => requested.min(ceiling),
155 None => ceiling,
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 fn cap_config(mode: &str) -> AgentCapabilityConfig {
164 AgentCapabilityConfig::with_config(
165 ERROR_DISCLOSURE_CAPABILITY_ID,
166 serde_json::json!({ "mode": mode }),
167 )
168 }
169
170 #[test]
171 fn resolve_defaults_to_standard_without_capability() {
172 assert_eq!(
173 resolve_error_disclosure(&[], None),
174 ErrorDisclosure::Standard
175 );
176 }
177
178 #[test]
179 fn resolve_uses_capability_mode() {
180 assert_eq!(
181 resolve_error_disclosure(&[cap_config("detailed")], None),
182 ErrorDisclosure::Detailed
183 );
184 assert_eq!(
185 resolve_error_disclosure(&[cap_config("generic")], None),
186 ErrorDisclosure::Generic
187 );
188 }
189
190 #[test]
191 fn resolve_capability_without_mode_defaults_to_standard() {
192 let config = AgentCapabilityConfig::new(ERROR_DISCLOSURE_CAPABILITY_ID);
193 assert_eq!(
194 resolve_error_disclosure(&[config], None),
195 ErrorDisclosure::Standard
196 );
197 }
198
199 #[test]
200 fn controls_can_narrow_but_not_widen() {
201 assert_eq!(
203 resolve_error_disclosure(&[cap_config("detailed")], Some("generic")),
204 ErrorDisclosure::Generic
205 );
206 assert_eq!(
208 resolve_error_disclosure(&[cap_config("generic")], Some("detailed")),
209 ErrorDisclosure::Generic
210 );
211 assert_eq!(
213 resolve_error_disclosure(&[], Some("detailed")),
214 ErrorDisclosure::Standard
215 );
216 assert_eq!(
218 resolve_error_disclosure(&[cap_config("detailed")], Some("everything")),
219 ErrorDisclosure::Detailed
220 );
221 }
222
223 #[test]
224 fn validate_config_accepts_known_modes_only() {
225 let cap = ErrorDisclosureCapability;
226 assert!(cap.validate_config(&serde_json::Value::Null).is_ok());
227 assert!(cap.validate_config(&serde_json::json!({})).is_ok());
228 assert!(
229 cap.validate_config(&serde_json::json!({"mode": "generic"}))
230 .is_ok()
231 );
232 assert!(
233 cap.validate_config(&serde_json::json!({"mode": "loud"}))
234 .is_err()
235 );
236 assert!(cap.validate_config(&serde_json::json!([])).is_err());
237 }
238
239 #[test]
240 fn localizations_resolve_uk() {
241 let cap = ErrorDisclosureCapability;
242 assert_eq!(cap.localized_name(Some("uk-UA")), "Розкриття помилок");
243 }
244}