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 if card.capabilities.streaming != Some(true) {
42 return Err(ServerError::UnsupportedOperation(
43 "agent does not support streaming (AgentCard.capabilities.streaming is not true)"
44 .into(),
45 ));
46 }
47 }
48 Ok(())
49 }
50
51 pub(crate) fn ensure_push_supported(&self) -> ServerResult<()> {
63 if let Some(card) = &self.agent_card {
64 if card.capabilities.push_notifications != Some(true) {
65 return Err(ServerError::PushNotSupported);
66 }
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("Test", Some(&headers))
267 }
268
269 #[tokio::test]
270 async fn missing_required_extension_is_rejected() {
271 let handler = RequestHandlerBuilder::new(DummyExecutor)
272 .with_agent_card(card_with_extensions())
273 .build()
274 .unwrap();
275 let ctx = ctx_with_extensions(&[]);
276 let err = handler
277 .ensure_required_extensions(&ctx)
278 .expect_err("client without the required extension must be rejected");
279 assert!(
280 matches!(err, ServerError::Protocol(ref e)
281 if e.code == a2a_protocol_types::error::ErrorCode::ExtensionSupportRequired
282 && e.message.contains("https://example.com/ext/required/v1")),
283 "expected ExtensionSupportRequired naming the URI, got: {err:?}"
284 );
285 }
286
287 #[tokio::test]
288 async fn declared_required_extension_is_accepted() {
289 let handler = RequestHandlerBuilder::new(DummyExecutor)
290 .with_agent_card(card_with_extensions())
291 .build()
292 .unwrap();
293 let ctx = ctx_with_extensions(&["https://example.com/ext/required/v1"]);
294 assert!(handler.ensure_required_extensions(&ctx).is_ok());
295 }
296
297 #[tokio::test]
298 async fn optional_extension_absence_is_fine() {
299 let handler = RequestHandlerBuilder::new(DummyExecutor)
302 .with_agent_card(card_with_extensions())
303 .build()
304 .unwrap();
305 let ctx = ctx_with_extensions(&[
306 "https://example.com/ext/required/v1",
307 "https://other.example/uninvolved",
308 ]);
309 assert!(handler.ensure_required_extensions(&ctx).is_ok());
310 }
311
312 #[tokio::test]
313 async fn no_card_no_required_extensions() {
314 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
315 let ctx = ctx_with_extensions(&[]);
316 assert!(handler.ensure_required_extensions(&ctx).is_ok());
317 }
318
319 #[test]
320 fn activated_extensions_header_intersects_with_declared() {
321 let handler = RequestHandlerBuilder::new(DummyExecutor)
322 .with_agent_card(card_with_extensions())
323 .build()
324 .unwrap();
325 let echoed = handler.activated_extensions_header_value(Some(
327 "https://example.com/ext/optional/v1, https://unknown.example/ext/v9",
328 ));
329 assert_eq!(
330 echoed.as_deref(),
331 Some("https://example.com/ext/optional/v1")
332 );
333 assert_eq!(handler.activated_extensions_header_value(None), None);
335 assert_eq!(
337 handler.activated_extensions_header_value(Some("https://unknown.example/ext/v9")),
338 None
339 );
340 }
341
342 #[tokio::test]
346 async fn get_task_enforces_required_extension() {
347 let handler = RequestHandlerBuilder::new(DummyExecutor)
348 .with_agent_card(card_with_extensions())
349 .build()
350 .unwrap();
351
352 let params = a2a_protocol_types::params::TaskQueryParams {
353 tenant: None,
354 id: "missing-task".into(),
355 history_length: None,
356 };
357
358 let err = handler
360 .on_get_task(params.clone(), None)
361 .await
362 .expect_err("must reject undeclared client");
363 assert!(
364 matches!(err, ServerError::Protocol(ref e)
365 if e.code == a2a_protocol_types::error::ErrorCode::ExtensionSupportRequired),
366 "expected ExtensionSupportRequired, got: {err:?}"
367 );
368
369 let mut headers = std::collections::HashMap::new();
371 headers.insert(
372 "a2a-extensions".to_owned(),
373 "https://example.com/ext/required/v1".to_owned(),
374 );
375 let err = handler
376 .on_get_task(params, Some(&headers))
377 .await
378 .expect_err("task does not exist");
379 assert!(
380 matches!(err, ServerError::TaskNotFound(_)),
381 "expected TaskNotFound once the extension is declared, got: {err:?}"
382 );
383 }
384}