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