use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Error)]
#[error("{code} at {stage}: {message}")]
pub struct DashboardError {
pub code: String,
pub stage: String,
pub target_id: Option<String>,
pub message: String,
pub retryable: bool,
}
impl DashboardError {
pub fn new(
code: impl Into<String>,
stage: impl Into<String>,
target_id: Option<String>,
message: impl Into<String>,
retryable: bool,
) -> Self {
Self {
code: code.into(),
stage: stage.into(),
target_id,
message: message.into(),
retryable,
}
}
pub fn unsupported_method(method: impl AsRef<str>) -> Self {
Self::new(
"unsupported_method",
"protocol_parse",
None,
format!("unsupported dashboard IPC method {}", method.as_ref()),
false,
)
}
pub fn validation(
stage: impl Into<String>,
target_id: Option<String>,
message: impl Into<String>,
) -> Self {
Self::new("validation_failed", stage, target_id, message, false)
}
pub fn target_unavailable(
stage: impl Into<String>,
target_id: impl Into<String>,
message: impl Into<String>,
) -> Self {
Self::new(
"target_unavailable",
stage,
Some(target_id.into()),
message,
true,
)
}
pub fn ipc_socket_owner_mismatch(message: impl Into<String>) -> Self {
Self::new(
"ipc_socket_owner_mismatch",
"ipc_bind",
None,
message,
false,
)
}
pub fn peer_cred_uid_mismatch(expected: u32, got: u32) -> Self {
Self::new(
"peer_cred_uid_mismatch",
"peer_credentials",
None,
format!("peer uid mismatch: expected {expected}, got {got}"),
false,
)
}
pub fn peer_cred_gid_not_allowed(gid: u32) -> Self {
Self::new(
"peer_cred_gid_not_allowed",
"peer_credentials",
None,
format!("peer gid {gid} is not in the allowed gid list"),
false,
)
}
pub fn peer_cred_pid_not_allowed(pid: u32) -> Self {
Self::new(
"peer_cred_pid_not_allowed",
"peer_credentials",
None,
format!("peer pid {pid} is not in the allowed pid list"),
false,
)
}
pub fn peer_cred_unavailable(message: impl Into<String>) -> Self {
Self::new(
"peer_cred_unavailable",
"peer_credentials",
None,
message,
false,
)
}
pub fn authz_denied(method: impl Into<String>) -> Self {
Self::new(
"authz_denied",
"authorization",
None,
format!("command {} is not authorized", method.into()),
false,
)
}
pub fn authz_not_configured() -> Self {
Self::new(
"authz_not_configured",
"authorization",
None,
"command authorization is not configured",
false,
)
}
pub fn replay_detected(request_id: impl Into<String>) -> Self {
Self::new(
"replay_detected",
"replay_protection",
None,
format!("replay detected for request_id {}", request_id.into()),
false,
)
}
pub fn request_too_large(actual: usize, max_bytes: usize) -> Self {
Self::new(
"request_too_large",
"size_limit",
None,
format!("request body {actual} bytes exceeds limit of {max_bytes} bytes"),
false,
)
}
pub fn rate_limit_exceeded() -> Self {
Self::new(
"rate_limit_exceeded",
"rate_limit",
None,
"rate limit exceeded",
false,
)
}
pub fn audit_write_failed(message: impl Into<String>) -> Self {
Self::new("audit_write_failed", "audit", None, message, false)
}
pub fn audit_queue_full() -> Self {
Self::new(
"audit_queue_full",
"audit",
None,
"audit defer queue is full",
false,
)
}
pub fn allowlist_denied(path: impl Into<String>) -> Self {
Self::new(
"allowlist_denied",
"allowlist",
None,
format!("external command not in allowlist: {}", path.into()),
false,
)
}
pub fn allowlist_empty() -> Self {
Self::new(
"allowlist_empty",
"allowlist",
None,
"external command allowlist is empty — all external commands are denied",
false,
)
}
}