Skip to main content

khive_types/
refusal.rs

1//! Stable refusal classifications emitted by operator-facing command surfaces.
2//!
3//! The token spellings are a machine contract. This vocabulary is closed (only
4//! the variants below are accepted) and append-only: variants may be added, but
5//! an existing token must never be renamed or reused for a different meaning.
6
7use core::fmt;
8
9use crate::UnknownVariant;
10
11/// Stable reason attached to a refused `kkernel exec` invocation or operation.
12#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
15#[non_exhaustive]
16pub enum RefusalReason {
17    /// The resolved actor was anonymous where attribution was required.
18    AnonymousActor,
19    /// The resolved actor did not match `--expect-actor`.
20    ExpectActorMismatch,
21    /// A write was refused by the content secret gate.
22    GateRefusal,
23    /// `--strict` observed at least one failed or aborted operation.
24    StrictOpFailure,
25    /// The supplied operation expression could not be parsed.
26    ParseError,
27    /// The requested verb was unknown or was not loaded.
28    VerbRefused,
29    /// A write was refused by the immutable stream record policy.
30    PolicyRefusal,
31}
32
33impl RefusalReason {
34    /// Every currently defined reason, in documentation order.
35    pub const ALL: [Self; 7] = [
36        Self::AnonymousActor,
37        Self::ExpectActorMismatch,
38        Self::GateRefusal,
39        Self::StrictOpFailure,
40        Self::ParseError,
41        Self::VerbRefused,
42        Self::PolicyRefusal,
43    ];
44
45    /// Exact machine token written to stderr and JSON envelopes.
46    pub const fn as_str(self) -> &'static str {
47        match self {
48            Self::AnonymousActor => "anonymous-actor",
49            Self::ExpectActorMismatch => "expect-actor-mismatch",
50            Self::GateRefusal => "gate-refusal",
51            Self::StrictOpFailure => "strict-op-failure",
52            Self::ParseError => "parse-error",
53            Self::VerbRefused => "verb-refused",
54            Self::PolicyRefusal => "policy-refusal",
55        }
56    }
57
58    /// Parse one exact machine token without accepting aliases or case folding.
59    pub fn from_token(token: &str) -> Option<Self> {
60        match token {
61            "anonymous-actor" => Some(Self::AnonymousActor),
62            "expect-actor-mismatch" => Some(Self::ExpectActorMismatch),
63            "gate-refusal" => Some(Self::GateRefusal),
64            "strict-op-failure" => Some(Self::StrictOpFailure),
65            "parse-error" => Some(Self::ParseError),
66            "verb-refused" => Some(Self::VerbRefused),
67            "policy-refusal" => Some(Self::PolicyRefusal),
68            _ => None,
69        }
70    }
71}
72
73impl fmt::Display for RefusalReason {
74    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75        f.write_str(self.as_str())
76    }
77}
78
79impl core::str::FromStr for RefusalReason {
80    type Err = UnknownVariant;
81
82    fn from_str(value: &str) -> Result<Self, Self::Err> {
83        Self::from_token(value).ok_or_else(|| {
84            UnknownVariant::new(
85                "refusal_reason",
86                value,
87                &[
88                    "anonymous-actor",
89                    "expect-actor-mismatch",
90                    "gate-refusal",
91                    "strict-op-failure",
92                    "parse-error",
93                    "verb-refused",
94                    "policy-refusal",
95                ],
96            )
97        })
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn token_vocabulary_is_exact_and_append_only() {
107        let tokens = RefusalReason::ALL.map(RefusalReason::as_str);
108        assert_eq!(
109            tokens,
110            [
111                "anonymous-actor",
112                "expect-actor-mismatch",
113                "gate-refusal",
114                "strict-op-failure",
115                "parse-error",
116                "verb-refused",
117                "policy-refusal",
118            ]
119        );
120        for (reason, token) in RefusalReason::ALL.into_iter().zip(tokens) {
121            assert_eq!(RefusalReason::from_token(token), Some(reason));
122            assert_eq!(token.parse::<RefusalReason>().unwrap(), reason);
123        }
124        assert_eq!(RefusalReason::from_token("Gate-Refusal"), None);
125        assert_eq!(RefusalReason::from_token("gate_refusal"), None);
126        assert_eq!(RefusalReason::from_token("Policy-Refusal"), None);
127        assert_eq!(RefusalReason::from_token("policy_refusal"), None);
128    }
129
130    #[cfg(feature = "serde")]
131    #[test]
132    fn serde_uses_the_machine_token() {
133        for reason in RefusalReason::ALL {
134            let encoded = serde_json::to_string(&reason).unwrap();
135            assert_eq!(encoded, alloc::format!("\"{}\"", reason.as_str()));
136            assert_eq!(
137                serde_json::from_str::<RefusalReason>(&encoded).unwrap(),
138                reason
139            );
140        }
141    }
142}