termivar_scanner/decision_runner/
failures.rs1use super::{
4 DecisionEvidenceReceipt, DecisionExecutionFailureReceipt, DecisionExecutionRequest,
5 DecisionExecutionStage, DecisionLoopError, DecisionReasoningCommitReceipt, Deserialize,
6 EntityId, Error, KnowledgeBaseError, PayloadStrategyRef, RuntimeLimitExceeded, Serialize,
7};
8
9#[derive(Debug, Clone, PartialEq, Eq, Error)]
11#[error("{message}")]
12pub struct DecisionExecutorError {
13 message: String,
14 kind: DecisionExecutionFailureKind,
15 receipt: Option<Box<DecisionExecutionFailureReceipt>>,
16 runtime_limit: Option<Box<RuntimeLimitExceeded>>,
17}
18
19impl DecisionExecutorError {
20 pub fn new(message: impl Into<String>) -> Self {
26 Self::with_kind(DecisionExecutionFailureKind::ExecutorFailure, message)
27 }
28
29 pub fn with_kind(kind: DecisionExecutionFailureKind, message: impl Into<String>) -> Self {
31 let message = message.into();
32 Self {
33 message: if message.trim().is_empty() {
34 "executor failed without a diagnostic".to_owned()
35 } else {
36 message
37 },
38 kind,
39 receipt: None,
40 runtime_limit: None,
41 }
42 }
43
44 pub(crate) fn from_runtime_limit(limit: RuntimeLimitExceeded) -> Self {
45 Self {
46 message: limit.to_string(),
47 kind: DecisionExecutionFailureKind::BlockedByPolicy,
48 receipt: None,
49 runtime_limit: Some(Box::new(limit)),
50 }
51 }
52
53 pub fn message(&self) -> &str {
55 &self.message
56 }
57
58 pub fn kind(&self) -> DecisionExecutionFailureKind {
60 self.kind
61 }
62
63 pub fn execution_failure(&self) -> Option<&DecisionExecutionFailureReceipt> {
66 self.receipt.as_deref()
67 }
68
69 pub fn runtime_limit(&self) -> Option<&RuntimeLimitExceeded> {
71 self.runtime_limit.as_deref()
72 }
73
74 pub(super) fn with_execution_context(
75 mut self,
76 request: DecisionExecutionRequest,
77 executor_id: String,
78 ) -> Self {
79 self.receipt = Some(Box::new(DecisionExecutionFailureReceipt {
80 request,
81 executor_id,
82 diagnostic: self.message.clone(),
83 kind: self.kind,
84 runtime_limit: self.runtime_limit.as_deref().cloned(),
85 }));
86 self
87 }
88
89 fn into_execution_failure(self) -> Option<DecisionExecutionFailureReceipt> {
90 self.receipt.map(|receipt| *receipt)
91 }
92
93 fn into_runtime_limit(self) -> Option<RuntimeLimitExceeded> {
94 self.runtime_limit.map(|limit| *limit)
95 }
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
105#[serde(rename_all = "snake_case")]
106#[non_exhaustive]
107pub enum DecisionExecutionFailureKind {
108 NotApplicable,
110 BlockedByPolicy,
112 TransportFailure,
114 RequestTimeout,
116 ExecutorFailure,
118}
119
120#[derive(Debug, Error)]
122#[non_exhaustive]
123pub enum DecisionRunnerError {
124 #[error("{field} must not be empty")]
126 EmptyValue { field: &'static str },
127
128 #[error("executor identity {executor_id} is already registered")]
130 ExecutorIdentityConflict { executor_id: String },
131
132 #[error("{stage} action {action_id} already has a different executor route")]
134 ActionRouteConflict {
135 stage: DecisionExecutionStage,
137 action_id: String,
139 },
140
141 #[error("decision executor {executor_id} is not registered")]
143 UnknownExecutor { executor_id: String },
144
145 #[error("{stage} action {action_id} has no executor route")]
147 MissingActionRoute {
148 stage: DecisionExecutionStage,
150 action_id: String,
152 },
153
154 #[error("decision executor {executor_id} does not support payload strategy {strategy}")]
156 UnsupportedPayloadStrategy {
157 executor_id: String,
159 strategy: PayloadStrategyRef,
161 },
162
163 #[error("command {command} does not execute an action")]
165 NonExecutionCommand { command: &'static str },
166
167 #[error("command case {actual} does not match outstanding case {expected}")]
169 CommandCaseMismatch { expected: String, actual: String },
170
171 #[error("cannot execute {expected} evidence while decision session is {actual}")]
173 UnexpectedSessionState {
174 expected: DecisionExecutionStage,
176 actual: &'static str,
178 },
179
180 #[error("active execution receipt did not capture a baseline snapshot")]
182 MissingActiveBaseline,
183
184 #[error("command {command} requires explicit host suppression context before execution")]
187 HostPolicyContextRequired {
188 command: &'static str,
190 },
191
192 #[error("host policy suppresses action {action_id} before execution")]
194 ActionSuppressedByHostPolicy {
195 action_id: String,
197 },
198
199 #[error("defense enforcement suppresses action {action_id} before execution")]
201 ActionSuppressedByDefense {
202 action_id: String,
204 },
205
206 #[error("evidence {evidence_id} subject {actual} does not match case subject {expected}")]
208 EvidenceSubjectMismatch {
209 evidence_id: String,
211 expected: EntityId,
213 actual: EntityId,
215 },
216
217 #[error("evidence {evidence_id} source {actual} does not match executor {expected}")]
219 EvidenceSourceMismatch {
220 evidence_id: String,
222 expected: String,
224 actual: String,
226 },
227
228 #[error("evidence {evidence_id} correlation does not match case {expected}")]
230 EvidenceCorrelationMismatch {
231 evidence_id: String,
233 expected: String,
235 actual: Option<String>,
237 },
238
239 #[error("decision executor {executor_id} failed: {source}")]
241 Executor {
242 executor_id: String,
244 #[source]
246 source: DecisionExecutorError,
247 },
248
249 #[error("decision transition failed after evidence was committed: {source}")]
251 OutcomeAfterEvidenceCommit {
252 receipt: Box<DecisionEvidenceReceipt>,
254 #[source]
256 source: Box<DecisionRunnerError>,
257 },
258
259 #[error(transparent)]
261 Knowledge(#[from] KnowledgeBaseError),
262
263 #[error(transparent)]
265 Decision(#[from] DecisionLoopError),
266}
267
268impl DecisionRunnerError {
269 pub fn runtime_limit(&self) -> Option<&RuntimeLimitExceeded> {
271 match self {
272 Self::Executor { source, .. } => source.runtime_limit(),
273 _ => None,
274 }
275 }
276
277 pub fn into_runtime_limit(self) -> Option<RuntimeLimitExceeded> {
279 match self {
280 Self::Executor { source, .. } => source.into_runtime_limit(),
281 _ => None,
282 }
283 }
284
285 pub fn execution_failure(&self) -> Option<&DecisionExecutionFailureReceipt> {
287 match self {
288 Self::Executor { source, .. } => source.execution_failure(),
289 _ => None,
290 }
291 }
292
293 pub fn into_execution_failure(self) -> Option<DecisionExecutionFailureReceipt> {
295 match self {
296 Self::Executor { source, .. } => source.into_execution_failure(),
297 _ => None,
298 }
299 }
300
301 pub fn committed_evidence(&self) -> Option<&DecisionEvidenceReceipt> {
303 match self {
304 Self::OutcomeAfterEvidenceCommit { receipt, .. } => Some(receipt),
305 _ => None,
306 }
307 }
308
309 pub fn into_committed_evidence(self) -> Option<DecisionEvidenceReceipt> {
311 match self {
312 Self::OutcomeAfterEvidenceCommit { receipt, .. } => Some(*receipt),
313 _ => None,
314 }
315 }
316
317 pub fn committed_reasoning(&self) -> Option<&DecisionReasoningCommitReceipt> {
319 match self {
320 Self::Decision(source) => source.committed_reasoning(),
321 Self::OutcomeAfterEvidenceCommit { source, .. } => source.committed_reasoning(),
322 _ => None,
323 }
324 }
325
326 pub fn into_committed_reasoning(self) -> Option<DecisionReasoningCommitReceipt> {
328 match self {
329 Self::Decision(source) => source.into_committed_reasoning(),
330 Self::OutcomeAfterEvidenceCommit { source, .. } => source.into_committed_reasoning(),
331 _ => None,
332 }
333 }
334}