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}
105
106impl AxCode {
107    /// Every code, in the order the SPEC table lists them. Data face for tests and
108    /// `xtask specalign`.
109    pub const ALL: [AxCode; 42] = [
110        AxCode::PathNotFound,
111        AxCode::ToolUnknown,
112        AxCode::ToolUnavailable,
113        AxCode::InvalidArgs,
114        AxCode::OutsideWriteDomain,
115        AxCode::VersionConflict,
116        AxCode::GateDenied,
117        AxCode::BudgetExhausted,
118        AxCode::Timeout,
119        AxCode::Provider,
120        AxCode::EvidenceMissing,
121        AxCode::LoopSuspected,
122        AxCode::LocatorInvalid,
123        AxCode::SandboxDenied,
124        AxCode::Busy,
125        AxCode::DraftStale,
126        AxCode::GoalConflict,
127        AxCode::TaintedAction,
128        AxCode::RepairBusy,
129        AxCode::DelegationDepth,
130        AxCode::ApprovalPending,
131        AxCode::ApprovalDenied,
132        AxCode::CrossBuildingDenied,
133        AxCode::DigestSuspect,
134        AxCode::CredentialMissing,
135        AxCode::ModelUnchosen,
136        AxCode::ConfigInvalid,
137        AxCode::CasCorrupt,
138        AxCode::StorageFatal,
139        AxCode::WorktreeBusy,
140        AxCode::BrowserUnavailable,
141        AxCode::EndpointDialectUnsupported,
142        AxCode::WireMismatch,
143        AxCode::LogVersionUnsupported,
144        AxCode::LedgerHeld,
145        AxCode::HistoryUnproven,
146        AxCode::SecretEgress,
147        AxCode::DiscardIrreversible,
148        AxCode::BackpressureShed,
149        AxCode::ToolOutcomeUnknown,
150        AxCode::PlanMissing,
151        AxCode::ProviderAccountsExhausted,
152    ];
153
154    /// The wire spelling. Sole spelling authority; serde and Display reuse it.
155    pub fn as_str(&self) -> &'static str {
156        match self {
157            AxCode::PathNotFound => "E_PATH_NOT_FOUND",
158            AxCode::ToolUnknown => "E_TOOL_UNKNOWN",
159            AxCode::ToolUnavailable => "E_TOOL_UNAVAILABLE",
160            AxCode::InvalidArgs => "E_INVALID_ARGS",
161            AxCode::OutsideWriteDomain => "E_OUTSIDE_WRITE_DOMAIN",
162            AxCode::VersionConflict => "E_VERSION_CONFLICT",
163            AxCode::GateDenied => "E_GATE_DENIED",
164            AxCode::BudgetExhausted => "E_BUDGET_EXHAUSTED",
165            AxCode::Timeout => "E_TIMEOUT",
166            AxCode::Provider => "E_PROVIDER",
167            AxCode::EvidenceMissing => "E_EVIDENCE_MISSING",
168            AxCode::LoopSuspected => "E_LOOP_SUSPECTED",
169            AxCode::LocatorInvalid => "E_LOCATOR_INVALID",
170            AxCode::SandboxDenied => "E_SANDBOX_DENIED",
171            AxCode::Busy => "E_BUSY",
172            AxCode::DraftStale => "E_DRAFT_STALE",
173            AxCode::GoalConflict => "E_GOAL_CONFLICT",
174            AxCode::TaintedAction => "E_TAINTED_ACTION",
175            AxCode::RepairBusy => "E_REPAIR_BUSY",
176            AxCode::DelegationDepth => "E_DELEGATION_DEPTH",
177            AxCode::ApprovalPending => "E_APPROVAL_PENDING",
178            AxCode::ApprovalDenied => "E_APPROVAL_DENIED",
179            AxCode::CrossBuildingDenied => "E_CROSS_BUILDING_DENIED",
180            AxCode::DigestSuspect => "E_DIGEST_SUSPECT",
181            AxCode::CredentialMissing => "E_CREDENTIAL_MISSING",
182            AxCode::ModelUnchosen => "E_MODEL_UNCHOSEN",
183            AxCode::ConfigInvalid => "E_CONFIG_INVALID",
184            AxCode::CasCorrupt => "E_CAS_CORRUPT",
185            AxCode::StorageFatal => "E_STORAGE_FATAL",
186            AxCode::WorktreeBusy => "E_WORKTREE_BUSY",
187            AxCode::BrowserUnavailable => "E_BROWSER_UNAVAILABLE",
188            AxCode::EndpointDialectUnsupported => "E_ENDPOINT_DIALECT_UNSUPPORTED",
189            AxCode::WireMismatch => "E_WIRE_MISMATCH",
190            AxCode::LogVersionUnsupported => "E_LOG_VERSION_UNSUPPORTED",
191            AxCode::LedgerHeld => "E_LEDGER_HELD",
192            AxCode::HistoryUnproven => "E_HISTORY_UNPROVEN",
193            AxCode::SecretEgress => "E_SECRET_EGRESS",
194            AxCode::BackpressureShed => "E_BACKPRESSURE_SHED",
195            AxCode::DiscardIrreversible => "E_DISCARD_IRREVERSIBLE",
196            AxCode::ToolOutcomeUnknown => "E_TOOL_OUTCOME_UNKNOWN",
197            AxCode::PlanMissing => "E_PLAN_MISSING",
198            AxCode::ProviderAccountsExhausted => "E_PROVIDER_ACCOUNTS_EXHAUSTED",
199        }
200    }
201
202    /// Wire spelling back to code; `None` is the caller's fail-closed branch.
203    pub fn parse(raw: &str) -> Option<AxCode> {
204        AxCode::ALL.into_iter().find(|code| code.as_str() == raw)
205    }
206
207    /// The carrier declaration (C9): which event carries this code into
208    /// history. Sole declaration site, exhaustive on purpose — a new code
209    /// without a carrier decision is a compile error. The loadtime arm is
210    /// the closed whitelist of codes that arise only while this process
211    /// cannot write the Ledger; `crates/kernel/Spec.lean` §12 gives each one's
212    /// reason.
213    pub fn carrier(&self) -> Carrier {
214        match self {
215            AxCode::GateDenied
216            | AxCode::OutsideWriteDomain
217            | AxCode::TaintedAction
218            | AxCode::CrossBuildingDenied
219            | AxCode::DiscardIrreversible
220            | AxCode::SecretEgress
221            | AxCode::DelegationDepth => Carrier::Event(EventKind::GateDenied),
222            AxCode::ApprovalPending => Carrier::Event(EventKind::ApprovalRequested),
223            AxCode::ApprovalDenied => Carrier::Event(EventKind::ApprovalResolved),
224            AxCode::BudgetExhausted => Carrier::Event(EventKind::BudgetLimit),
225            AxCode::LoopSuspected => Carrier::Event(EventKind::WatchdogFired),
226            AxCode::Provider | AxCode::ProviderAccountsExhausted => {
227                Carrier::Event(EventKind::ProviderDegraded)
228            }
229            AxCode::EndpointDialectUnsupported => Carrier::Event(EventKind::EndpointLost),
230            AxCode::ConfigInvalid
231            | AxCode::CasCorrupt
232            | AxCode::StorageFatal
233            | AxCode::WireMismatch
234            | AxCode::LogVersionUnsupported
235            | AxCode::LedgerHeld
236            | AxCode::HistoryUnproven => Carrier::Loadtime,
237            AxCode::PathNotFound
238            | AxCode::ToolUnknown
239            | AxCode::ToolUnavailable
240            | AxCode::InvalidArgs
241            | AxCode::VersionConflict
242            | AxCode::Timeout
243            | AxCode::EvidenceMissing
244            | AxCode::LocatorInvalid
245            | AxCode::SandboxDenied
246            | AxCode::Busy
247            | AxCode::DraftStale
248            | AxCode::GoalConflict
249            | AxCode::RepairBusy
250            | AxCode::DigestSuspect
251            | AxCode::CredentialMissing
252            | AxCode::ModelUnchosen
253            | AxCode::WorktreeBusy
254            | AxCode::BrowserUnavailable
255            | AxCode::ToolOutcomeUnknown
256            | AxCode::BackpressureShed
257            | AxCode::PlanMissing => Carrier::Event(EventKind::ToolResult),
258        }
259    }
260}
261
262impl std::fmt::Display for AxCode {
263    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
264        f.write_str(self.as_str())
265    }
266}
267
268impl Serialize for AxCode {
269    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
270        serializer.serialize_str(self.as_str())
271    }
272}
273
274impl<'de> Deserialize<'de> for AxCode {
275    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
276        let raw = String::deserialize(deserializer)?;
277        AxCode::parse(&raw)
278            .ok_or_else(|| serde::de::Error::custom(format_args!("unknown AxCode `{raw}`")))
279    }
280}
281
282#[cfg(test)]
283#[allow(
284    clippy::unwrap_used,
285    clippy::expect_used,
286    clippy::panic,
287    clippy::indexing_slicing,
288    reason = "test code"
289)]
290mod tests {
291    use super::*;
292    use std::collections::BTreeSet;
293
294    #[test]
295    fn axcode_is_the_declared_length_and_spelling_is_bijective() {
296        // The length is the close of the table, so it is stated once:
297        // a code added without a spelling, or two codes sharing one,
298        // fails here rather than at a caller.
299        assert_eq!(AxCode::ALL.len(), 42);
300        let spellings: BTreeSet<&str> = AxCode::ALL.iter().map(AxCode::as_str).collect();
301        assert_eq!(spellings.len(), 42);
302        for s in &spellings {
303            assert!(s.starts_with("E_"));
304        }
305    }
306
307    #[test]
308    fn axcode_parse_roundtrips_every_variant() {
309        for code in AxCode::ALL {
310            assert_eq!(AxCode::parse(code.as_str()), Some(code));
311        }
312        assert_eq!(AxCode::parse("E_NO_SUCH_CODE"), None);
313    }
314
315    #[test]
316    fn axcode_serde_uses_the_wire_spelling() {
317        let json = serde_json::to_string(&AxCode::PathNotFound).unwrap();
318        assert_eq!(json, "\"E_PATH_NOT_FOUND\"");
319        let back: AxCode = serde_json::from_str("\"E_LOCATOR_INVALID\"").unwrap();
320        assert_eq!(back, AxCode::LocatorInvalid);
321        assert!(serde_json::from_str::<AxCode>("\"E_BOGUS\"").is_err());
322    }
323}