Skip to main content

kernel/error/
code.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4// Copyright (c) 2026 2youg1 and the sprawling contributors
5
6//! Error codes: the closed AxCode set and its carrier events.
7//! The part `crates/kernel/spec/Error.lean` specifies this module with the
8//! rest of `kernel::error`.
9
10use serde::{Deserialize, Serialize};
11
12use crate::event::EventKind;
13
14/// Where an AxCode surfaces in history: its carrier event, or the loadtime
15/// class (the closed C9 exception: the Ledger is not writable yet).
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum Carrier {
18    Event(EventKind),
19    Loadtime,
20}
21
22/// Closed set of error codes.
23/// Extension is additive only; the wire spelling lives in [`AxCode::as_str`].
24#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
25pub enum AxCode {
26    // Base table (15).
27    PathNotFound,
28    ToolUnknown,
29    ToolUnavailable,
30    InvalidArgs,
31    OutsideWriteDomain,
32    VersionConflict,
33    GateDenied,
34    BudgetExhausted,
35    Timeout,
36    Provider,
37    EvidenceMissing,
38    LoopSuspected,
39    LocatorInvalid,
40    SandboxDenied,
41    /// The address or resource this names is taken right now.
42    ///
43    /// Distinct from `WorktreeBusy` and `RepairBusy`, which are
44    /// about those two mechanisms in particular: this one says a
45    /// *run* is working where the caller wants to act, and names the
46    /// run so the caller can stop it and act.
47    Busy,
48    // Collaboration (5). A sixth, `SignalUnknown`, was defined away:
49    // signal payloads are written and read by one module, the kind is
50    // an exhaustive enum, and a kind this version does not know
51    // can only come from a newer binary's ledger — which the version
52    // door already refuses.
53    DraftStale,
54    GoalConflict,
55    TaintedAction,
56    RepairBusy,
57    DelegationDepth,
58    // Governance and facilities (15).
59    ApprovalPending,
60    ApprovalDenied,
61    CrossBuildingDenied,
62    DigestSuspect,
63    CredentialMissing,
64    /// No model is chosen for the tag a caller asked for. Apart from
65    /// `ConfigInvalid` because its recovery differs: a new city starts
66    /// here and is sent to the settings page, while `ConfigInvalid`
67    /// also answers a room whose frozen model changed, which a new
68    /// session recovers.
69    ModelUnchosen,
70    ConfigInvalid,
71    CasCorrupt,
72    StorageFatal,
73    WorktreeBusy,
74    BrowserUnavailable,
75    EndpointDialectUnsupported,
76    WireMismatch,
77    LogVersionUnsupported,
78    /// Another process holds this city's Ledger for writing. Loadtime,
79    /// because the process it refuses is the one that may not write.
80    LedgerHeld,
81    /// A served city is still proving the history it opened from, and
82    /// its writer takes no line until the proof is whole. Loadtime for
83    /// the same reason as `LedgerHeld`: the refused process may not write
84    /// yet (`crates/storage/Spec.lean` §8-30).
85    HistoryUnproven,
86    // Privacy and Discard (2).
87    SecretEgress,
88    DiscardIrreversible,
89    // Backpressure (1). A shed signal is a refused delivery, not a lost
90    // one: the line is on the ledger, the queue never took it, and the
91    // caller learns it here rather than by reading the queue back.
92    BackpressureShed,
93    // Unknown outcome (1).
94    ToolOutcomeUnknown,
95    // Plan (1). A standing goal set on a building with no plan; the
96    // subject is always `<building address>: <goal>`, which is the shape
97    // the client's form recovery reads.
98    PlanMissing,
99    // Accounts (1). Every redeemable account of a provider was tried in
100    // one round and none answered; the carrier is `E_PROVIDER`'s, because
101    // the fact is the same one, said by every account
102    // (`crates/kernel/spec/AccountRecovery.lean` §8-86).
103    ProviderAccountsExhausted,
104    // Admission (2). An ACP agent answered that it needs a login first;
105    // a pairing or open code was refused at this machine's door.
106    AuthRequired,
107    PairingRefused,
108}
109
110impl AxCode {
111    /// Every code, in the order the SPEC table lists them. Data face for tests and
112    /// `xtask specalign`.
113    pub const ALL: [AxCode; 44] = [
114        AxCode::PathNotFound,
115        AxCode::ToolUnknown,
116        AxCode::ToolUnavailable,
117        AxCode::InvalidArgs,
118        AxCode::OutsideWriteDomain,
119        AxCode::VersionConflict,
120        AxCode::GateDenied,
121        AxCode::BudgetExhausted,
122        AxCode::Timeout,
123        AxCode::Provider,
124        AxCode::EvidenceMissing,
125        AxCode::LoopSuspected,
126        AxCode::LocatorInvalid,
127        AxCode::SandboxDenied,
128        AxCode::Busy,
129        AxCode::DraftStale,
130        AxCode::GoalConflict,
131        AxCode::TaintedAction,
132        AxCode::RepairBusy,
133        AxCode::DelegationDepth,
134        AxCode::ApprovalPending,
135        AxCode::ApprovalDenied,
136        AxCode::CrossBuildingDenied,
137        AxCode::DigestSuspect,
138        AxCode::CredentialMissing,
139        AxCode::ModelUnchosen,
140        AxCode::ConfigInvalid,
141        AxCode::CasCorrupt,
142        AxCode::StorageFatal,
143        AxCode::WorktreeBusy,
144        AxCode::BrowserUnavailable,
145        AxCode::EndpointDialectUnsupported,
146        AxCode::WireMismatch,
147        AxCode::LogVersionUnsupported,
148        AxCode::LedgerHeld,
149        AxCode::HistoryUnproven,
150        AxCode::SecretEgress,
151        AxCode::DiscardIrreversible,
152        AxCode::BackpressureShed,
153        AxCode::ToolOutcomeUnknown,
154        AxCode::PlanMissing,
155        AxCode::ProviderAccountsExhausted,
156        AxCode::AuthRequired,
157        AxCode::PairingRefused,
158    ];
159
160    /// The wire spelling. Sole spelling authority; serde and Display reuse it.
161    pub fn as_str(&self) -> &'static str {
162        match self {
163            AxCode::PathNotFound => "E_PATH_NOT_FOUND",
164            AxCode::ToolUnknown => "E_TOOL_UNKNOWN",
165            AxCode::ToolUnavailable => "E_TOOL_UNAVAILABLE",
166            AxCode::InvalidArgs => "E_INVALID_ARGS",
167            AxCode::OutsideWriteDomain => "E_OUTSIDE_WRITE_DOMAIN",
168            AxCode::VersionConflict => "E_VERSION_CONFLICT",
169            AxCode::GateDenied => "E_GATE_DENIED",
170            AxCode::BudgetExhausted => "E_BUDGET_EXHAUSTED",
171            AxCode::Timeout => "E_TIMEOUT",
172            AxCode::Provider => "E_PROVIDER",
173            AxCode::EvidenceMissing => "E_EVIDENCE_MISSING",
174            AxCode::LoopSuspected => "E_LOOP_SUSPECTED",
175            AxCode::LocatorInvalid => "E_LOCATOR_INVALID",
176            AxCode::SandboxDenied => "E_SANDBOX_DENIED",
177            AxCode::Busy => "E_BUSY",
178            AxCode::DraftStale => "E_DRAFT_STALE",
179            AxCode::GoalConflict => "E_GOAL_CONFLICT",
180            AxCode::TaintedAction => "E_TAINTED_ACTION",
181            AxCode::RepairBusy => "E_REPAIR_BUSY",
182            AxCode::DelegationDepth => "E_DELEGATION_DEPTH",
183            AxCode::ApprovalPending => "E_APPROVAL_PENDING",
184            AxCode::ApprovalDenied => "E_APPROVAL_DENIED",
185            AxCode::CrossBuildingDenied => "E_CROSS_BUILDING_DENIED",
186            AxCode::DigestSuspect => "E_DIGEST_SUSPECT",
187            AxCode::CredentialMissing => "E_CREDENTIAL_MISSING",
188            AxCode::ModelUnchosen => "E_MODEL_UNCHOSEN",
189            AxCode::ConfigInvalid => "E_CONFIG_INVALID",
190            AxCode::CasCorrupt => "E_CAS_CORRUPT",
191            AxCode::StorageFatal => "E_STORAGE_FATAL",
192            AxCode::WorktreeBusy => "E_WORKTREE_BUSY",
193            AxCode::BrowserUnavailable => "E_BROWSER_UNAVAILABLE",
194            AxCode::EndpointDialectUnsupported => "E_ENDPOINT_DIALECT_UNSUPPORTED",
195            AxCode::WireMismatch => "E_WIRE_MISMATCH",
196            AxCode::LogVersionUnsupported => "E_LOG_VERSION_UNSUPPORTED",
197            AxCode::LedgerHeld => "E_LEDGER_HELD",
198            AxCode::HistoryUnproven => "E_HISTORY_UNPROVEN",
199            AxCode::SecretEgress => "E_SECRET_EGRESS",
200            AxCode::BackpressureShed => "E_BACKPRESSURE_SHED",
201            AxCode::DiscardIrreversible => "E_DISCARD_IRREVERSIBLE",
202            AxCode::ToolOutcomeUnknown => "E_TOOL_OUTCOME_UNKNOWN",
203            AxCode::PlanMissing => "E_PLAN_MISSING",
204            AxCode::ProviderAccountsExhausted => "E_PROVIDER_ACCOUNTS_EXHAUSTED",
205            AxCode::AuthRequired => "E_AUTH_REQUIRED",
206            AxCode::PairingRefused => "E_PAIRING_REFUSED",
207        }
208    }
209
210    /// Wire spelling back to code; `None` is the caller's fail-closed branch.
211    pub fn parse(raw: &str) -> Option<AxCode> {
212        AxCode::ALL.into_iter().find(|code| code.as_str() == raw)
213    }
214
215    /// The carrier declaration (C9): which event carries this code into
216    /// history. Sole declaration site, exhaustive on purpose — a new code
217    /// without a carrier decision is a compile error. The loadtime arm is
218    /// the closed whitelist of codes that arise only while this process
219    /// cannot write the Ledger; `crates/kernel/Spec.lean` §12 gives each one's
220    /// reason.
221    pub fn carrier(&self) -> Carrier {
222        match self {
223            AxCode::GateDenied
224            | AxCode::OutsideWriteDomain
225            | AxCode::TaintedAction
226            | AxCode::CrossBuildingDenied
227            | AxCode::DiscardIrreversible
228            | AxCode::SecretEgress
229            | AxCode::DelegationDepth => Carrier::Event(EventKind::GateDenied),
230            AxCode::ApprovalPending => Carrier::Event(EventKind::ApprovalRequested),
231            AxCode::ApprovalDenied => Carrier::Event(EventKind::ApprovalResolved),
232            AxCode::BudgetExhausted => Carrier::Event(EventKind::BudgetLimit),
233            AxCode::LoopSuspected => Carrier::Event(EventKind::WatchdogFired),
234            AxCode::Provider | AxCode::ProviderAccountsExhausted => {
235                Carrier::Event(EventKind::ProviderDegraded)
236            }
237            AxCode::EndpointDialectUnsupported => Carrier::Event(EventKind::EndpointLost),
238            AxCode::ConfigInvalid
239            | AxCode::CasCorrupt
240            | AxCode::StorageFatal
241            | AxCode::WireMismatch
242            | AxCode::LogVersionUnsupported
243            | AxCode::LedgerHeld
244            | AxCode::HistoryUnproven => Carrier::Loadtime,
245            AxCode::PathNotFound
246            | AxCode::ToolUnknown
247            | AxCode::ToolUnavailable
248            | AxCode::InvalidArgs
249            | AxCode::VersionConflict
250            | AxCode::Timeout
251            | AxCode::EvidenceMissing
252            | AxCode::LocatorInvalid
253            | AxCode::SandboxDenied
254            | AxCode::Busy
255            | AxCode::DraftStale
256            | AxCode::GoalConflict
257            | AxCode::RepairBusy
258            | AxCode::DigestSuspect
259            | AxCode::CredentialMissing
260            | AxCode::ModelUnchosen
261            | AxCode::WorktreeBusy
262            | AxCode::BrowserUnavailable
263            | AxCode::ToolOutcomeUnknown
264            | AxCode::BackpressureShed
265            | AxCode::PlanMissing
266            | AxCode::AuthRequired
267            | AxCode::PairingRefused => Carrier::Event(EventKind::ToolResult),
268        }
269    }
270}
271
272impl std::fmt::Display for AxCode {
273    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
274        f.write_str(self.as_str())
275    }
276}
277
278impl Serialize for AxCode {
279    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
280        serializer.serialize_str(self.as_str())
281    }
282}
283
284impl<'de> Deserialize<'de> for AxCode {
285    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
286        let raw = String::deserialize(deserializer)?;
287        AxCode::parse(&raw)
288            .ok_or_else(|| serde::de::Error::custom(format_args!("unknown AxCode `{raw}`")))
289    }
290}
291
292#[cfg(test)]
293#[allow(
294    clippy::unwrap_used,
295    clippy::expect_used,
296    clippy::panic,
297    clippy::indexing_slicing,
298    reason = "test code"
299)]
300mod tests {
301    use super::*;
302    use std::collections::BTreeSet;
303
304    #[test]
305    fn axcode_is_the_declared_length_and_spelling_is_bijective() {
306        // The length is the close of the table, so it is stated once:
307        // a code added without a spelling, or two codes sharing one,
308        // fails here rather than at a caller.
309        assert_eq!(AxCode::ALL.len(), 44);
310        let spellings: BTreeSet<&str> = AxCode::ALL.iter().map(AxCode::as_str).collect();
311        assert_eq!(spellings.len(), 44);
312        for s in &spellings {
313            assert!(s.starts_with("E_"));
314        }
315    }
316
317    #[test]
318    fn axcode_parse_roundtrips_every_variant() {
319        for code in AxCode::ALL {
320            assert_eq!(AxCode::parse(code.as_str()), Some(code));
321        }
322        assert_eq!(AxCode::parse("E_NO_SUCH_CODE"), None);
323    }
324
325    #[test]
326    fn axcode_serde_uses_the_wire_spelling() {
327        let json = serde_json::to_string(&AxCode::PathNotFound).unwrap();
328        assert_eq!(json, "\"E_PATH_NOT_FOUND\"");
329        let back: AxCode = serde_json::from_str("\"E_LOCATOR_INVALID\"").unwrap();
330        assert_eq!(back, AxCode::LocatorInvalid);
331        assert!(serde_json::from_str::<AxCode>("\"E_BOGUS\"").is_err());
332    }
333}