1use std::sync::Arc;
4
5use a2a::AgentCard;
6use axum::{
7 Json,
8 extract::State,
9 http::{HeaderMap, StatusCode, header},
10 response::IntoResponse,
11};
12
13pub const WELL_KNOWN_AGENT_CARD_PATH: &str = "/.well-known/agent-card.json";
15
16pub trait AgentCardProducer: Send + Sync + 'static {
18 fn card(&self) -> AgentCard;
19}
20
21pub struct StaticAgentCard {
23 card: AgentCard,
24}
25
26impl StaticAgentCard {
27 pub fn new(card: AgentCard) -> Self {
28 StaticAgentCard { card }
29 }
30}
31
32impl AgentCardProducer for StaticAgentCard {
33 fn card(&self) -> AgentCard {
34 self.card.clone()
35 }
36}
37
38pub fn agent_card_router<P: AgentCardProducer>(producer: Arc<P>) -> axum::Router {
41 axum::Router::new()
42 .route(
43 WELL_KNOWN_AGENT_CARD_PATH,
44 axum::routing::get(handle_agent_card::<P>),
45 )
46 .with_state(producer)
47}
48
49async fn handle_agent_card<P: AgentCardProducer>(
50 State(producer): State<Arc<P>>,
51 headers: HeaderMap,
52) -> impl IntoResponse {
53 let card = producer.card();
54 let mut resp_headers = HeaderMap::new();
55
56 let origin = headers
58 .get(header::ORIGIN)
59 .and_then(|v| v.to_str().ok())
60 .unwrap_or("*");
61
62 if origin != "*" {
63 resp_headers.insert(
64 header::ACCESS_CONTROL_ALLOW_ORIGIN,
65 origin.parse().unwrap_or_else(|_| "*".parse().unwrap()),
66 );
67 resp_headers.insert(
68 header::ACCESS_CONTROL_ALLOW_CREDENTIALS,
69 "true".parse().unwrap(),
70 );
71 resp_headers.insert(header::VARY, "Origin".parse().unwrap());
72 } else {
73 resp_headers.insert(header::ACCESS_CONTROL_ALLOW_ORIGIN, "*".parse().unwrap());
74 }
75
76 (StatusCode::OK, resp_headers, Json(card))
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82 use a2a::AgentCapabilities;
83 use axum::body::Body;
84 use axum::http::Request;
85 use tower::ServiceExt;
86
87 fn test_card() -> AgentCard {
88 AgentCard {
89 name: "TestAgent".into(),
90 description: "A test agent".into(),
91 version: "1.0".into(),
92 supported_interfaces: vec![],
93 capabilities: AgentCapabilities::default(),
94 default_input_modes: vec!["text".into()],
95 default_output_modes: vec!["text".into()],
96 skills: vec![],
97 provider: None,
98 documentation_url: None,
99 icon_url: None,
100 security_schemes: None,
101 security_requirements: None,
102 signatures: None,
103 }
104 }
105
106 #[test]
107 fn test_static_agent_card() {
108 let card = test_card();
109 let sac = StaticAgentCard::new(card.clone());
110 assert_eq!(sac.card().name, "TestAgent");
111 }
112
113 #[test]
114 fn test_well_known_path() {
115 assert_eq!(WELL_KNOWN_AGENT_CARD_PATH, "/.well-known/agent-card.json");
116 }
117
118 #[tokio::test]
119 async fn test_agent_card_router_no_origin() {
120 let card = test_card();
121 let producer = Arc::new(StaticAgentCard::new(card));
122 let app = agent_card_router(producer);
123
124 let req = Request::builder()
125 .uri("/.well-known/agent-card.json")
126 .body(Body::empty())
127 .unwrap();
128
129 let resp = app.oneshot(req).await.unwrap();
130 assert_eq!(resp.status(), StatusCode::OK);
131 assert_eq!(
132 resp.headers()
133 .get("access-control-allow-origin")
134 .unwrap()
135 .to_str()
136 .unwrap(),
137 "*"
138 );
139 }
140
141 #[tokio::test]
142 async fn test_agent_card_router_with_origin() {
143 let card = test_card();
144 let producer = Arc::new(StaticAgentCard::new(card));
145 let app = agent_card_router(producer);
146
147 let req = Request::builder()
148 .uri("/.well-known/agent-card.json")
149 .header("origin", "https://example.com")
150 .body(Body::empty())
151 .unwrap();
152
153 let resp = app.oneshot(req).await.unwrap();
154 assert_eq!(resp.status(), StatusCode::OK);
155 assert_eq!(
156 resp.headers()
157 .get("access-control-allow-origin")
158 .unwrap()
159 .to_str()
160 .unwrap(),
161 "https://example.com"
162 );
163 assert_eq!(
164 resp.headers()
165 .get("access-control-allow-credentials")
166 .unwrap()
167 .to_str()
168 .unwrap(),
169 "true"
170 );
171 }
172}