Skip to main content

a2a_client/
lib.rs

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