a2a_protocol_server/handler/lifecycle/
extended_card.rs1use std::collections::HashMap;
9use std::time::Instant;
10
11use a2a_protocol_types::agent_card::AgentCard;
12
13use crate::error::{ServerError, ServerResult};
14
15use super::super::RequestHandler;
16use super::super::helpers::build_call_context;
17
18impl RequestHandler {
19 pub async fn on_get_extended_agent_card(
25 &self,
26 headers: Option<&HashMap<String, String>>,
27 ) -> ServerResult<AgentCard> {
28 let start = Instant::now();
29 self.metrics.on_request("GetExtendedAgentCard");
30
31 let result: ServerResult<_> = async {
32 let call_ctx =
33 build_call_context("GetExtendedAgentCard", headers, self.inbound_trace_policy);
34 let mut call = self.interceptors.begin(&call_ctx);
35 let result = async {
36 call.before().await?;
37
38 self.extended_card_for_this_call()
39 }
40 .await;
41 call.finish(result).await
42 }
43 .await;
44
45 let elapsed = start.elapsed();
46 match &result {
47 Ok(_) => {
48 self.metrics.on_response("GetExtendedAgentCard");
49 self.metrics.on_latency("GetExtendedAgentCard", elapsed);
50 }
51 Err(e) => {
52 self.metrics
53 .on_error("GetExtendedAgentCard", e.metric_label());
54 self.metrics.on_latency("GetExtendedAgentCard", elapsed);
55 }
56 }
57 result
58 }
59
60 fn extended_card_for_this_call(&self) -> ServerResult<AgentCard> {
62 use a2a_protocol_types::error::{A2aError, ErrorCode};
63
64 let Some(card) = &self.agent_card else {
68 return Err(ServerError::Protocol(A2aError::new(
69 ErrorCode::ExtendedAgentCardNotConfigured,
70 "extended agent card not configured",
71 )));
72 };
73 if !card.capabilities.extended_agent_card.unwrap_or(false) {
74 return Err(ServerError::UnsupportedOperation(
75 "agent does not support extended agent card".into(),
76 ));
77 }
78 if !self.interceptors.has_authenticator() && !self.allow_unauthenticated_extended_card {
85 return Err(ServerError::Protocol(A2aError::new(
86 ErrorCode::InvalidRequest,
87 "extended agent card requires authentication, but no \
88 authenticating interceptor is configured; register one \
89 (e.g. BearerTokenAuthInterceptor / JwtAuthInterceptor) or \
90 opt in explicitly with \
91 RequestHandlerBuilder::allow_unauthenticated_extended_card()",
92 )));
93 }
94 Ok(card.clone())
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use a2a_protocol_types::agent_card::{AgentCapabilities, AgentCard, AgentInterface};
101
102 use crate::agent_executor;
103 use crate::builder::RequestHandlerBuilder;
104 use crate::error::ServerError;
105
106 struct DummyExecutor;
107 agent_executor!(DummyExecutor, |_ctx, _queue| async { Ok(()) });
108
109 fn make_agent_card() -> AgentCard {
110 AgentCard {
111 url: None,
112 name: "Test Agent".into(),
113 description: "A test agent".into(),
114 version: "1.0.0".into(),
115 supported_interfaces: vec![AgentInterface {
116 url: "http://localhost:8080".into(),
117 protocol_binding: "JSONRPC".into(),
118 protocol_version: "1.0.0".into(),
119 tenant: None,
120 }],
121 default_input_modes: vec![],
122 default_output_modes: vec![],
123 skills: vec![],
124 capabilities: AgentCapabilities::none(),
125 provider: None,
126 icon_url: None,
127 documentation_url: None,
128 security_schemes: None,
129 security_requirements: None,
130 signatures: None,
131 }
132 }
133
134 #[tokio::test]
135 async fn get_extended_agent_card_no_card_returns_not_configured_error() {
136 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
137 let result = handler.on_get_extended_agent_card(None).await;
138 assert!(
139 matches!(result, Err(ServerError::Protocol(ref e)) if e.code == a2a_protocol_types::error::ErrorCode::ExtendedAgentCardNotConfigured),
140 "expected ExtendedAgentCardNotConfigured when no card configured, got: {result:?}"
141 );
142 }
143
144 #[tokio::test]
145 async fn get_extended_agent_card_without_capability_returns_unsupported() {
146 let card = make_agent_card(); let handler = RequestHandlerBuilder::new(DummyExecutor)
148 .with_agent_card(card)
149 .build()
150 .unwrap();
151 let result = handler.on_get_extended_agent_card(None).await;
152 assert!(
153 matches!(result, Err(ServerError::UnsupportedOperation(_))),
154 "expected UnsupportedOperation when capability is false, got: {result:?}"
155 );
156 }
157
158 #[tokio::test]
162 async fn get_extended_agent_card_without_authenticator_is_refused() {
163 let mut card = make_agent_card();
164 card.capabilities = AgentCapabilities::none().with_extended_agent_card(true);
165 let handler = RequestHandlerBuilder::new(DummyExecutor)
166 .with_agent_card(card)
167 .build()
168 .unwrap();
169 let result = handler.on_get_extended_agent_card(None).await;
170 assert!(
171 matches!(result, Err(ServerError::Protocol(ref e))
172 if e.message.contains("requires authentication")),
173 "expected an authentication-required refusal, got: {result:?}"
174 );
175 }
176
177 #[tokio::test]
179 async fn get_extended_agent_card_with_optout_returns_ok() {
180 let mut card = make_agent_card();
181 card.capabilities = AgentCapabilities::none().with_extended_agent_card(true);
182 let handler = RequestHandlerBuilder::new(DummyExecutor)
183 .with_agent_card(card)
184 .allow_unauthenticated_extended_card()
185 .build()
186 .unwrap();
187 let result = handler.on_get_extended_agent_card(None).await;
188 assert!(
189 result.is_ok(),
190 "expected Ok with explicit unauthenticated opt-in, got: {result:?}"
191 );
192 assert_eq!(result.unwrap().name, "Test Agent");
193 }
194
195 #[tokio::test]
198 async fn get_extended_agent_card_with_authenticator_gates_on_credentials() {
199 let mut card = make_agent_card();
200 card.capabilities = AgentCapabilities::none().with_extended_agent_card(true);
201 let handler = RequestHandlerBuilder::new(DummyExecutor)
202 .with_agent_card(card)
203 .with_interceptor(crate::auth::BearerTokenAuthInterceptor::new(["sekret"]))
204 .build()
205 .unwrap();
206
207 let anon = handler.on_get_extended_agent_card(None).await;
209 assert!(
210 anon.is_err(),
211 "unauthenticated request must be rejected, got: {anon:?}"
212 );
213
214 let mut headers = std::collections::HashMap::new();
216 headers.insert("authorization".to_owned(), "Bearer sekret".to_owned());
217 let authed = handler.on_get_extended_agent_card(Some(&headers)).await;
218 assert!(
219 authed.is_ok(),
220 "authenticated request must be served, got: {authed:?}"
221 );
222 assert_eq!(authed.unwrap().name, "Test Agent");
223 }
224
225 #[tokio::test]
226 async fn get_extended_agent_card_error_path_records_metrics() {
227 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
229 let result = handler.on_get_extended_agent_card(None).await;
230 assert!(
231 result.is_err(),
232 "expected error for error metrics path, got: {result:?}"
233 );
234 }
235}