Skip to main content

a2a_client/
lib.rs

1// Copyright AGNTCY Contributors (https://github.com/agntcy)
2// SPDX-License-Identifier: Apache-2.0
3pub mod agent_card;
4pub mod auth;
5pub mod client;
6pub mod factory;
7pub mod jsonrpc;
8pub mod middleware;
9mod push_config_compat;
10pub mod rest;
11pub mod transport;
12
13pub use client::A2AClient;
14pub use factory::A2AClientFactory;
15pub use futures::stream::BoxStream;
16pub use transport::{ServiceParams, Transport, TransportFactory};
17
18pub(crate) fn a2a_error_from_details(
19    code: i32,
20    message: String,
21    details: Vec<a2a::TypedDetail>,
22) -> a2a::A2AError {
23    use a2a::{error_code, errordetails, reason_to_error_code};
24    use serde_json::Value;
25
26    let mut code = code;
27    let mut message = message;
28
29    for detail in &details {
30        match detail.type_url.as_str() {
31            errordetails::BAD_REQUEST_TYPE => {
32                if let Some(Value::Array(violations)) = detail.value.get("fieldViolations") {
33                    let violation_strs: Vec<String> = violations
34                        .iter()
35                        .filter_map(|v| {
36                            let field = v.get("field")?.as_str()?;
37                            let desc = v.get("description")?.as_str()?;
38                            if field.is_empty() {
39                                Some(desc.to_string())
40                            } else {
41                                Some(format!("{field}: {desc}"))
42                            }
43                        })
44                        .collect();
45                    if !violation_strs.is_empty() {
46                        message = format!("{}: {}", message, violation_strs.join("; "));
47                    }
48                }
49                if code == error_code::INTERNAL_ERROR {
50                    code = error_code::INVALID_PARAMS;
51                }
52            }
53            errordetails::ERROR_INFO_TYPE => {
54                if let Some(Value::String(domain)) = detail.value.get("domain") {
55                    if domain == errordetails::PROTOCOL_DOMAIN {
56                        if let Some(Value::String(reason)) = detail.value.get("reason") {
57                            if let Some(c) = reason_to_error_code(reason) {
58                                code = c;
59                            }
60                        }
61                    }
62                }
63            }
64            _ => {}
65        }
66    }
67
68    a2a::A2AError {
69        code,
70        message,
71        details: (!details.is_empty()).then_some(details),
72    }
73}
74
75#[cfg(any(feature = "rustls-tls", feature = "native-tls"))]
76pub(crate) fn build_reqwest_client_with_root_pem(
77    pem: &[u8],
78) -> Result<reqwest::Client, a2a::A2AError> {
79    let cert = reqwest::Certificate::from_pem(pem)
80        .map_err(|e| a2a::A2AError::internal(format!("invalid PEM certificate: {e}")))?;
81    reqwest::Client::builder()
82        .add_root_certificate(cert)
83        .build()
84        .map_err(|e| a2a::A2AError::internal(format!("failed to build HTTP client: {e}")))
85}
86
87#[cfg(test)]
88pub(crate) mod test_utils {
89    pub fn rcgen_self_signed_ca_pem() -> Vec<u8> {
90        let mut params = rcgen::CertificateParams::new(Vec::<String>::new()).unwrap();
91        params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
92        params
93            .distinguished_name
94            .push(rcgen::DnType::CommonName, "Test CA");
95        let key = rcgen::KeyPair::generate().unwrap();
96        let cert = params.self_signed(&key).unwrap();
97        cert.pem().into_bytes()
98    }
99}