Skip to main content

entelix_tools/
error.rs

1//! Tool-layer errors. Surface to callers as
2//! [`entelix_core::Error::InvalidRequest`] (input shape mismatch),
3//! [`entelix_core::Error::Provider`] (HTTP / upstream failure), or
4//! [`entelix_core::Error::Config`] (misconfiguration).
5
6use thiserror::Error;
7
8use entelix_core::error::Error;
9
10/// Result alias used inside `entelix-tools`.
11pub type ToolResult<T> = std::result::Result<T, ToolError>;
12
13/// Tool-layer failures.
14#[derive(Debug, Error)]
15#[non_exhaustive]
16pub enum ToolError {
17    /// Input did not match the tool's `input_schema`.
18    #[error("invalid input: {0}")]
19    InvalidInput(String),
20
21    /// Host blocked by the configured allowlist.
22    #[error("host '{host}' not on the allowlist")]
23    HostBlocked {
24        /// The rejected host.
25        host: String,
26    },
27
28    /// URL scheme was not `http` or `https`.
29    #[error("unsupported URL scheme '{scheme}': only http/https are allowed")]
30    UnsupportedScheme {
31        /// The rejected scheme.
32        scheme: String,
33    },
34
35    /// HTTP method blocked by the configured method allowlist.
36    #[error("method '{method}' not allowed by this tool")]
37    MethodBlocked {
38        /// The rejected method, uppercased.
39        method: String,
40    },
41
42    /// Response body exceeded the configured cap.
43    #[error("response body exceeded {limit_bytes} bytes")]
44    BodyTooLarge {
45        /// Configured cap.
46        limit_bytes: usize,
47    },
48
49    /// Network / HTTP failure. Carries the underlying transport error
50    /// as the source so callers see the full chain via
51    /// `std::error::Error::source`.
52    #[error("network: {message}")]
53    Network {
54        /// Human-readable summary.
55        message: String,
56        /// Underlying transport error.
57        #[source]
58        source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
59    },
60
61    /// Configuration error (malformed allowlist pattern, etc.). Carries
62    /// the parse / validation source when the misconfiguration was
63    /// detected via a typed error.
64    #[error("configuration: {message}")]
65    Config {
66        /// Human-readable summary.
67        message: String,
68        /// Underlying parse / validation error.
69        #[source]
70        source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
71    },
72
73    /// Calculator parser rejected the input expression.
74    #[error("calculator: {0}")]
75    Calculator(String),
76
77    /// JSON encode/decode failure.
78    #[error(transparent)]
79    Serde(#[from] serde_json::Error),
80}
81
82impl ToolError {
83    /// Wrap any transport error as a [`ToolError::Network`] preserving
84    /// the source chain.
85    pub fn network<E>(source: E) -> Self
86    where
87        E: std::error::Error + Send + Sync + 'static,
88    {
89        Self::Network {
90            message: source.to_string(),
91            source: Some(Box::new(source)),
92        }
93    }
94
95    /// Construct a [`ToolError::Network`] from a bare message
96    /// (timeout, body cap exceeded, …).
97    pub fn network_msg(message: impl Into<String>) -> Self {
98        Self::Network {
99            message: message.into(),
100            source: None,
101        }
102    }
103
104    /// Wrap a parse/validation error as a [`ToolError::Config`]
105    /// preserving the source chain.
106    pub fn config<E>(source: E) -> Self
107    where
108        E: std::error::Error + Send + Sync + 'static,
109    {
110        Self::Config {
111            message: source.to_string(),
112            source: Some(Box::new(source)),
113        }
114    }
115
116    /// Construct a [`ToolError::Config`] from a bare message.
117    pub fn config_msg(message: impl Into<String>) -> Self {
118        Self::Config {
119            message: message.into(),
120            source: None,
121        }
122    }
123}
124
125impl From<ToolError> for Error {
126    fn from(err: ToolError) -> Self {
127        match err {
128            ToolError::Config { .. } => Self::config(err.to_string()),
129            ToolError::BodyTooLarge { .. } => Self::provider_http_from(413, err),
130            ToolError::Network { .. } => Self::provider_network_from(err),
131            // Everything else is a caller-side input mismatch.
132            ToolError::InvalidInput(_)
133            | ToolError::HostBlocked { .. }
134            | ToolError::UnsupportedScheme { .. }
135            | ToolError::MethodBlocked { .. }
136            | ToolError::Calculator(_)
137            | ToolError::Serde(_) => Self::invalid_request(err.to_string()),
138        }
139    }
140}