Skip to main content

atm_core/
error_codes.rs

1//! Stable ATM-owned error-code registry.
2//!
3//! These codes are the machine-readable contract for command failures and
4//! degraded-warning diagnostics emitted by ATM.
5
6use std::fmt;
7use std::str::FromStr;
8
9use serde::{Deserialize, Deserializer, Serialize};
10
11/// Stable ATM error and warning codes.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub enum AtmErrorCode {
14    /// ATM home directory could not be resolved.
15    ConfigHomeUnavailable,
16    /// A generic ATM config parse failed.
17    ConfigParseFailed,
18    /// `.atm.toml` uses a retired post-send hook key.
19    ConfigRetiredHookMembersKey,
20    /// `.atm.toml` uses retired flat post-send hook keys.
21    ConfigRetiredLegacyHookKeys,
22    /// Team config parsing failed.
23    ConfigTeamParseFailed,
24    /// Team config document is missing.
25    ConfigTeamMissing,
26    /// Sender identity could not be resolved.
27    IdentityUnavailable,
28    /// Address parsing failed.
29    AddressParseFailed,
30    /// Team could not be resolved from config or input.
31    TeamUnavailable,
32    /// The requested team does not exist.
33    TeamNotFound,
34    /// The requested agent does not exist in the target team.
35    AgentNotFound,
36    /// Reading a mailbox failed.
37    MailboxReadFailed,
38    /// Writing a mailbox failed.
39    MailboxWriteFailed,
40    /// Acquiring or releasing a mailbox lock failed.
41    MailboxLockFailed,
42    /// The mailbox lock path lives on a read-only filesystem.
43    MailboxLockReadOnlyFilesystem,
44    /// Acquiring a mailbox lock timed out.
45    MailboxLockTimeout,
46    /// Message validation failed.
47    MessageValidationFailed,
48    /// Serialization or deserialization failed.
49    SerializationFailed,
50    /// File-policy enforcement rejected the operation.
51    FilePolicyRejected,
52    /// Rewriting a file reference failed.
53    FileReferenceRewriteFailed,
54    /// A wait/read timed out.
55    WaitTimeout,
56    /// Ack was attempted from an invalid state.
57    AckInvalidState,
58    /// Clear was attempted from an invalid state.
59    ClearInvalidState,
60    /// Emitting an observability event failed.
61    ObservabilityEmitFailed,
62    /// Querying retained observability records failed.
63    ObservabilityQueryFailed,
64    /// Starting or polling an observability follow session failed.
65    ObservabilityFollowFailed,
66    /// Observability health evaluation failed.
67    ObservabilityHealthFailed,
68    /// Observability bootstrap/initialization failed.
69    ObservabilityBootstrapFailed,
70    /// Observability health is healthy.
71    ObservabilityHealthOk,
72    /// A malformed team member record was skipped.
73    WarningInvalidTeamMemberSkipped,
74    /// A mailbox record was skipped during degraded recovery.
75    WarningMailboxRecordSkipped,
76    /// A malformed ATM-owned field was ignored.
77    WarningMalformedAtmFieldIgnored,
78    /// Observability health is degraded.
79    WarningObservabilityHealthDegraded,
80    /// An origin inbox entry was skipped.
81    WarningOriginInboxEntrySkipped,
82    /// Send fell back because the team config was missing.
83    WarningMissingTeamConfigFallback,
84    /// Send alert state degraded but the command continued.
85    WarningSendAlertStateDegraded,
86    /// Obsolete .atm.toml identity config is still present.
87    WarningIdentityDrift,
88    /// A baseline team member declared in .atm.toml is missing from config.json.
89    WarningBaselineMemberMissing,
90    /// A restore operation left a stale in-progress marker behind.
91    WarningRestoreInProgress,
92    /// A mailbox lock sentinel persisted for the full doctor run.
93    WarningStaleMailboxLock,
94    /// A configured post-send hook was skipped because no filter matched.
95    WarningHookSkipped,
96    /// A configured post-send hook failed during best-effort execution.
97    WarningHookExecutionFailed,
98}
99
100impl AtmErrorCode {
101    pub const fn as_str(self) -> &'static str {
102        match self {
103            Self::ConfigHomeUnavailable => "ATM_CONFIG_HOME_UNAVAILABLE",
104            Self::ConfigParseFailed => "ATM_CONFIG_PARSE_FAILED",
105            Self::ConfigRetiredHookMembersKey => "ATM_CONFIG_RETIRED_HOOK_MEMBERS_KEY",
106            Self::ConfigRetiredLegacyHookKeys => "ATM_CONFIG_RETIRED_LEGACY_HOOK_KEYS",
107            Self::ConfigTeamParseFailed => "ATM_CONFIG_TEAM_PARSE_FAILED",
108            Self::ConfigTeamMissing => "ATM_CONFIG_TEAM_MISSING",
109            Self::IdentityUnavailable => "ATM_IDENTITY_UNAVAILABLE",
110            Self::AddressParseFailed => "ATM_ADDRESS_PARSE_FAILED",
111            Self::TeamUnavailable => "ATM_TEAM_UNAVAILABLE",
112            Self::TeamNotFound => "ATM_TEAM_NOT_FOUND",
113            Self::AgentNotFound => "ATM_AGENT_NOT_FOUND",
114            Self::MailboxReadFailed => "ATM_MAILBOX_READ_FAILED",
115            Self::MailboxWriteFailed => "ATM_MAILBOX_WRITE_FAILED",
116            Self::MailboxLockFailed => "ATM_MAILBOX_LOCK_FAILED",
117            Self::MailboxLockReadOnlyFilesystem => "ATM_MAILBOX_LOCK_READ_ONLY_FILESYSTEM",
118            Self::MailboxLockTimeout => "ATM_MAILBOX_LOCK_TIMEOUT",
119            Self::MessageValidationFailed => "ATM_MESSAGE_VALIDATION_FAILED",
120            Self::SerializationFailed => "ATM_SERIALIZATION_FAILED",
121            Self::FilePolicyRejected => "ATM_FILE_POLICY_REJECTED",
122            Self::FileReferenceRewriteFailed => "ATM_FILE_REFERENCE_REWRITE_FAILED",
123            Self::WaitTimeout => "ATM_WAIT_TIMEOUT",
124            Self::AckInvalidState => "ATM_ACK_INVALID_STATE",
125            Self::ClearInvalidState => "ATM_CLEAR_INVALID_STATE",
126            Self::ObservabilityEmitFailed => "ATM_OBSERVABILITY_EMIT_FAILED",
127            Self::ObservabilityQueryFailed => "ATM_OBSERVABILITY_QUERY_FAILED",
128            Self::ObservabilityFollowFailed => "ATM_OBSERVABILITY_FOLLOW_FAILED",
129            Self::ObservabilityHealthFailed => "ATM_OBSERVABILITY_HEALTH_FAILED",
130            Self::ObservabilityBootstrapFailed => "ATM_OBSERVABILITY_BOOTSTRAP_FAILED",
131            Self::ObservabilityHealthOk => "ATM_OBSERVABILITY_HEALTH_OK",
132            Self::WarningInvalidTeamMemberSkipped => "ATM_WARNING_INVALID_TEAM_MEMBER_SKIPPED",
133            Self::WarningMailboxRecordSkipped => "ATM_WARNING_MAILBOX_RECORD_SKIPPED",
134            Self::WarningMalformedAtmFieldIgnored => "ATM_WARNING_MALFORMED_ATM_FIELD_IGNORED",
135            Self::WarningObservabilityHealthDegraded => "ATM_WARNING_OBSERVABILITY_HEALTH_DEGRADED",
136            Self::WarningOriginInboxEntrySkipped => "ATM_WARNING_ORIGIN_INBOX_ENTRY_SKIPPED",
137            Self::WarningMissingTeamConfigFallback => "ATM_WARNING_MISSING_TEAM_CONFIG_FALLBACK",
138            Self::WarningSendAlertStateDegraded => "ATM_WARNING_SEND_ALERT_STATE_DEGRADED",
139            Self::WarningIdentityDrift => "ATM_WARNING_IDENTITY_DRIFT",
140            Self::WarningBaselineMemberMissing => "ATM_WARNING_BASELINE_MEMBER_MISSING",
141            Self::WarningRestoreInProgress => "ATM_WARNING_RESTORE_IN_PROGRESS",
142            Self::WarningStaleMailboxLock => "ATM_WARNING_STALE_MAILBOX_LOCK",
143            Self::WarningHookSkipped => "ATM_WARNING_HOOK_SKIPPED",
144            Self::WarningHookExecutionFailed => "ATM_WARNING_HOOK_EXECUTION_FAILED",
145        }
146    }
147}
148
149impl FromStr for AtmErrorCode {
150    type Err = &'static str;
151
152    fn from_str(value: &str) -> Result<Self, Self::Err> {
153        match value {
154            "ATM_CONFIG_HOME_UNAVAILABLE" => Ok(Self::ConfigHomeUnavailable),
155            "ATM_CONFIG_PARSE_FAILED" => Ok(Self::ConfigParseFailed),
156            "ATM_CONFIG_RETIRED_HOOK_MEMBERS_KEY" => Ok(Self::ConfigRetiredHookMembersKey),
157            "ATM_CONFIG_RETIRED_LEGACY_HOOK_KEYS" => Ok(Self::ConfigRetiredLegacyHookKeys),
158            "ATM_CONFIG_TEAM_PARSE_FAILED" => Ok(Self::ConfigTeamParseFailed),
159            "ATM_CONFIG_TEAM_MISSING" => Ok(Self::ConfigTeamMissing),
160            "ATM_IDENTITY_UNAVAILABLE" => Ok(Self::IdentityUnavailable),
161            "ATM_ADDRESS_PARSE_FAILED" => Ok(Self::AddressParseFailed),
162            "ATM_TEAM_UNAVAILABLE" => Ok(Self::TeamUnavailable),
163            "ATM_TEAM_NOT_FOUND" => Ok(Self::TeamNotFound),
164            "ATM_AGENT_NOT_FOUND" => Ok(Self::AgentNotFound),
165            "ATM_MAILBOX_READ_FAILED" => Ok(Self::MailboxReadFailed),
166            "ATM_MAILBOX_WRITE_FAILED" => Ok(Self::MailboxWriteFailed),
167            "ATM_MAILBOX_LOCK_FAILED" => Ok(Self::MailboxLockFailed),
168            "ATM_MAILBOX_LOCK_READ_ONLY_FILESYSTEM" => Ok(Self::MailboxLockReadOnlyFilesystem),
169            "ATM_MAILBOX_LOCK_TIMEOUT" => Ok(Self::MailboxLockTimeout),
170            "ATM_MESSAGE_VALIDATION_FAILED" => Ok(Self::MessageValidationFailed),
171            "ATM_SERIALIZATION_FAILED" => Ok(Self::SerializationFailed),
172            "ATM_FILE_POLICY_REJECTED" => Ok(Self::FilePolicyRejected),
173            "ATM_FILE_REFERENCE_REWRITE_FAILED" => Ok(Self::FileReferenceRewriteFailed),
174            "ATM_WAIT_TIMEOUT" => Ok(Self::WaitTimeout),
175            "ATM_ACK_INVALID_STATE" => Ok(Self::AckInvalidState),
176            "ATM_CLEAR_INVALID_STATE" => Ok(Self::ClearInvalidState),
177            "ATM_OBSERVABILITY_EMIT_FAILED" => Ok(Self::ObservabilityEmitFailed),
178            "ATM_OBSERVABILITY_QUERY_FAILED" => Ok(Self::ObservabilityQueryFailed),
179            "ATM_OBSERVABILITY_FOLLOW_FAILED" => Ok(Self::ObservabilityFollowFailed),
180            "ATM_OBSERVABILITY_HEALTH_FAILED" => Ok(Self::ObservabilityHealthFailed),
181            "ATM_OBSERVABILITY_BOOTSTRAP_FAILED" => Ok(Self::ObservabilityBootstrapFailed),
182            "ATM_OBSERVABILITY_HEALTH_OK" => Ok(Self::ObservabilityHealthOk),
183            "ATM_WARNING_INVALID_TEAM_MEMBER_SKIPPED" => Ok(Self::WarningInvalidTeamMemberSkipped),
184            "ATM_WARNING_MAILBOX_RECORD_SKIPPED" => Ok(Self::WarningMailboxRecordSkipped),
185            "ATM_WARNING_MALFORMED_ATM_FIELD_IGNORED" => Ok(Self::WarningMalformedAtmFieldIgnored),
186            "ATM_WARNING_OBSERVABILITY_HEALTH_DEGRADED" => {
187                Ok(Self::WarningObservabilityHealthDegraded)
188            }
189            "ATM_WARNING_ORIGIN_INBOX_ENTRY_SKIPPED" => Ok(Self::WarningOriginInboxEntrySkipped),
190            "ATM_WARNING_MISSING_TEAM_CONFIG_FALLBACK" => {
191                Ok(Self::WarningMissingTeamConfigFallback)
192            }
193            "ATM_WARNING_SEND_ALERT_STATE_DEGRADED" => Ok(Self::WarningSendAlertStateDegraded),
194            "ATM_WARNING_IDENTITY_DRIFT" => Ok(Self::WarningIdentityDrift),
195            "ATM_WARNING_BASELINE_MEMBER_MISSING" => Ok(Self::WarningBaselineMemberMissing),
196            "ATM_WARNING_RESTORE_IN_PROGRESS" => Ok(Self::WarningRestoreInProgress),
197            "ATM_WARNING_STALE_MAILBOX_LOCK" => Ok(Self::WarningStaleMailboxLock),
198            "ATM_WARNING_HOOK_SKIPPED" => Ok(Self::WarningHookSkipped),
199            "ATM_WARNING_HOOK_EXECUTION_FAILED" => Ok(Self::WarningHookExecutionFailed),
200            _ => Err("unknown ATM error code"),
201        }
202    }
203}
204
205impl fmt::Display for AtmErrorCode {
206    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
207        f.write_str(self.as_str())
208    }
209}
210
211impl Serialize for AtmErrorCode {
212    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
213    where
214        S: serde::Serializer,
215    {
216        serializer.serialize_str(self.as_str())
217    }
218}
219
220impl<'de> Deserialize<'de> for AtmErrorCode {
221    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
222    where
223        D: Deserializer<'de>,
224    {
225        let value = String::deserialize(deserializer)?;
226        value.parse().map_err(serde::de::Error::custom)
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::AtmErrorCode;
233
234    #[test]
235    fn error_code_round_trips_through_json_string() {
236        let encoded =
237            serde_json::to_string(&AtmErrorCode::MailboxLockReadOnlyFilesystem).expect("serialize");
238        assert_eq!(encoded, "\"ATM_MAILBOX_LOCK_READ_ONLY_FILESYSTEM\"");
239
240        let decoded: AtmErrorCode = serde_json::from_str(&encoded).expect("deserialize");
241        assert_eq!(decoded, AtmErrorCode::MailboxLockReadOnlyFilesystem);
242    }
243}