Skip to main content

ai_crew_sync/
error.rs

1use rmcp::ErrorData;
2
3/// Errors produced by the store layer.
4///
5/// These map onto MCP tool errors so that the calling agent gets a useful
6/// message instead of an opaque "internal error".
7#[derive(Debug, thiserror::Error)]
8pub enum BusError {
9    #[error("not found: {0}")]
10    NotFound(String),
11
12    #[error("invalid input: {0}")]
13    Invalid(String),
14
15    #[error("conflict: {0}")]
16    Conflict(String),
17
18    #[error("unauthenticated: {0}")]
19    Unauthenticated(String),
20
21    /// The caller is known but not allowed: a team-scoped administrative
22    /// credential reaching for another team, or for a global-only action.
23    #[error("forbidden: {0}")]
24    Forbidden(String),
25
26    #[error("database error: {0}")]
27    Db(#[from] sqlx::Error),
28}
29
30impl BusError {
31    pub fn not_found(msg: impl Into<String>) -> Self {
32        Self::NotFound(msg.into())
33    }
34    pub fn invalid(msg: impl Into<String>) -> Self {
35        Self::Invalid(msg.into())
36    }
37    pub fn conflict(msg: impl Into<String>) -> Self {
38        Self::Conflict(msg.into())
39    }
40    pub fn forbidden(msg: impl Into<String>) -> Self {
41        Self::Forbidden(msg.into())
42    }
43}
44
45/// The one boundary where a store error becomes what a caller reads.
46///
47/// The rule every message crossing it keeps: it is written for its reader —
48/// the model that made the call, or the human that model works for — and
49/// says what to do. Text written for someone else (this server's operator,
50/// the broker, the transport) is never forwarded into a tool result, not
51/// even interpolated as `({e})`: it is rewritten where it crosses, in the
52/// caller's terms, and the original goes to `tracing`. `Db` below is the
53/// general case; `broker_unreadable_note` in `store/inbox.rs` and
54/// `remote_error_text` in `proxy.rs` are the same rule at the two other
55/// boundaries a tool result can cross.
56impl From<BusError> for ErrorData {
57    fn from(err: BusError) -> Self {
58        match err {
59            BusError::NotFound(m) => ErrorData::invalid_params(format!("not found: {m}"), None),
60            BusError::Invalid(m) => ErrorData::invalid_params(m, None),
61            BusError::Conflict(m) => ErrorData::invalid_params(format!("conflict: {m}"), None),
62            BusError::Unauthenticated(m) => {
63                ErrorData::invalid_request(format!("unauthenticated: {m}"), None)
64            }
65            BusError::Forbidden(m) => ErrorData::invalid_request(format!("forbidden: {m}"), None),
66            BusError::Db(e) => {
67                // Never leak SQL/connection detail to the model; log it instead.
68                tracing::error!(error = %e, "database error");
69                ErrorData::internal_error("database error", None)
70            }
71        }
72    }
73}
74
75pub type BusResult<T> = Result<T, BusError>;