Skip to main content

a2a_client/
lib.rs

1// Copyright AGNTCY Contributors (https://github.com/agntcy)
2// Copyright A2A Contributors (https://github.com/a2aproject)
3// SPDX-License-Identifier: Apache-2.0
4// Framing internals reached by the fuzz targets in fuzz/ — see #238.
5// Thin wrappers, not re-exports: the originals stay crate-private.
6#[cfg(feature = "fuzzing")]
7#[doc(hidden)]
8pub mod fuzz_support {
9    use a2a::{A2AError, StreamResponse};
10
11    pub fn find_event_boundary(buf: &[u8]) -> Option<(usize, usize)> {
12        crate::jsonrpc::find_event_boundary(buf)
13    }
14
15    pub fn parse_stream_tail(
16        buf: &[u8],
17        parse_event: &dyn Fn(&str) -> Option<Result<StreamResponse, A2AError>>,
18    ) -> Option<Result<StreamResponse, A2AError>> {
19        crate::jsonrpc::parse_stream_tail(buf, parse_event)
20    }
21}
22
23pub mod agent_card;
24pub mod auth;
25pub mod client;
26pub mod factory;
27pub mod jsonrpc;
28pub mod middleware;
29mod push_config_compat;
30pub mod rest;
31pub mod transport;
32mod wire;
33
34pub use client::A2AClient;
35pub use factory::A2AClientFactory;
36pub use futures::stream::BoxStream;
37pub use transport::{ServiceParams, Transport, TransportFactory};
38
39#[cfg(any(feature = "rustls-tls", feature = "rustls-no-provider"))]
40pub use rustls;
41
42/// Build a `reqwest::Client` whose TLS layer matches this crate's feature
43/// selection, optionally adding extra root certificates from a PEM bundle.
44pub fn default_reqwest_client(
45    #[allow(unused_variables)] extra_root_pem: Option<&[u8]>,
46) -> Result<reqwest::Client, a2a::A2AError> {
47    let builder = reqwest::Client::builder();
48
49    #[cfg(any(
50        feature = "rustls-tls",
51        feature = "rustls-no-provider",
52        feature = "native-tls"
53    ))]
54    let builder = match extra_root_pem {
55        Some(pem) => {
56            let certs = reqwest::Certificate::from_pem_bundle(pem)
57                .map_err(|e| a2a::A2AError::internal(format!("invalid PEM certificate: {e}")))?;
58            builder.tls_certs_merge(certs)
59        }
60        None => builder,
61    };
62
63    builder
64        .build()
65        .map_err(|e| a2a::A2AError::internal(format!("failed to build HTTP client: {e}")))
66}
67
68pub(crate) fn a2a_error_from_details(
69    code: i32,
70    message: String,
71    details: Vec<a2a::TypedDetail>,
72) -> a2a::A2AError {
73    use a2a::{error_code, errordetails, reason_to_error_code};
74    use serde_json::Value;
75
76    let mut code = code;
77    let mut message = message;
78
79    for detail in &details {
80        match detail.type_url.as_str() {
81            errordetails::BAD_REQUEST_TYPE => {
82                if let Some(Value::Array(violations)) = detail.value.get("fieldViolations") {
83                    let violation_strs: Vec<String> = violations
84                        .iter()
85                        .filter_map(|v| {
86                            let field = v.get("field")?.as_str()?;
87                            let desc = v.get("description")?.as_str()?;
88                            if field.is_empty() {
89                                Some(desc.to_string())
90                            } else {
91                                Some(format!("{field}: {desc}"))
92                            }
93                        })
94                        .collect();
95                    if !violation_strs.is_empty() {
96                        message = format!("{}: {}", message, violation_strs.join("; "));
97                    }
98                }
99                if code == error_code::INTERNAL_ERROR {
100                    code = error_code::INVALID_PARAMS;
101                }
102            }
103            errordetails::ERROR_INFO_TYPE => {
104                if let Some(Value::String(domain)) = detail.value.get("domain") {
105                    if domain == errordetails::PROTOCOL_DOMAIN {
106                        if let Some(Value::String(reason)) = detail.value.get("reason") {
107                            if let Some(c) = reason_to_error_code(reason) {
108                                code = c;
109                            }
110                        }
111                    }
112                }
113            }
114            _ => {}
115        }
116    }
117
118    a2a::A2AError {
119        code,
120        message,
121        details: (!details.is_empty()).then_some(details),
122    }
123}
124
125#[cfg(all(test, any(feature = "rustls-tls", feature = "native-tls")))]
126pub(crate) mod test_utils {
127    pub fn rcgen_self_signed_ca_pem() -> Vec<u8> {
128        let mut params = rcgen::CertificateParams::new(Vec::<String>::new()).unwrap();
129        params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
130        params
131            .distinguished_name
132            .push(rcgen::DnType::CommonName, "Test CA");
133        let key = rcgen::KeyPair::generate().unwrap();
134        let cert = params.self_signed(&key).unwrap();
135        cert.pem().into_bytes()
136    }
137}