use std::collections::HashMap;
use std::time::Instant;
use a2a_protocol_types::agent_card::AgentCard;
use crate::error::{ServerError, ServerResult};
use super::super::helpers::build_call_context;
use super::super::RequestHandler;
impl RequestHandler {
pub async fn on_get_extended_agent_card(
&self,
headers: Option<&HashMap<String, String>>,
) -> ServerResult<AgentCard> {
let start = Instant::now();
self.metrics.on_request("GetExtendedAgentCard");
let result: ServerResult<_> = async {
let call_ctx = build_call_context("GetExtendedAgentCard", headers);
self.interceptors.run_before(&call_ctx).await?;
let card = match &self.agent_card {
Some(card) => {
let has_capability = card.capabilities.extended_agent_card.unwrap_or(false);
if !has_capability {
return Err(ServerError::UnsupportedOperation(
"agent does not support extended agent card".into(),
));
}
if !self.interceptors.has_authenticator()
&& !self.allow_unauthenticated_extended_card
{
return Err(ServerError::Protocol(
a2a_protocol_types::error::A2aError::new(
a2a_protocol_types::error::ErrorCode::InvalidRequest,
"extended agent card requires authentication, but no \
authenticating interceptor is configured; register one \
(e.g. BearerTokenAuthInterceptor / JwtAuthInterceptor) or \
opt in explicitly with \
RequestHandlerBuilder::allow_unauthenticated_extended_card()",
),
));
}
card.clone()
}
None => {
return Err(ServerError::Protocol(
a2a_protocol_types::error::A2aError::new(
a2a_protocol_types::error::ErrorCode::ExtendedAgentCardNotConfigured,
"extended agent card not configured",
),
));
}
};
self.interceptors.run_after(&call_ctx).await?;
Ok(card)
}
.await;
let elapsed = start.elapsed();
match &result {
Ok(_) => {
self.metrics.on_response("GetExtendedAgentCard");
self.metrics.on_latency("GetExtendedAgentCard", elapsed);
}
Err(e) => {
self.metrics
.on_error("GetExtendedAgentCard", e.metric_label());
self.metrics.on_latency("GetExtendedAgentCard", elapsed);
}
}
result
}
}
#[cfg(test)]
mod tests {
use a2a_protocol_types::agent_card::{AgentCapabilities, AgentCard, AgentInterface};
use crate::agent_executor;
use crate::builder::RequestHandlerBuilder;
use crate::error::ServerError;
struct DummyExecutor;
agent_executor!(DummyExecutor, |_ctx, _queue| async { Ok(()) });
fn make_agent_card() -> AgentCard {
AgentCard {
url: None,
name: "Test Agent".into(),
description: "A test agent".into(),
version: "1.0.0".into(),
supported_interfaces: vec![AgentInterface {
url: "http://localhost:8080".into(),
protocol_binding: "JSONRPC".into(),
protocol_version: "1.0.0".into(),
tenant: None,
}],
default_input_modes: vec![],
default_output_modes: vec![],
skills: vec![],
capabilities: AgentCapabilities::none(),
provider: None,
icon_url: None,
documentation_url: None,
security_schemes: None,
security_requirements: None,
signatures: None,
}
}
#[tokio::test]
async fn get_extended_agent_card_no_card_returns_not_configured_error() {
let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
let result = handler.on_get_extended_agent_card(None).await;
assert!(
matches!(result, Err(ServerError::Protocol(ref e)) if e.code == a2a_protocol_types::error::ErrorCode::ExtendedAgentCardNotConfigured),
"expected ExtendedAgentCardNotConfigured when no card configured, got: {result:?}"
);
}
#[tokio::test]
async fn get_extended_agent_card_without_capability_returns_unsupported() {
let card = make_agent_card(); let handler = RequestHandlerBuilder::new(DummyExecutor)
.with_agent_card(card)
.build()
.unwrap();
let result = handler.on_get_extended_agent_card(None).await;
assert!(
matches!(result, Err(ServerError::UnsupportedOperation(_))),
"expected UnsupportedOperation when capability is false, got: {result:?}"
);
}
#[tokio::test]
async fn get_extended_agent_card_without_authenticator_is_refused() {
let mut card = make_agent_card();
card.capabilities = AgentCapabilities::none().with_extended_agent_card(true);
let handler = RequestHandlerBuilder::new(DummyExecutor)
.with_agent_card(card)
.build()
.unwrap();
let result = handler.on_get_extended_agent_card(None).await;
assert!(
matches!(result, Err(ServerError::Protocol(ref e))
if e.message.contains("requires authentication")),
"expected an authentication-required refusal, got: {result:?}"
);
}
#[tokio::test]
async fn get_extended_agent_card_with_optout_returns_ok() {
let mut card = make_agent_card();
card.capabilities = AgentCapabilities::none().with_extended_agent_card(true);
let handler = RequestHandlerBuilder::new(DummyExecutor)
.with_agent_card(card)
.allow_unauthenticated_extended_card()
.build()
.unwrap();
let result = handler.on_get_extended_agent_card(None).await;
assert!(
result.is_ok(),
"expected Ok with explicit unauthenticated opt-in, got: {result:?}"
);
assert_eq!(result.unwrap().name, "Test Agent");
}
#[tokio::test]
async fn get_extended_agent_card_with_authenticator_gates_on_credentials() {
let mut card = make_agent_card();
card.capabilities = AgentCapabilities::none().with_extended_agent_card(true);
let handler = RequestHandlerBuilder::new(DummyExecutor)
.with_agent_card(card)
.with_interceptor(crate::auth::BearerTokenAuthInterceptor::new(["sekret"]))
.build()
.unwrap();
let anon = handler.on_get_extended_agent_card(None).await;
assert!(
anon.is_err(),
"unauthenticated request must be rejected, got: {anon:?}"
);
let mut headers = std::collections::HashMap::new();
headers.insert("authorization".to_owned(), "Bearer sekret".to_owned());
let authed = handler.on_get_extended_agent_card(Some(&headers)).await;
assert!(
authed.is_ok(),
"authenticated request must be served, got: {authed:?}"
);
assert_eq!(authed.unwrap().name, "Test Agent");
}
#[tokio::test]
async fn get_extended_agent_card_error_path_records_metrics() {
let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
let result = handler.on_get_extended_agent_card(None).await;
assert!(
result.is_err(),
"expected error for error metrics path, got: {result:?}"
);
}
}