a2a_protocol_server/handler/
capability.rs1use crate::error::{ServerError, ServerResult};
25
26use super::RequestHandler;
27
28impl RequestHandler {
29 pub(crate) fn ensure_streaming_supported(&self) -> ServerResult<()> {
40 if let Some(card) = &self.agent_card
41 && card.capabilities.streaming != Some(true)
42 {
43 return Err(ServerError::UnsupportedOperation(
44 "agent does not support streaming (AgentCard.capabilities.streaming is not true)"
45 .into(),
46 ));
47 }
48 Ok(())
49 }
50
51 pub(crate) fn ensure_push_supported(&self) -> ServerResult<()> {
63 if let Some(card) = &self.agent_card
64 && card.capabilities.push_notifications != Some(true)
65 {
66 return Err(ServerError::PushNotSupported);
67 }
68 Ok(())
69 }
70
71 pub(crate) fn ensure_required_extensions(
85 &self,
86 ctx: &crate::call_context::CallContext,
87 ) -> ServerResult<()> {
88 if self.required_extensions.is_empty() {
89 return Ok(());
90 }
91 let declared = ctx.extensions();
92 let missing: Vec<&str> = self
93 .required_extensions
94 .iter()
95 .filter(|uri| !declared.iter().any(|d| d == *uri))
96 .map(String::as_str)
97 .collect();
98 if missing.is_empty() {
99 Ok(())
100 } else {
101 Err(ServerError::Protocol(
102 a2a_protocol_types::error::A2aError::extension_support_required(format!(
103 "this agent requires extension support the client did not declare \
104 (send them in the A2A-Extensions header): {}",
105 missing.join(", ")
106 )),
107 ))
108 }
109 }
110
111 #[must_use]
117 pub fn activated_extensions(
118 &self,
119 headers: &std::collections::HashMap<String, String>,
120 ) -> Vec<String> {
121 if self.declared_extensions.is_empty() {
122 return Vec::new();
123 }
124 super::helpers::parse_extensions_header(headers)
125 .into_iter()
126 .filter(|uri| self.declared_extensions.iter().any(|d| d == uri))
127 .collect()
128 }
129
130 pub(crate) fn activated_extensions_header_value(&self, raw: Option<&str>) -> Option<String> {
134 let raw = raw?;
135 if self.declared_extensions.is_empty() {
136 return None;
137 }
138 let activated: Vec<&str> = raw
139 .split(',')
140 .map(str::trim)
141 .filter(|s| !s.is_empty())
142 .filter(|uri| self.declared_extensions.iter().any(|d| d == uri))
143 .collect();
144 if activated.is_empty() {
145 None
146 } else {
147 Some(activated.join(", "))
148 }
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use a2a_protocol_types::agent_card::{AgentCapabilities, AgentCard, AgentInterface};
155
156 use crate::agent_executor;
157 use crate::builder::RequestHandlerBuilder;
158 use crate::error::ServerError;
159
160 struct DummyExecutor;
161 agent_executor!(DummyExecutor, |_ctx, _queue| async { Ok(()) });
162
163 fn card_with(caps: AgentCapabilities) -> AgentCard {
164 AgentCard {
165 url: None,
166 name: "Test Agent".into(),
167 description: "A test agent".into(),
168 version: "1.0.0".into(),
169 supported_interfaces: vec![AgentInterface {
170 url: "http://localhost:8080".into(),
171 protocol_binding: "JSONRPC".into(),
172 protocol_version: "1.0.0".into(),
173 tenant: None,
174 }],
175 default_input_modes: vec![],
176 default_output_modes: vec![],
177 skills: vec![],
178 capabilities: caps,
179 provider: None,
180 icon_url: None,
181 documentation_url: None,
182 security_schemes: None,
183 security_requirements: None,
184 signatures: None,
185 }
186 }
187
188 #[test]
189 fn no_card_allows_streaming_and_push() {
190 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
191 assert!(handler.ensure_streaming_supported().is_ok());
192 assert!(handler.ensure_push_supported().is_ok());
193 }
194
195 #[test]
196 fn card_without_streaming_rejects_streaming() {
197 let handler = RequestHandlerBuilder::new(DummyExecutor)
198 .with_agent_card(card_with(AgentCapabilities::none()))
199 .build()
200 .unwrap();
201 assert!(matches!(
202 handler.ensure_streaming_supported(),
203 Err(ServerError::UnsupportedOperation(_))
204 ));
205 }
206
207 #[test]
208 fn card_with_streaming_allows_streaming() {
209 let handler = RequestHandlerBuilder::new(DummyExecutor)
210 .with_agent_card(card_with(AgentCapabilities::none().with_streaming(true)))
211 .build()
212 .unwrap();
213 assert!(handler.ensure_streaming_supported().is_ok());
214 }
215
216 #[test]
217 fn card_without_push_rejects_push() {
218 let handler = RequestHandlerBuilder::new(DummyExecutor)
219 .with_agent_card(card_with(AgentCapabilities::none()))
220 .build()
221 .unwrap();
222 assert!(matches!(
223 handler.ensure_push_supported(),
224 Err(ServerError::PushNotSupported)
225 ));
226 }
227
228 #[test]
229 fn card_with_push_allows_push() {
230 let handler = RequestHandlerBuilder::new(DummyExecutor)
231 .with_agent_card(card_with(
232 AgentCapabilities::none().with_push_notifications(true),
233 ))
234 .build()
235 .unwrap();
236 assert!(handler.ensure_push_supported().is_ok());
237 }
238
239 fn card_with_extensions() -> AgentCard {
242 use a2a_protocol_types::extensions::AgentExtension;
243 let mut caps = AgentCapabilities::none();
244 caps.extensions = Some(vec![
245 AgentExtension {
246 uri: "https://example.com/ext/required/v1".into(),
247 description: None,
248 required: Some(true),
249 params: None,
250 },
251 AgentExtension {
252 uri: "https://example.com/ext/optional/v1".into(),
253 description: None,
254 required: Some(false),
255 params: None,
256 },
257 ]);
258 card_with(caps)
259 }
260
261 fn ctx_with_extensions(exts: &[&str]) -> crate::call_context::CallContext {
262 let mut headers = std::collections::HashMap::new();
263 if !exts.is_empty() {
264 headers.insert("a2a-extensions".to_owned(), exts.join(","));
265 }
266 crate::handler::helpers::build_call_context(
267 "Test",
268 Some(&headers),
269 crate::handler::InboundTracePolicy::Continue,
270 )
271 }
272
273 #[tokio::test]
274 async fn missing_required_extension_is_rejected() {
275 let handler = RequestHandlerBuilder::new(DummyExecutor)
276 .with_agent_card(card_with_extensions())
277 .build()
278 .unwrap();
279 let ctx = ctx_with_extensions(&[]);
280 let err = handler
281 .ensure_required_extensions(&ctx)
282 .expect_err("client without the required extension must be rejected");
283 assert!(
284 matches!(err, ServerError::Protocol(ref e)
285 if e.code == a2a_protocol_types::error::ErrorCode::ExtensionSupportRequired
286 && e.message.contains("https://example.com/ext/required/v1")),
287 "expected ExtensionSupportRequired naming the URI, got: {err:?}"
288 );
289 }
290
291 #[tokio::test]
292 async fn declared_required_extension_is_accepted() {
293 let handler = RequestHandlerBuilder::new(DummyExecutor)
294 .with_agent_card(card_with_extensions())
295 .build()
296 .unwrap();
297 let ctx = ctx_with_extensions(&["https://example.com/ext/required/v1"]);
298 assert!(handler.ensure_required_extensions(&ctx).is_ok());
299 }
300
301 #[tokio::test]
302 async fn optional_extension_absence_is_fine() {
303 let handler = RequestHandlerBuilder::new(DummyExecutor)
306 .with_agent_card(card_with_extensions())
307 .build()
308 .unwrap();
309 let ctx = ctx_with_extensions(&[
310 "https://example.com/ext/required/v1",
311 "https://other.example/uninvolved",
312 ]);
313 assert!(handler.ensure_required_extensions(&ctx).is_ok());
314 }
315
316 #[tokio::test]
317 async fn no_card_no_required_extensions() {
318 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
319 let ctx = ctx_with_extensions(&[]);
320 assert!(handler.ensure_required_extensions(&ctx).is_ok());
321 }
322
323 #[test]
324 fn activated_extensions_header_intersects_with_declared() {
325 let handler = RequestHandlerBuilder::new(DummyExecutor)
326 .with_agent_card(card_with_extensions())
327 .build()
328 .unwrap();
329 let echoed = handler.activated_extensions_header_value(Some(
331 "https://example.com/ext/optional/v1, https://unknown.example/ext/v9",
332 ));
333 assert_eq!(
334 echoed.as_deref(),
335 Some("https://example.com/ext/optional/v1")
336 );
337 assert_eq!(handler.activated_extensions_header_value(None), None);
339 assert_eq!(
341 handler.activated_extensions_header_value(Some("https://unknown.example/ext/v9")),
342 None
343 );
344 }
345
346 #[test]
347 fn activated_extensions_returns_the_declared_intersection() {
348 let handler = RequestHandlerBuilder::new(DummyExecutor)
356 .with_agent_card(card_with_extensions())
357 .build()
358 .unwrap();
359
360 let mut headers = std::collections::HashMap::new();
361 headers.insert(
362 "a2a-extensions".to_owned(),
363 "https://example.com/ext/optional/v1,https://unknown.example/ext/v9".to_owned(),
364 );
365 assert_eq!(
369 handler.activated_extensions(&headers),
370 vec!["https://example.com/ext/optional/v1".to_owned()]
371 );
372
373 assert_eq!(
375 handler.activated_extensions(&std::collections::HashMap::new()),
376 [] as [std::string::String; 0]
377 );
378 }
379
380 #[tokio::test]
384 async fn get_task_enforces_required_extension() {
385 let handler = RequestHandlerBuilder::new(DummyExecutor)
386 .with_agent_card(card_with_extensions())
387 .build()
388 .unwrap();
389
390 let params = a2a_protocol_types::params::TaskQueryParams {
391 tenant: None,
392 id: "missing-task".into(),
393 history_length: None,
394 };
395
396 let err = handler
398 .on_get_task(params.clone(), None)
399 .await
400 .expect_err("must reject undeclared client");
401 assert!(
402 matches!(err, ServerError::Protocol(ref e)
403 if e.code == a2a_protocol_types::error::ErrorCode::ExtensionSupportRequired),
404 "expected ExtensionSupportRequired, got: {err:?}"
405 );
406
407 let mut headers = std::collections::HashMap::new();
409 headers.insert(
410 "a2a-extensions".to_owned(),
411 "https://example.com/ext/required/v1".to_owned(),
412 );
413 let err = handler
414 .on_get_task(params, Some(&headers))
415 .await
416 .expect_err("task does not exist");
417 assert!(
418 matches!(err, ServerError::TaskNotFound(_)),
419 "expected TaskNotFound once the extension is declared, got: {err:?}"
420 );
421 }
422}