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}