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