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 self.interceptors.run_before(&call_ctx).await?;
35
36 let card = match &self.agent_card {
40 Some(card) => {
41 let has_capability = card.capabilities.extended_agent_card.unwrap_or(false);
42 if !has_capability {
43 return Err(ServerError::UnsupportedOperation(
44 "agent does not support extended agent card".into(),
45 ));
46 }
47 if !self.interceptors.has_authenticator()
54 && !self.allow_unauthenticated_extended_card
55 {
56 return Err(ServerError::Protocol(
57 a2a_protocol_types::error::A2aError::new(
58 a2a_protocol_types::error::ErrorCode::InvalidRequest,
59 "extended agent card requires authentication, but no \
60 authenticating interceptor is configured; register one \
61 (e.g. BearerTokenAuthInterceptor / JwtAuthInterceptor) or \
62 opt in explicitly with \
63 RequestHandlerBuilder::allow_unauthenticated_extended_card()",
64 ),
65 ));
66 }
67 card.clone()
68 }
69 None => {
70 return Err(ServerError::Protocol(
71 a2a_protocol_types::error::A2aError::new(
72 a2a_protocol_types::error::ErrorCode::ExtendedAgentCardNotConfigured,
73 "extended agent card not configured",
74 ),
75 ));
76 }
77 };
78
79 self.interceptors.run_after(&call_ctx).await?;
80 Ok(card)
81 }
82 .await;
83
84 let elapsed = start.elapsed();
85 match &result {
86 Ok(_) => {
87 self.metrics.on_response("GetExtendedAgentCard");
88 self.metrics.on_latency("GetExtendedAgentCard", elapsed);
89 }
90 Err(e) => {
91 self.metrics
92 .on_error("GetExtendedAgentCard", e.metric_label());
93 self.metrics.on_latency("GetExtendedAgentCard", elapsed);
94 }
95 }
96 result
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use a2a_protocol_types::agent_card::{AgentCapabilities, AgentCard, AgentInterface};
103
104 use crate::agent_executor;
105 use crate::builder::RequestHandlerBuilder;
106 use crate::error::ServerError;
107
108 struct DummyExecutor;
109 agent_executor!(DummyExecutor, |_ctx, _queue| async { Ok(()) });
110
111 fn make_agent_card() -> AgentCard {
112 AgentCard {
113 url: None,
114 name: "Test Agent".into(),
115 description: "A test agent".into(),
116 version: "1.0.0".into(),
117 supported_interfaces: vec![AgentInterface {
118 url: "http://localhost:8080".into(),
119 protocol_binding: "JSONRPC".into(),
120 protocol_version: "1.0.0".into(),
121 tenant: None,
122 }],
123 default_input_modes: vec![],
124 default_output_modes: vec![],
125 skills: vec![],
126 capabilities: AgentCapabilities::none(),
127 provider: None,
128 icon_url: None,
129 documentation_url: None,
130 security_schemes: None,
131 security_requirements: None,
132 signatures: None,
133 }
134 }
135
136 #[tokio::test]
137 async fn get_extended_agent_card_no_card_returns_not_configured_error() {
138 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
139 let result = handler.on_get_extended_agent_card(None).await;
140 assert!(
141 matches!(result, Err(ServerError::Protocol(ref e)) if e.code == a2a_protocol_types::error::ErrorCode::ExtendedAgentCardNotConfigured),
142 "expected ExtendedAgentCardNotConfigured when no card configured, got: {result:?}"
143 );
144 }
145
146 #[tokio::test]
147 async fn get_extended_agent_card_without_capability_returns_unsupported() {
148 let card = make_agent_card(); let handler = RequestHandlerBuilder::new(DummyExecutor)
150 .with_agent_card(card)
151 .build()
152 .unwrap();
153 let result = handler.on_get_extended_agent_card(None).await;
154 assert!(
155 matches!(result, Err(ServerError::UnsupportedOperation(_))),
156 "expected UnsupportedOperation when capability is false, got: {result:?}"
157 );
158 }
159
160 #[tokio::test]
164 async fn get_extended_agent_card_without_authenticator_is_refused() {
165 let mut card = make_agent_card();
166 card.capabilities = AgentCapabilities::none().with_extended_agent_card(true);
167 let handler = RequestHandlerBuilder::new(DummyExecutor)
168 .with_agent_card(card)
169 .build()
170 .unwrap();
171 let result = handler.on_get_extended_agent_card(None).await;
172 assert!(
173 matches!(result, Err(ServerError::Protocol(ref e))
174 if e.message.contains("requires authentication")),
175 "expected an authentication-required refusal, got: {result:?}"
176 );
177 }
178
179 #[tokio::test]
181 async fn get_extended_agent_card_with_optout_returns_ok() {
182 let mut card = make_agent_card();
183 card.capabilities = AgentCapabilities::none().with_extended_agent_card(true);
184 let handler = RequestHandlerBuilder::new(DummyExecutor)
185 .with_agent_card(card)
186 .allow_unauthenticated_extended_card()
187 .build()
188 .unwrap();
189 let result = handler.on_get_extended_agent_card(None).await;
190 assert!(
191 result.is_ok(),
192 "expected Ok with explicit unauthenticated opt-in, got: {result:?}"
193 );
194 assert_eq!(result.unwrap().name, "Test Agent");
195 }
196
197 #[tokio::test]
200 async fn get_extended_agent_card_with_authenticator_gates_on_credentials() {
201 let mut card = make_agent_card();
202 card.capabilities = AgentCapabilities::none().with_extended_agent_card(true);
203 let handler = RequestHandlerBuilder::new(DummyExecutor)
204 .with_agent_card(card)
205 .with_interceptor(crate::auth::BearerTokenAuthInterceptor::new(["sekret"]))
206 .build()
207 .unwrap();
208
209 let anon = handler.on_get_extended_agent_card(None).await;
211 assert!(
212 anon.is_err(),
213 "unauthenticated request must be rejected, got: {anon:?}"
214 );
215
216 let mut headers = std::collections::HashMap::new();
218 headers.insert("authorization".to_owned(), "Bearer sekret".to_owned());
219 let authed = handler.on_get_extended_agent_card(Some(&headers)).await;
220 assert!(
221 authed.is_ok(),
222 "authenticated request must be served, got: {authed:?}"
223 );
224 assert_eq!(authed.unwrap().name, "Test Agent");
225 }
226
227 #[tokio::test]
228 async fn get_extended_agent_card_error_path_records_metrics() {
229 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
231 let result = handler.on_get_extended_agent_card(None).await;
232 assert!(
233 result.is_err(),
234 "expected error for error metrics path, got: {result:?}"
235 );
236 }
237}