use std::sync::Arc;
use std::time::Duration;
use http_body_util::{BodyExt, Full};
use hyper::body::Bytes;
use hyper::header;
#[cfg(not(feature = "tls-rustls"))]
use hyper_util::client::legacy::connect::HttpConnector;
#[cfg(not(feature = "tls-rustls"))]
use hyper_util::client::legacy::Client;
#[cfg(not(feature = "tls-rustls"))]
use hyper_util::rt::TokioExecutor;
use tokio::sync::RwLock;
use a2a_protocol_types::AgentCard;
use crate::error::{ClientError, ClientResult};
pub const AGENT_CARD_PATH: &str = "/.well-known/agent-card.json";
pub async fn resolve_agent_card(base_url: &str) -> ClientResult<AgentCard> {
trace_info!(base_url, "resolving agent card");
let url = build_card_url(base_url, AGENT_CARD_PATH)?;
fetch_card(&url, None).await
}
pub async fn resolve_agent_card_with_path(base_url: &str, path: &str) -> ClientResult<AgentCard> {
let url = build_card_url(base_url, path)?;
fetch_card(&url, None).await
}
pub async fn fetch_card_from_url(url: &str) -> ClientResult<AgentCard> {
fetch_card(url, None).await
}
#[derive(Debug, Clone)]
struct CachedCard {
card: AgentCard,
etag: Option<String>,
last_modified: Option<String>,
}
#[derive(Debug, Clone)]
pub struct CachingCardResolver {
url: String,
cache: Arc<RwLock<Option<CachedCard>>>,
}
impl CachingCardResolver {
pub fn new(base_url: &str) -> ClientResult<Self> {
let url = build_card_url(base_url, AGENT_CARD_PATH)?;
Ok(Self {
url,
cache: Arc::new(RwLock::new(None)),
})
}
pub fn with_path(base_url: &str, path: &str) -> ClientResult<Self> {
let url = build_card_url(base_url, path)?;
Ok(Self {
url,
cache: Arc::new(RwLock::new(None)),
})
}
pub async fn resolve(&self) -> ClientResult<AgentCard> {
trace_info!(url = %self.url, "resolving agent card (cached)");
let cached = self.cache.read().await.clone();
let (card, etag, last_modified) =
fetch_card_with_metadata(&self.url, cached.as_ref()).await?;
{
let mut guard = self.cache.write().await;
*guard = Some(CachedCard {
card: card.clone(),
etag,
last_modified,
});
}
Ok(card)
}
pub async fn invalidate(&self) {
let mut cache = self.cache.write().await;
*cache = None;
}
}
fn build_card_url(base_url: &str, path: &str) -> ClientResult<String> {
if base_url.is_empty() {
return Err(ClientError::InvalidEndpoint(
"base URL must not be empty".into(),
));
}
if !base_url.starts_with("http://") && !base_url.starts_with("https://") {
return Err(ClientError::InvalidEndpoint(format!(
"base URL must start with http:// or https://: {base_url}"
)));
}
let base = base_url.trim_end_matches('/');
let path = if path.starts_with('/') {
path.to_owned()
} else {
format!("/{path}")
};
Ok(format!("{base}{path}"))
}
async fn fetch_card(url: &str, cached: Option<&CachedCard>) -> ClientResult<AgentCard> {
let (card, _, _) = fetch_card_with_metadata(url, cached).await?;
Ok(card)
}
#[allow(clippy::too_many_lines)]
async fn fetch_card_with_metadata(
url: &str,
cached: Option<&CachedCard>,
) -> ClientResult<(AgentCard, Option<String>, Option<String>)> {
#[cfg(not(feature = "tls-rustls"))]
let client: Client<HttpConnector, Full<Bytes>> = {
let mut connector = HttpConnector::new();
connector.set_connect_timeout(Some(Duration::from_secs(10)));
connector.set_nodelay(true);
Client::builder(TokioExecutor::new()).build(connector)
};
#[cfg(feature = "tls-rustls")]
let client = crate::tls::build_https_client();
let mut builder = hyper::Request::builder()
.method(hyper::Method::GET)
.uri(url)
.header(header::ACCEPT, "application/json");
if let Some(cached) = cached {
if let Some(ref etag) = cached.etag {
builder = builder.header("if-none-match", etag.as_str());
}
if let Some(ref lm) = cached.last_modified {
builder = builder.header("if-modified-since", lm.as_str());
}
}
let req = builder
.body(Full::new(Bytes::new()))
.map_err(|e| ClientError::Transport(e.to_string()))?;
let resp = tokio::time::timeout(Duration::from_secs(30), client.request(req))
.await
.map_err(|_| ClientError::Transport("agent card fetch timed out".into()))?
.map_err(|e| ClientError::HttpClient(e.to_string()))?;
let status = resp.status();
if status == hyper::StatusCode::NOT_MODIFIED {
if let Some(cached) = cached {
return Ok((
cached.card.clone(),
cached.etag.clone(),
cached.last_modified.clone(),
));
}
}
let etag = resp
.headers()
.get("etag")
.and_then(|v| v.to_str().ok())
.map(str::to_owned);
let last_modified = resp
.headers()
.get("last-modified")
.and_then(|v| v.to_str().ok())
.map(str::to_owned);
let max_card_body_size: u64 = 2 * 1024 * 1024;
if let Some(cl) = resp.headers().get(header::CONTENT_LENGTH) {
if let Ok(len) = cl.to_str().unwrap_or("0").parse::<u64>() {
if len > max_card_body_size {
return Err(ClientError::Transport(format!(
"agent card response too large: {len} bytes exceeds {max_card_body_size} byte limit"
)));
}
}
}
let body_bytes = tokio::time::timeout(Duration::from_secs(30), resp.collect())
.await
.map_err(|_| ClientError::Transport("agent card body read timed out".into()))?
.map_err(ClientError::Http)?
.to_bytes();
if body_bytes.len() as u64 > max_card_body_size {
return Err(ClientError::Transport(format!(
"agent card response too large: {} bytes exceeds {max_card_body_size} byte limit",
body_bytes.len()
)));
}
if !status.is_success() {
let body_str = String::from_utf8_lossy(&body_bytes).into_owned();
return Err(ClientError::UnexpectedStatus {
status: status.as_u16(),
body: body_str,
});
}
let card =
serde_json::from_slice::<AgentCard>(&body_bytes).map_err(ClientError::Serialization)?;
Ok((card, etag, last_modified))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_card_url_standard() {
let url = build_card_url("http://localhost:8080", AGENT_CARD_PATH).unwrap();
assert_eq!(url, "http://localhost:8080/.well-known/agent-card.json");
}
#[test]
fn build_card_url_trailing_slash() {
let url = build_card_url("http://localhost:8080/", AGENT_CARD_PATH).unwrap();
assert_eq!(url, "http://localhost:8080/.well-known/agent-card.json");
}
#[test]
fn build_card_url_custom_path() {
let url = build_card_url("http://localhost:8080", "/api/card.json").unwrap();
assert_eq!(url, "http://localhost:8080/api/card.json");
}
#[test]
fn build_card_url_rejects_empty() {
assert!(build_card_url("", AGENT_CARD_PATH).is_err());
}
#[test]
fn build_card_url_rejects_non_http() {
assert!(build_card_url("ftp://example.com", AGENT_CARD_PATH).is_err());
}
#[test]
fn caching_resolver_new() {
let resolver = CachingCardResolver::new("http://localhost:8080").unwrap();
assert_eq!(
resolver.url,
"http://localhost:8080/.well-known/agent-card.json"
);
}
#[test]
fn caching_resolver_new_rejects_invalid_url() {
assert!(CachingCardResolver::new("").is_err());
assert!(CachingCardResolver::new("ftp://example.com").is_err());
}
#[test]
fn caching_resolver_with_path() {
let resolver =
CachingCardResolver::with_path("http://localhost:8080", "/custom/card.json").unwrap();
assert_eq!(resolver.url, "http://localhost:8080/custom/card.json");
}
#[tokio::test]
async fn caching_resolver_invalidate_empty() {
let resolver = CachingCardResolver::new("http://localhost:8080").unwrap();
assert!(resolver.cache.read().await.is_none());
resolver.invalidate().await;
assert!(resolver.cache.read().await.is_none());
}
#[tokio::test]
async fn caching_resolver_invalidate_clears_populated_cache() {
use a2a_protocol_types::{AgentCapabilities, AgentCard};
let resolver = CachingCardResolver::new("http://localhost:8080").unwrap();
{
let mut guard = resolver.cache.write().await;
*guard = Some(CachedCard {
card: AgentCard {
url: None,
name: "cached".into(),
version: "1.0".into(),
description: "Cached agent".into(),
supported_interfaces: vec![],
provider: None,
icon_url: None,
documentation_url: None,
capabilities: AgentCapabilities::none(),
security_schemes: None,
security_requirements: None,
default_input_modes: vec![],
default_output_modes: vec![],
skills: vec![],
signatures: None,
},
etag: Some("test-etag".into()),
last_modified: None,
});
}
{
let cached = resolver.cache.read().await;
let entry = cached.as_ref().expect("cache should be populated");
assert_eq!(entry.card.name, "cached");
assert_eq!(entry.etag, Some("test-etag".into()));
drop(cached);
}
resolver.invalidate().await;
assert!(
resolver.cache.read().await.is_none(),
"invalidate must clear a populated cache"
);
}
#[tokio::test]
async fn fetch_card_with_metadata_non_success_status() {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
loop {
let (stream, _) = listener.accept().await.unwrap();
let io = hyper_util::rt::TokioIo::new(stream);
tokio::spawn(async move {
let service = hyper::service::service_fn(|_req| async {
Ok::<_, hyper::Error>(
hyper::Response::builder()
.status(404)
.body(http_body_util::Full::new(hyper::body::Bytes::from(
"Not Found",
)))
.unwrap(),
)
});
let _ = hyper_util::server::conn::auto::Builder::new(
hyper_util::rt::TokioExecutor::new(),
)
.serve_connection(io, service)
.await;
});
}
});
let url = format!("http://127.0.0.1:{}/agent.json", addr.port());
let result = fetch_card_with_metadata(&url, None).await;
assert!(result.is_err());
match result.unwrap_err() {
ClientError::UnexpectedStatus { status, body } => {
assert_eq!(status, 404);
assert!(body.contains("Not Found"));
}
other => panic!("expected UnexpectedStatus, got {other:?}"),
}
}
#[tokio::test]
async fn fetch_card_with_metadata_304_returns_cached() {
use a2a_protocol_types::{AgentCapabilities, AgentCard};
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
loop {
let (stream, _) = listener.accept().await.unwrap();
let io = hyper_util::rt::TokioIo::new(stream);
tokio::spawn(async move {
let service = hyper::service::service_fn(|_req| async {
Ok::<_, hyper::Error>(
hyper::Response::builder()
.status(304)
.body(http_body_util::Full::new(hyper::body::Bytes::new()))
.unwrap(),
)
});
let _ = hyper_util::server::conn::auto::Builder::new(
hyper_util::rt::TokioExecutor::new(),
)
.serve_connection(io, service)
.await;
});
}
});
let cached = CachedCard {
card: AgentCard {
url: None,
name: "cached-agent".into(),
version: "2.0".into(),
description: "Cached".into(),
supported_interfaces: vec![],
provider: None,
icon_url: None,
documentation_url: None,
capabilities: AgentCapabilities::none(),
security_schemes: None,
security_requirements: None,
default_input_modes: vec![],
default_output_modes: vec![],
skills: vec![],
signatures: None,
},
etag: Some("\"abc123\"".into()),
last_modified: None,
};
let url = format!("http://127.0.0.1:{}/agent.json", addr.port());
let (card, etag, _) = fetch_card_with_metadata(&url, Some(&cached)).await.unwrap();
assert_eq!(card.name, "cached-agent");
assert_eq!(etag, Some("\"abc123\"".into()));
}
#[tokio::test]
async fn fetch_card_with_metadata_200_parses_card() {
use a2a_protocol_types::{AgentCapabilities, AgentCard, AgentInterface};
let card = AgentCard {
url: None,
name: "test-agent".into(),
version: "1.0".into(),
description: "A test".into(),
supported_interfaces: vec![AgentInterface {
url: "http://localhost:9090".into(),
protocol_binding: "JSONRPC".into(),
protocol_version: "1.0.0".into(),
tenant: None,
}],
provider: None,
icon_url: None,
documentation_url: None,
capabilities: AgentCapabilities::none(),
security_schemes: None,
security_requirements: None,
default_input_modes: vec![],
default_output_modes: vec![],
skills: vec![],
signatures: None,
};
let card_json = serde_json::to_string(&card).unwrap();
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
loop {
let (stream, _) = listener.accept().await.unwrap();
let io = hyper_util::rt::TokioIo::new(stream);
let body = card_json.clone();
tokio::spawn(async move {
let service = hyper::service::service_fn(move |_req| {
let body = body.clone();
async move {
Ok::<_, hyper::Error>(
hyper::Response::builder()
.status(200)
.header("etag", "\"xyz\"")
.header("last-modified", "Mon, 01 Jan 2026 00:00:00 GMT")
.body(http_body_util::Full::new(hyper::body::Bytes::from(body)))
.unwrap(),
)
}
});
let _ = hyper_util::server::conn::auto::Builder::new(
hyper_util::rt::TokioExecutor::new(),
)
.serve_connection(io, service)
.await;
});
}
});
let url = format!("http://127.0.0.1:{}/agent.json", addr.port());
let (parsed_card, etag, last_modified) =
fetch_card_with_metadata(&url, None).await.unwrap();
assert_eq!(parsed_card.name, "test-agent");
assert_eq!(etag, Some("\"xyz\"".into()));
assert_eq!(last_modified, Some("Mon, 01 Jan 2026 00:00:00 GMT".into()));
}
#[allow(clippy::too_many_lines)]
#[tokio::test]
async fn caching_resolver_resolve_fetches_and_caches() {
use a2a_protocol_types::{AgentCapabilities, AgentCard, AgentInterface};
let card = AgentCard {
url: None,
name: "resolver-test".into(),
version: "1.0".into(),
description: "Resolver test agent".into(),
supported_interfaces: vec![AgentInterface {
url: "http://localhost:9090".into(),
protocol_binding: "JSONRPC".into(),
protocol_version: "1.0.0".into(),
tenant: None,
}],
provider: None,
icon_url: None,
documentation_url: None,
capabilities: AgentCapabilities::none(),
security_schemes: None,
security_requirements: None,
default_input_modes: vec![],
default_output_modes: vec![],
skills: vec![],
signatures: None,
};
let card_json = serde_json::to_string(&card).unwrap();
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
loop {
let (stream, _) = listener.accept().await.unwrap();
let io = hyper_util::rt::TokioIo::new(stream);
let body = card_json.clone();
tokio::spawn(async move {
let service = hyper::service::service_fn(move |_req| {
let body = body.clone();
async move {
Ok::<_, hyper::Error>(
hyper::Response::builder()
.status(200)
.header("etag", "\"res-etag\"")
.body(http_body_util::Full::new(hyper::body::Bytes::from(body)))
.unwrap(),
)
}
});
let _ = hyper_util::server::conn::auto::Builder::new(
hyper_util::rt::TokioExecutor::new(),
)
.serve_connection(io, service)
.await;
});
}
});
let base_url = format!("http://127.0.0.1:{}", addr.port());
let resolver = CachingCardResolver::with_path(&base_url, "/agent.json").unwrap();
assert!(
resolver.cache.read().await.is_none(),
"cache should start empty"
);
let fetched = resolver.resolve().await.unwrap();
assert_eq!(fetched.name, "resolver-test");
let cached = resolver.cache.read().await;
let entry = cached
.as_ref()
.expect("cache should be populated after resolve");
assert_eq!(entry.card.name, "resolver-test");
assert_eq!(entry.etag, Some("\"res-etag\"".into()));
drop(cached);
}
#[tokio::test]
async fn caching_resolver_resolve_returns_error_on_failure() {
let resolver = CachingCardResolver::with_path("http://127.0.0.1:1", "/agent.json").unwrap();
let result = resolver.resolve().await;
assert!(
result.is_err(),
"resolve should fail with unreachable server"
);
}
#[test]
fn build_card_url_path_without_leading_slash() {
let url = build_card_url("http://localhost:8080", "custom/card.json").unwrap();
assert_eq!(url, "http://localhost:8080/custom/card.json");
}
#[tokio::test]
async fn fetch_card_from_url_success() {
use a2a_protocol_types::{AgentCapabilities, AgentCard, AgentInterface};
let card = AgentCard {
url: None,
name: "url-fetch-test".into(),
version: "1.0".into(),
description: "URL fetch test".into(),
supported_interfaces: vec![AgentInterface {
url: "http://localhost:9090".into(),
protocol_binding: "JSONRPC".into(),
protocol_version: "1.0.0".into(),
tenant: None,
}],
provider: None,
icon_url: None,
documentation_url: None,
capabilities: AgentCapabilities::none(),
security_schemes: None,
security_requirements: None,
default_input_modes: vec![],
default_output_modes: vec![],
skills: vec![],
signatures: None,
};
let card_json = serde_json::to_string(&card).unwrap();
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
loop {
let (stream, _) = listener.accept().await.unwrap();
let io = hyper_util::rt::TokioIo::new(stream);
let body = card_json.clone();
tokio::spawn(async move {
let service = hyper::service::service_fn(move |_req| {
let body = body.clone();
async move {
Ok::<_, hyper::Error>(
hyper::Response::builder()
.status(200)
.body(http_body_util::Full::new(hyper::body::Bytes::from(body)))
.unwrap(),
)
}
});
let _ = hyper_util::server::conn::auto::Builder::new(
hyper_util::rt::TokioExecutor::new(),
)
.serve_connection(io, service)
.await;
});
}
});
let url = format!("http://127.0.0.1:{}/agent.json", addr.port());
let fetched = fetch_card_from_url(&url).await.unwrap();
assert_eq!(fetched.name, "url-fetch-test");
}
#[tokio::test]
async fn resolve_agent_card_with_path_success() {
use a2a_protocol_types::{AgentCapabilities, AgentCard, AgentInterface};
let card = AgentCard {
url: None,
name: "path-resolve-test".into(),
version: "2.0".into(),
description: "Path resolve test".into(),
supported_interfaces: vec![AgentInterface {
url: "http://localhost:9090".into(),
protocol_binding: "JSONRPC".into(),
protocol_version: "1.0.0".into(),
tenant: None,
}],
provider: None,
icon_url: None,
documentation_url: None,
capabilities: AgentCapabilities::none(),
security_schemes: None,
security_requirements: None,
default_input_modes: vec![],
default_output_modes: vec![],
skills: vec![],
signatures: None,
};
let card_json = serde_json::to_string(&card).unwrap();
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
loop {
let (stream, _) = listener.accept().await.unwrap();
let io = hyper_util::rt::TokioIo::new(stream);
let body = card_json.clone();
tokio::spawn(async move {
let service = hyper::service::service_fn(move |_req| {
let body = body.clone();
async move {
Ok::<_, hyper::Error>(
hyper::Response::builder()
.status(200)
.body(http_body_util::Full::new(hyper::body::Bytes::from(body)))
.unwrap(),
)
}
});
let _ = hyper_util::server::conn::auto::Builder::new(
hyper_util::rt::TokioExecutor::new(),
)
.serve_connection(io, service)
.await;
});
}
});
let base_url = format!("http://127.0.0.1:{}", addr.port());
let fetched = resolve_agent_card_with_path(&base_url, "/custom.json")
.await
.unwrap();
assert_eq!(fetched.name, "path-resolve-test");
}
#[tokio::test]
async fn fetch_card_rejects_oversized_content_length() {
use tokio::io::AsyncWriteExt;
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
loop {
let (mut stream, _) = listener.accept().await.unwrap();
tokio::spawn(async move {
let mut buf = [0u8; 4096];
let _ = tokio::io::AsyncReadExt::read(&mut stream, &mut buf).await;
let response = "HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: 10000000\r\n\r\nsmall";
let _ = stream.write_all(response.as_bytes()).await;
drop(stream);
});
}
});
let url = format!("http://127.0.0.1:{}/agent.json", addr.port());
let result = fetch_card_with_metadata(&url, None).await;
match result {
Err(ClientError::Transport(msg)) => {
assert!(
msg.contains("too large"),
"should mention size limit: {msg}"
);
}
other => panic!("expected Transport error about size, got {other:?}"),
}
}
#[tokio::test]
async fn fetch_card_accepts_content_length_at_exact_limit() {
use tokio::io::AsyncWriteExt;
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let max_size: u64 = 2 * 1024 * 1024;
tokio::spawn(async move {
loop {
let (mut stream, _) = listener.accept().await.unwrap();
tokio::spawn(async move {
let mut buf = [0u8; 4096];
let _ = tokio::io::AsyncReadExt::read(&mut stream, &mut buf).await;
let response = format!(
"HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: {max_size}\r\n\r\nsmall"
);
let _ = stream.write_all(response.as_bytes()).await;
drop(stream);
});
}
});
let url = format!("http://127.0.0.1:{}/agent.json", addr.port());
let result = fetch_card_with_metadata(&url, None).await;
match &result {
Err(ClientError::Transport(msg)) if msg.contains("too large") => {
panic!("Content-Length at exact limit should not be rejected: {msg}");
}
_ => {} }
}
#[tokio::test]
async fn fetch_card_rejects_oversized_body_without_content_length() {
use tokio::io::AsyncWriteExt;
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let max_size = 2 * 1024 * 1024_usize;
tokio::spawn(async move {
loop {
let (mut stream, _) = listener.accept().await.unwrap();
let body_size = max_size + 1;
tokio::spawn(async move {
let mut buf = [0u8; 4096];
let _ = tokio::io::AsyncReadExt::read(&mut stream, &mut buf).await;
let header = "HTTP/1.0 200 OK\r\ncontent-type: application/json\r\n\r\n";
let _ = stream.write_all(header.as_bytes()).await;
let chunk = vec![b'x'; 64 * 1024];
let mut remaining = body_size;
while remaining > 0 {
let n = remaining.min(chunk.len());
if stream.write_all(&chunk[..n]).await.is_err() {
break;
}
remaining -= n;
}
drop(stream);
});
}
});
let url = format!("http://127.0.0.1:{}/agent.json", addr.port());
let result = fetch_card_with_metadata(&url, None).await;
match result {
Err(ClientError::Transport(msg)) => {
assert!(
msg.contains("too large"),
"should mention size limit: {msg}"
);
}
other => panic!("expected Transport error about size for body > limit, got {other:?}"),
}
}
#[tokio::test]
async fn fetch_card_with_metadata_304_with_last_modified() {
use a2a_protocol_types::{AgentCapabilities, AgentCard};
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
loop {
let (stream, _) = listener.accept().await.unwrap();
let io = hyper_util::rt::TokioIo::new(stream);
tokio::spawn(async move {
let service = hyper::service::service_fn(|_req| async {
Ok::<_, hyper::Error>(
hyper::Response::builder()
.status(304)
.body(http_body_util::Full::new(hyper::body::Bytes::new()))
.unwrap(),
)
});
let _ = hyper_util::server::conn::auto::Builder::new(
hyper_util::rt::TokioExecutor::new(),
)
.serve_connection(io, service)
.await;
});
}
});
let cached = CachedCard {
card: AgentCard {
url: None,
name: "lm-cached".into(),
version: "1.0".into(),
description: "Last-modified cached".into(),
supported_interfaces: vec![],
provider: None,
icon_url: None,
documentation_url: None,
capabilities: AgentCapabilities::none(),
security_schemes: None,
security_requirements: None,
default_input_modes: vec![],
default_output_modes: vec![],
skills: vec![],
signatures: None,
},
etag: None,
last_modified: Some("Mon, 01 Jan 2026 00:00:00 GMT".into()),
};
let url = format!("http://127.0.0.1:{}/agent.json", addr.port());
let (card, _, last_modified) = fetch_card_with_metadata(&url, Some(&cached)).await.unwrap();
assert_eq!(card.name, "lm-cached");
assert_eq!(last_modified, Some("Mon, 01 Jan 2026 00:00:00 GMT".into()));
}
}