Skip to main content

rust_supervisor/dashboard/
error.rs

1//! Structured dashboard IPC errors.
2//!
3//! The dashboard feature exchanges errors with relay code over JSON. This
4//! module keeps those errors typed so tests can assert code, stage, target, and
5//! retry behavior without parsing strings.
6
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};
9use thiserror::Error;
10
11/// Error returned by target-side dashboard IPC handlers.
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Error)]
13#[error("{code} at {stage}: {message}")]
14pub struct DashboardError {
15    /// Machine-readable error code.
16    pub code: String,
17    /// Processing stage that produced the error.
18    pub stage: String,
19    /// Optional target process identifier related to the error.
20    pub target_id: Option<String>,
21    /// Human-readable diagnostic message.
22    pub message: String,
23    /// Whether a caller can retry after the reported condition changes.
24    pub retryable: bool,
25}
26
27impl DashboardError {
28    /// Creates a structured dashboard error.
29    ///
30    /// # Arguments
31    ///
32    /// - `code`: Stable machine-readable error code.
33    /// - `stage`: Processing stage that produced the error.
34    /// - `target_id`: Optional target process identifier.
35    /// - `message`: Human-readable diagnostic message.
36    /// - `retryable`: Whether a retry can later succeed.
37    ///
38    /// # Returns
39    ///
40    /// Returns a [`DashboardError`] value ready for JSON serialization.
41    pub fn new(
42        code: impl Into<String>,
43        stage: impl Into<String>,
44        target_id: Option<String>,
45        message: impl Into<String>,
46        retryable: bool,
47    ) -> Self {
48        Self {
49            code: code.into(),
50            stage: stage.into(),
51            target_id,
52            message: message.into(),
53            retryable,
54        }
55    }
56
57    /// Creates an unsupported method error.
58    ///
59    /// # Arguments
60    ///
61    /// - `method`: Method name rejected by the parser.
62    ///
63    /// # Returns
64    ///
65    /// Returns a non-retryable [`DashboardError`].
66    pub fn unsupported_method(method: impl AsRef<str>) -> Self {
67        Self::new(
68            "unsupported_method",
69            "protocol_parse",
70            None,
71            format!("unsupported dashboard IPC method {}", method.as_ref()),
72            false,
73        )
74    }
75
76    /// Creates a validation error.
77    ///
78    /// # Arguments
79    ///
80    /// - `stage`: Validation stage.
81    /// - `target_id`: Optional target process identifier.
82    /// - `message`: Human-readable validation message.
83    ///
84    /// # Returns
85    ///
86    /// Returns a non-retryable [`DashboardError`].
87    pub fn validation(
88        stage: impl Into<String>,
89        target_id: Option<String>,
90        message: impl Into<String>,
91    ) -> Self {
92        Self::new("validation_failed", stage, target_id, message, false)
93    }
94
95    /// Creates a target unavailable error.
96    ///
97    /// # Arguments
98    ///
99    /// - `stage`: Processing stage that tried to use the target.
100    /// - `target_id`: Target process identifier.
101    /// - `message`: Human-readable diagnostic message.
102    ///
103    /// # Returns
104    ///
105    /// Returns a retryable [`DashboardError`].
106    pub fn target_unavailable(
107        stage: impl Into<String>,
108        target_id: impl Into<String>,
109        message: impl Into<String>,
110    ) -> Self {
111        Self::new(
112            "target_unavailable",
113            stage,
114            Some(target_id.into()),
115            message,
116            true,
117        )
118    }
119
120    // ------------------------------------------------------------------
121    // IPC security error constructors
122    // ------------------------------------------------------------------
123
124    /// Creates a socket owner mismatch error.
125    pub fn ipc_socket_owner_mismatch(message: impl Into<String>) -> Self {
126        Self::new(
127            "ipc_socket_owner_mismatch",
128            "ipc_bind",
129            None,
130            message,
131            false,
132        )
133    }
134
135    /// Creates a peer credential uid mismatch error.
136    pub fn peer_cred_uid_mismatch(expected: u32, got: u32) -> Self {
137        Self::new(
138            "peer_cred_uid_mismatch",
139            "peer_credentials",
140            None,
141            format!("peer uid mismatch: expected {expected}, got {got}"),
142            false,
143        )
144    }
145
146    /// Creates a peer credential gid not allowed error.
147    pub fn peer_cred_gid_not_allowed(gid: u32) -> Self {
148        Self::new(
149            "peer_cred_gid_not_allowed",
150            "peer_credentials",
151            None,
152            format!("peer gid {gid} is not in the allowed gid list"),
153            false,
154        )
155    }
156
157    /// Creates a peer credential pid not allowed error.
158    pub fn peer_cred_pid_not_allowed(pid: u32) -> Self {
159        Self::new(
160            "peer_cred_pid_not_allowed",
161            "peer_credentials",
162            None,
163            format!("peer pid {pid} is not in the allowed pid list"),
164            false,
165        )
166    }
167
168    /// Creates a peer credential unavailable error.
169    pub fn peer_cred_unavailable(message: impl Into<String>) -> Self {
170        Self::new(
171            "peer_cred_unavailable",
172            "peer_credentials",
173            None,
174            message,
175            false,
176        )
177    }
178
179    /// Creates an authorization denied error.
180    pub fn authz_denied(method: impl Into<String>) -> Self {
181        Self::new(
182            "authz_denied",
183            "authorization",
184            None,
185            format!("command {} is not authorized", method.into()),
186            false,
187        )
188    }
189
190    /// Creates an authorization not configured error.
191    pub fn authz_not_configured() -> Self {
192        Self::new(
193            "authz_not_configured",
194            "authorization",
195            None,
196            "command authorization is not configured",
197            false,
198        )
199    }
200
201    /// Creates a replay detected error.
202    pub fn replay_detected(request_id: impl Into<String>) -> Self {
203        Self::new(
204            "replay_detected",
205            "replay_protection",
206            None,
207            format!("replay detected for request_id {}", request_id.into()),
208            false,
209        )
210    }
211
212    /// Creates a request too large error.
213    pub fn request_too_large(actual: usize, max_bytes: usize) -> Self {
214        Self::new(
215            "request_too_large",
216            "size_limit",
217            None,
218            format!("request body {actual} bytes exceeds limit of {max_bytes} bytes"),
219            false,
220        )
221    }
222
223    /// Creates a rate limit exceeded error.
224    pub fn rate_limit_exceeded() -> Self {
225        Self::new(
226            "rate_limit_exceeded",
227            "rate_limit",
228            None,
229            "rate limit exceeded",
230            false,
231        )
232    }
233
234    /// Creates an audit write failed error.
235    pub fn audit_write_failed(message: impl Into<String>) -> Self {
236        Self::new("audit_write_failed", "audit", None, message, false)
237    }
238
239    /// Creates an audit queue full error.
240    pub fn audit_queue_full() -> Self {
241        Self::new(
242            "audit_queue_full",
243            "audit",
244            None,
245            "audit defer queue is full",
246            false,
247        )
248    }
249
250    /// Creates an allowlist denied error.
251    pub fn allowlist_denied(path: impl Into<String>) -> Self {
252        Self::new(
253            "allowlist_denied",
254            "allowlist",
255            None,
256            format!("external command not in allowlist: {}", path.into()),
257            false,
258        )
259    }
260
261    /// Creates an allowlist empty error.
262    pub fn allowlist_empty() -> Self {
263        Self::new(
264            "allowlist_empty",
265            "allowlist",
266            None,
267            "external command allowlist is empty — all external commands are denied",
268            false,
269        )
270    }
271}