Skip to main content

chio_kernel/kernel/
error.rs

1use super::*;
2
3#[derive(Clone, Debug, PartialEq, serde::Serialize)]
4pub struct StructuredErrorReport {
5    pub code: String,
6    pub message: String,
7    pub context: serde_json::Value,
8    pub suggested_fix: String,
9}
10
11impl StructuredErrorReport {
12    pub fn new(
13        code: impl Into<String>,
14        message: impl Into<String>,
15        context: serde_json::Value,
16        suggested_fix: impl Into<String>,
17    ) -> Self {
18        Self {
19            code: code.into(),
20            message: message.into(),
21            context,
22            suggested_fix: suggested_fix.into(),
23        }
24    }
25}
26
27/// Which bounded resource shed. Included in the receipt deny reason and the
28/// structured error report so operators can see which policy fired.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum OverloadResource {
31    ReceiptMirror,
32    FederationCache,
33    VelocityBuckets,
34    AdmissionKeys,
35    ConcurrencyBuckets,
36    SessionJournal,
37    StreamBytes,
38    StreamChunks,
39    Allocation,
40}
41
42/// Invalid settlement-runtime installation or mutation.
43#[derive(Debug, thiserror::Error, Clone, Copy, PartialEq, Eq)]
44pub enum SettlementRuntimeConfigError {
45    #[error("invalid settlement retry policy: {0}")]
46    InvalidRetryPolicy(#[from] chio_settle::RetryPolicyError),
47    #[error("settlement observer runtime requires a receipt store")]
48    MissingReceiptStore,
49    #[error("receipt store lacks timeout-aware atomic settlement observation projection")]
50    UnsupportedAtomicProjection,
51    #[error("receipt store lacks a settlement backend binding")]
52    MissingStoreBinding,
53    #[error("receipt store and outcome store use different settlement backend bindings")]
54    StoreBindingMismatch,
55    #[error("receipt store cannot be replaced while a settlement observer runtime is installed")]
56    ReceiptStoreReplacement,
57}
58
59impl SettlementRuntimeConfigError {
60    const fn as_str(self) -> &'static str {
61        match self {
62            Self::InvalidRetryPolicy(_) => "invalid_retry_policy",
63            Self::MissingReceiptStore => "missing_receipt_store",
64            Self::UnsupportedAtomicProjection => "unsupported_atomic_projection",
65            Self::MissingStoreBinding => "missing_store_binding",
66            Self::StoreBindingMismatch => "store_binding_mismatch",
67            Self::ReceiptStoreReplacement => "receipt_store_replacement",
68        }
69    }
70}
71
72/// A stage of the mediation hot path that can exceed its configured wall-clock
73/// budget. Carried in the deadline error and the structured report so operators
74/// can see which await ran long.
75#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
76#[serde(rename_all = "snake_case")]
77pub enum HotPathStage {
78    GuardPipeline,
79    Dispatch,
80    ReceiptAppend,
81}
82
83impl std::fmt::Display for HotPathStage {
84    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85        let s = match self {
86            Self::GuardPipeline => "guard_pipeline",
87            Self::Dispatch => "dispatch",
88            Self::ReceiptAppend => "receipt_append",
89        };
90        f.write_str(s)
91    }
92}
93
94/// Errors that can occur during kernel operations.
95#[derive(Debug, thiserror::Error)]
96pub enum KernelError {
97    #[error("unknown session: {0}")]
98    UnknownSession(SessionId),
99
100    #[error("session already exists: {0}")]
101    SessionAlreadyExists(SessionId),
102
103    #[error("session error: {0}")]
104    Session(#[from] SessionError),
105
106    #[error("capability has expired")]
107    CapabilityExpired,
108
109    #[error("capability not yet valid")]
110    CapabilityNotYetValid,
111
112    #[error("capability has been revoked: {0}")]
113    CapabilityRevoked(CapabilityId),
114
115    #[error("capability signature is invalid")]
116    InvalidSignature,
117
118    #[error("capability issuer is not a trusted CA")]
119    UntrustedIssuer,
120
121    #[error("capability issuance failed: {0}")]
122    CapabilityIssuanceFailed(String),
123
124    #[error("capability issuance denied: {0}")]
125    CapabilityIssuanceDenied(String),
126
127    #[error("requested tool {tool} on server {server} is not in capability scope")]
128    OutOfScope { tool: String, server: String },
129
130    #[error("requested resource {uri} is not in capability scope")]
131    OutOfScopeResource { uri: String },
132
133    #[error("requested prompt {prompt} is not in capability scope")]
134    OutOfScopePrompt { prompt: String },
135
136    #[error("invocation budget exhausted for capability {0}")]
137    BudgetExhausted(CapabilityId),
138
139    #[error("captured budget replay denied for capability {0}")]
140    CapturedBudgetReplay(CapabilityId),
141
142    #[error("direct tool dispatch is unavailable; use the full evaluation pipeline")]
143    DirectDispatchUnavailable,
144
145    #[error("request agent {actual} does not match capability subject {expected}")]
146    SubjectMismatch { expected: String, actual: String },
147
148    #[error("delegation chain revoked at ancestor {0}")]
149    DelegationChainRevoked(CapabilityId),
150
151    #[error("delegation admission failed: {0}")]
152    DelegationInvalid(String),
153
154    #[error("invalid capability constraint: {0}")]
155    InvalidConstraint(String),
156
157    #[error("governed transaction denied: {0}")]
158    GovernedTransactionDenied(String),
159
160    #[error("guard denied the request: {0}")]
161    GuardDenied(String),
162
163    #[error("tool server error: {0}")]
164    ToolServerError(String),
165
166    #[error("request stream incomplete: {0}")]
167    RequestIncomplete(String),
168
169    #[error("tool not registered: {0}")]
170    ToolNotRegistered(String),
171
172    #[error("resource not registered: {0}")]
173    ResourceNotRegistered(String),
174
175    #[error("resource read denied by session roots for {uri}: {reason}")]
176    ResourceRootDenied { uri: String, reason: String },
177
178    #[error("prompt not registered: {0}")]
179    PromptNotRegistered(String),
180
181    #[error("sampling is disabled by policy")]
182    SamplingNotAllowedByPolicy,
183
184    #[error("sampling was not negotiated with the client")]
185    SamplingNotNegotiated,
186
187    #[error("sampling context inclusion is not supported by the client")]
188    SamplingContextNotSupported,
189
190    #[error("sampling tool use is disabled by policy")]
191    SamplingToolUseNotAllowedByPolicy,
192
193    #[error("sampling tool use was not negotiated with the client")]
194    SamplingToolUseNotNegotiated,
195
196    #[error("elicitation is disabled by policy")]
197    ElicitationNotAllowedByPolicy,
198
199    #[error("elicitation was not negotiated with the client")]
200    ElicitationNotNegotiated,
201
202    #[error("elicitation form mode is not supported by the client")]
203    ElicitationFormNotSupported,
204
205    #[error("elicitation URL mode was not negotiated with the client")]
206    ElicitationUrlNotSupported,
207
208    #[error("{message}")]
209    UrlElicitationsRequired {
210        message: String,
211        elicitations: Vec<CreateElicitationOperation>,
212    },
213
214    #[error("roots/list was not negotiated with the client")]
215    RootsNotNegotiated,
216
217    #[error("sampling child requests require a ready session-bound parent request")]
218    InvalidChildRequestParent,
219
220    #[error("request {request_id} was cancelled: {reason}")]
221    RequestCancelled {
222        request_id: RequestId,
223        reason: String,
224    },
225
226    #[error("receipt signing failed: {0}")]
227    ReceiptSigningFailed(String),
228
229    #[error("receipt persistence failed: {0}")]
230    ReceiptPersistence(#[from] ReceiptStoreError),
231
232    #[error("revocation store error: {0}")]
233    RevocationStore(#[from] RevocationStoreError),
234
235    #[error("budget store error: {0}")]
236    BudgetStore(#[from] BudgetStoreError),
237
238    #[error("durable admission failed: {0}")]
239    DurableAdmission(String),
240
241    #[error(
242        "cross-currency budget enforcement failed: no price oracle configured for {base}/{quote}"
243    )]
244    NoCrossCurrencyOracle { base: String, quote: String },
245
246    #[error("cross-currency budget enforcement failed: {0}")]
247    CrossCurrencyOracle(String),
248
249    #[error("web3 evidence prerequisites unavailable: {0}")]
250    Web3EvidenceUnavailable(String),
251
252    #[error("settlement runtime configuration failed: {0}")]
253    SettlementConfiguration(#[from] SettlementRuntimeConfigError),
254
255    #[error("internal error: {0}")]
256    Internal(String),
257
258    #[error("DPoP proof verification failed: {0}")]
259    DpopVerificationFailed(String),
260
261    /// A human-in-the-loop approval token failed to satisfy
262    /// the pending approval contract (bad binding, bad signature,
263    /// expired, or replayed).
264    #[error("approval rejected: {0}")]
265    ApprovalRejected(String),
266
267    /// The sync `evaluate_tool_call` path was invoked from a context
268    /// where the only available Tokio runtime is a current-thread
269    /// runtime. The async tool-dispatch path cannot be safely driven
270    /// on a current-thread runtime: bridging via
271    /// `futures::executor::block_on` parks the caller's thread, and
272    /// Tokio I/O timers / reactor wakers cannot progress on the same
273    /// parked thread. Returning a typed error rather than deadlocking
274    /// lets callers move the dispatch onto a multi-thread runtime.
275    /// The public async `evaluate_tool_call` path is still backed by the
276    /// blocking evaluator on this branch, so it is not a current-thread
277    /// escape hatch.
278    #[error(
279        "sync tool-dispatch bridge cannot drive an async tool server on a current-thread \
280         Tokio runtime; switch the host to a multi-thread Tokio runtime"
281    )]
282    SyncBridgeIncompatibleWithCurrentThreadRuntime,
283
284    /// The reserving pre-execution authorization entry point received a request
285    /// carrying a presented execution nonce. That entry point mints nonces; it
286    /// never settles them. Accepting a presented nonce would silently skip the
287    /// reserve path and fall through to dispatch, so it is rejected fail-closed.
288    #[error(
289        "reserving authorization must not receive a presented execution nonce; this entry \
290         point mints nonces, it does not settle them"
291    )]
292    ReservingAuthorizationRejectsPresentedNonce,
293
294    /// The kernel shed load to stay within its memory budget. Always a deny;
295    /// never admits a call and never grows a collection.
296    #[error("kernel overloaded: {resource:?} at capacity")]
297    Overloaded { resource: OverloadResource },
298
299    /// A mediation-path stage exceeded its configured wall-clock budget. The
300    /// invocation is aborted fail-closed through the shared unwind path: budget
301    /// holds are reversed and, for a dispatch expiry, the runtime-admission
302    /// reservation is retained and marked auditable. Never yields Allow.
303    #[error("hot-path deadline exceeded at {stage}: budget {budget_ms}ms")]
304    HotPathDeadlineExceeded { stage: HotPathStage, budget_ms: u64 },
305
306    /// The receipt commit writer is wedged, saturated, or dead, as reported by
307    /// the receipt-writer watchdog. Enforced at the pre-dispatch readiness gate
308    /// so no tool side effect runs while receipts cannot be durably persisted.
309    #[error("receipt commit writer unavailable: {0}")]
310    ReceiptWriterUnavailable(String),
311}
312
313impl KernelError {
314    fn report_with_context(
315        &self,
316        code: &str,
317        context: serde_json::Value,
318        suggested_fix: impl Into<String>,
319    ) -> StructuredErrorReport {
320        StructuredErrorReport::new(code, self.to_string(), context, suggested_fix)
321    }
322
323    pub fn report(&self) -> StructuredErrorReport {
324        match self {
325            Self::Overloaded { resource } => self.report_with_context(
326                "CHIO-KERNEL-OVERLOADED",
327                serde_json::json!({ "resource": format!("{resource:?}") }),
328                "The kernel shed load to stay within its memory budget. Retry with backoff; \
329                 if sustained, raise the process memory budget or scale out.",
330            ),
331            Self::UnknownSession(session_id) => self.report_with_context(
332                "CHIO-KERNEL-UNKNOWN-SESSION",
333                serde_json::json!({ "session_id": session_id.to_string() }),
334                "Create the session first or reuse a session ID returned by the kernel before issuing follow-up operations.",
335            ),
336            Self::SessionAlreadyExists(session_id) => self.report_with_context(
337                "CHIO-KERNEL-SESSION-ALREADY-EXISTS",
338                serde_json::json!({ "session_id": session_id.to_string() }),
339                "Use a fresh session ID or drop the duplicate restored record before opening the session.",
340            ),
341            Self::Session(error) => self.report_with_context(
342                "CHIO-KERNEL-SESSION",
343                serde_json::json!({ "session_error": error.to_string() }),
344                "Inspect the session lifecycle and ordering of operations, then recreate the session if it is no longer valid.",
345            ),
346            Self::CapabilityExpired => self.report_with_context(
347                "CHIO-KERNEL-CAPABILITY-EXPIRED",
348                serde_json::json!({}),
349                "Refresh or reissue the capability so its validity window includes the current time.",
350            ),
351            Self::CapabilityNotYetValid => self.report_with_context(
352                "CHIO-KERNEL-CAPABILITY-NOT-YET-VALID",
353                serde_json::json!({}),
354                "Use a capability whose validity window has started, or correct the issuer clock skew if timestamps are wrong.",
355            ),
356            Self::CapabilityRevoked(capability_id) => self.report_with_context(
357                "CHIO-KERNEL-CAPABILITY-REVOKED",
358                serde_json::json!({ "capability_id": capability_id }),
359                "Request a new non-revoked capability or inspect the revocation record for this capability lineage.",
360            ),
361            Self::InvalidSignature => self.report_with_context(
362                "CHIO-KERNEL-INVALID-SIGNATURE",
363                serde_json::json!({}),
364                "Reissue the capability or receipt with the correct signing key and verify the payload was not mutated in transit.",
365            ),
366            Self::UntrustedIssuer => self.report_with_context(
367                "CHIO-KERNEL-UNTRUSTED-ISSUER",
368                serde_json::json!({}),
369                "Configure the issuing CA public key in the kernel trust set or use a capability issued by a trusted authority.",
370            ),
371            Self::CapabilityIssuanceFailed(reason) => self.report_with_context(
372                "CHIO-KERNEL-CAPABILITY-ISSUANCE-FAILED",
373                serde_json::json!({ "reason": reason }),
374                "Inspect the issuance pipeline inputs and upstream stores, then retry once the issuing dependency is healthy.",
375            ),
376            Self::CapabilityIssuanceDenied(reason) => self.report_with_context(
377                "CHIO-KERNEL-CAPABILITY-ISSUANCE-DENIED",
378                serde_json::json!({ "reason": reason }),
379                "Adjust the issuance request so it satisfies the policy, score, or trust requirements enforced by the authority.",
380            ),
381            Self::OutOfScope { tool, server } => self.report_with_context(
382                "CHIO-KERNEL-OUT-OF-SCOPE-TOOL",
383                serde_json::json!({ "tool": tool, "server": server }),
384                "Issue a capability that grants this tool on this server, or call a tool already inside the granted scope.",
385            ),
386            Self::OutOfScopeResource { uri } => self.report_with_context(
387                "CHIO-KERNEL-OUT-OF-SCOPE-RESOURCE",
388                serde_json::json!({ "uri": uri }),
389                "Issue a capability/resource grant that matches this URI, or request a resource already inside scope.",
390            ),
391            Self::OutOfScopePrompt { prompt } => self.report_with_context(
392                "CHIO-KERNEL-OUT-OF-SCOPE-PROMPT",
393                serde_json::json!({ "prompt": prompt }),
394                "Issue a capability/prompt grant that matches this prompt, or request a prompt already inside scope.",
395            ),
396            Self::BudgetExhausted(capability_id) => self.report_with_context(
397                "CHIO-KERNEL-BUDGET-EXHAUSTED",
398                serde_json::json!({ "capability_id": capability_id }),
399                "Increase the capability budget, wait for the budget window to reset, or lower the cost of the requested operation.",
400            ),
401            Self::CapturedBudgetReplay(capability_id) => self.report_with_context(
402                "CHIO-KERNEL-CAPTURED-BUDGET-REPLAY",
403                serde_json::json!({ "capability_id": capability_id }),
404                "Use a new request ID. A captured budget authorization cannot be reused by another dispatch.",
405            ),
406            Self::DirectDispatchUnavailable => self.report_with_context(
407                "CHIO-KERNEL-DIRECT-DISPATCH-UNAVAILABLE",
408                serde_json::json!({}),
409                "Use the full evaluation pipeline so admission, dispatch, compensation, and receipt persistence remain one lifecycle.",
410            ),
411            Self::SubjectMismatch { expected, actual } => self.report_with_context(
412                "CHIO-KERNEL-SUBJECT-MISMATCH",
413                serde_json::json!({ "expected": expected, "actual": actual }),
414                "Use a capability issued to the requesting subject, or correct the agent identity bound to the request.",
415            ),
416            Self::DelegationChainRevoked(capability_id) => self.report_with_context(
417                "CHIO-KERNEL-DELEGATION-CHAIN-REVOKED",
418                serde_json::json!({ "capability_id": capability_id }),
419                "Inspect the capability lineage and reissue the chain from a non-revoked ancestor.",
420            ),
421            Self::DelegationInvalid(reason) => self.report_with_context(
422                "CHIO-KERNEL-DELEGATION-INVALID",
423                serde_json::json!({ "reason": reason }),
424                "Reissue the delegated capability with a valid ancestor snapshot chain, delegator binding, attenuation proof, and delegated scope ceiling.",
425            ),
426            Self::InvalidConstraint(reason) => self.report_with_context(
427                "CHIO-KERNEL-INVALID-CONSTRAINT",
428                serde_json::json!({ "reason": reason }),
429                "Fix the capability constraint payload so it matches the kernel's supported schema and value rules.",
430            ),
431            Self::GovernedTransactionDenied(reason) => self.report_with_context(
432                "CHIO-KERNEL-GOVERNED-TRANSACTION-DENIED",
433                serde_json::json!({ "reason": reason }),
434                "Adjust the governed transaction intent so it satisfies the configured approval and policy requirements.",
435            ),
436            Self::GuardDenied(reason) => self.report_with_context(
437                "CHIO-KERNEL-GUARD-DENIED",
438                serde_json::json!({ "reason": reason }),
439                "Adjust the request or policy/guard configuration so the request satisfies the active guard pipeline.",
440            ),
441            Self::ToolServerError(reason) => self.report_with_context(
442                "CHIO-KERNEL-TOOL-SERVER",
443                serde_json::json!({ "reason": reason }),
444                "Inspect the wrapped tool server logs and protocol compatibility, then retry once the server is healthy.",
445            ),
446            Self::RequestIncomplete(reason) => self.report_with_context(
447                "CHIO-KERNEL-REQUEST-INCOMPLETE",
448                serde_json::json!({ "reason": reason }),
449                "Resubmit the request with all required fields and protocol state transitions present.",
450            ),
451            Self::ToolNotRegistered(tool) => self.report_with_context(
452                "CHIO-KERNEL-TOOL-NOT-REGISTERED",
453                serde_json::json!({ "tool": tool }),
454                "Register the tool on the target server or update the request to reference an exposed tool.",
455            ),
456            Self::ResourceNotRegistered(uri) => self.report_with_context(
457                "CHIO-KERNEL-RESOURCE-NOT-REGISTERED",
458                serde_json::json!({ "uri": uri }),
459                "Register the resource provider for this URI or request a resource that is actually exposed by the runtime.",
460            ),
461            Self::ResourceRootDenied { uri, reason } => self.report_with_context(
462                "CHIO-KERNEL-RESOURCE-ROOT-DENIED",
463                serde_json::json!({ "uri": uri, "reason": reason }),
464                "Expand the session filesystem roots if the access is intentional, or request a resource inside the approved root set.",
465            ),
466            Self::PromptNotRegistered(prompt) => self.report_with_context(
467                "CHIO-KERNEL-PROMPT-NOT-REGISTERED",
468                serde_json::json!({ "prompt": prompt }),
469                "Register the prompt provider for this prompt name or request a prompt that is actually exposed.",
470            ),
471            Self::SamplingNotAllowedByPolicy => self.report_with_context(
472                "CHIO-KERNEL-SAMPLING-NOT-ALLOWED",
473                serde_json::json!({}),
474                "Enable sampling in policy if this workflow requires it, or retry without a sampling request.",
475            ),
476            Self::SamplingNotNegotiated => self.report_with_context(
477                "CHIO-KERNEL-SAMPLING-NOT-NEGOTIATED",
478                serde_json::json!({}),
479                "Negotiate sampling support with the client before issuing sampling operations.",
480            ),
481            Self::SamplingContextNotSupported => self.report_with_context(
482                "CHIO-KERNEL-SAMPLING-CONTEXT-NOT-SUPPORTED",
483                serde_json::json!({}),
484                "Disable sampling context inclusion or upgrade the client to one that supports the negotiated feature.",
485            ),
486            Self::SamplingToolUseNotAllowedByPolicy => self.report_with_context(
487                "CHIO-KERNEL-SAMPLING-TOOL-USE-NOT-ALLOWED",
488                serde_json::json!({}),
489                "Enable sampling tool use in policy or retry without delegated tool execution inside the sampling branch.",
490            ),
491            Self::SamplingToolUseNotNegotiated => self.report_with_context(
492                "CHIO-KERNEL-SAMPLING-TOOL-USE-NOT-NEGOTIATED",
493                serde_json::json!({}),
494                "Negotiate sampling tool-use support with the client before attempting tool execution inside sampling.",
495            ),
496            Self::ElicitationNotAllowedByPolicy => self.report_with_context(
497                "CHIO-KERNEL-ELICITATION-NOT-ALLOWED",
498                serde_json::json!({}),
499                "Enable elicitation in policy or retry without requesting user input through the kernel.",
500            ),
501            Self::ElicitationNotNegotiated => self.report_with_context(
502                "CHIO-KERNEL-ELICITATION-NOT-NEGOTIATED",
503                serde_json::json!({}),
504                "Negotiate elicitation support with the client before attempting elicitation operations.",
505            ),
506            Self::ElicitationFormNotSupported => self.report_with_context(
507                "CHIO-KERNEL-ELICITATION-FORM-NOT-SUPPORTED",
508                serde_json::json!({}),
509                "Switch to a supported elicitation mode or upgrade the client to one that supports form-mode elicitation.",
510            ),
511            Self::ElicitationUrlNotSupported => self.report_with_context(
512                "CHIO-KERNEL-ELICITATION-URL-NOT-SUPPORTED",
513                serde_json::json!({}),
514                "Switch to a supported elicitation mode or negotiate URL-based elicitation support with the client.",
515            ),
516            Self::UrlElicitationsRequired {
517                message,
518                elicitations,
519            } => self.report_with_context(
520                "CHIO-KERNEL-URL-ELICITATIONS-REQUIRED",
521                serde_json::json!({
522                    "message": message,
523                    "elicitation_count": elicitations.len()
524                }),
525                "Complete the required URL-based elicitation flow and resubmit the request afterward.",
526            ),
527            Self::RootsNotNegotiated => self.report_with_context(
528                "CHIO-KERNEL-ROOTS-NOT-NEGOTIATED",
529                serde_json::json!({}),
530                "Negotiate roots/list support with the client before using root-scoped resource protections.",
531            ),
532            Self::InvalidChildRequestParent => self.report_with_context(
533                "CHIO-KERNEL-INVALID-CHILD-REQUEST-PARENT",
534                serde_json::json!({}),
535                "Create the child request from a ready session-bound parent request that is currently in flight.",
536            ),
537            Self::RequestCancelled { request_id, reason } => self.report_with_context(
538                "CHIO-KERNEL-REQUEST-CANCELLED",
539                serde_json::json!({ "request_id": request_id.to_string(), "reason": reason }),
540                "Stop using the cancelled request ID and restart the operation if the workflow still needs to continue.",
541            ),
542            Self::ReceiptSigningFailed(reason) => self.report_with_context(
543                "CHIO-KERNEL-RECEIPT-SIGNING-FAILED",
544                serde_json::json!({ "reason": reason }),
545                "Inspect the kernel signing key configuration and signing payload integrity, then retry receipt generation.",
546            ),
547            Self::ReceiptPersistence(error) => self.report_with_context(
548                "CHIO-KERNEL-RECEIPT-PERSISTENCE",
549                serde_json::json!({ "source": error.to_string() }),
550                "Check the configured receipt store connectivity, permissions, and schema health before retrying.",
551            ),
552            Self::RevocationStore(error) => self.report_with_context(
553                "CHIO-KERNEL-REVOCATION-STORE",
554                serde_json::json!({ "source": error.to_string() }),
555                "Check the configured revocation store connectivity, permissions, and schema health before retrying.",
556            ),
557            Self::BudgetStore(error) => self.report_with_context(
558                "CHIO-KERNEL-BUDGET-STORE",
559                serde_json::json!({ "source": error.to_string() }),
560                "Check the configured budget store connectivity, permissions, and schema health before retrying.",
561            ),
562            Self::DurableAdmission(reason) => self.report_with_context(
563                "CHIO-KERNEL-DURABLE-ADMISSION",
564                serde_json::json!({ "reason": reason }),
565                "Repair the fenced admission authority and reconcile the retained operation before retrying this request ID.",
566            ),
567            Self::NoCrossCurrencyOracle { base, quote } => self.report_with_context(
568                "CHIO-KERNEL-NO-CROSS-CURRENCY-ORACLE",
569                serde_json::json!({ "base": base, "quote": quote }),
570                "Configure a price oracle for this currency pair or avoid a cross-currency budget path for this request.",
571            ),
572            Self::CrossCurrencyOracle(reason) => self.report_with_context(
573                "CHIO-KERNEL-CROSS-CURRENCY-ORACLE",
574                serde_json::json!({ "reason": reason }),
575                "Inspect the price-oracle configuration and upstream quote availability for the requested currency conversion.",
576            ),
577            Self::Web3EvidenceUnavailable(reason) => self.report_with_context(
578                "CHIO-KERNEL-WEB3-EVIDENCE-UNAVAILABLE",
579                serde_json::json!({ "reason": reason }),
580                "Enable the required receipt-store, checkpoint, and oracle prerequisites before running the web3 evidence path.",
581            ),
582            Self::SettlementConfiguration(error) => self.report_with_context(
583                "CHIO-KERNEL-SETTLEMENT-CONFIGURATION",
584                serde_json::json!({ "kind": error.as_str() }),
585                "Install a receipt store and outcome store backed by the same atomic settlement writer, then correct the retry policy or backend capability before startup.",
586            ),
587            Self::Internal(reason) => self.report_with_context(
588                "CHIO-KERNEL-INTERNAL",
589                serde_json::json!({ "reason": reason }),
590                "Capture the error report and kernel logs, then treat this as a reproducible kernel bug if it persists.",
591            ),
592            Self::DpopVerificationFailed(reason) => self.report_with_context(
593                "CHIO-KERNEL-DPOP-VERIFICATION-FAILED",
594                serde_json::json!({ "reason": reason }),
595                "Attach a valid DPoP proof bound to the current capability, request, server, and tool before retrying.",
596            ),
597            Self::ApprovalRejected(reason) => self.report_with_context(
598                "CHIO-KERNEL-APPROVAL-REJECTED",
599                serde_json::json!({ "reason": reason }),
600                "Obtain a fresh approval token bound to this exact request and retry once a human approver has signed it.",
601            ),
602            Self::SyncBridgeIncompatibleWithCurrentThreadRuntime => self.report_with_context(
603                "CHIO-KERNEL-SYNC-BRIDGE-INCOMPATIBLE",
604                serde_json::json!({}),
605                "Move the host process to a multi-thread Tokio runtime so block_in_place can drive async tool dispatch. The public async evaluate_tool_call path is still backed by the blocking evaluator on this branch and is not a current-thread runtime workaround.",
606            ),
607            Self::ReservingAuthorizationRejectsPresentedNonce => self.report_with_context(
608                "CHIO-KERNEL-RESERVING-AUTHORIZATION-PRESENTED-NONCE",
609                serde_json::json!({}),
610                "Submit the reserving authorization request without a presented execution nonce; present the minted nonce to the tool server for settlement instead.",
611            ),
612            Self::HotPathDeadlineExceeded { stage, budget_ms } => self.report_with_context(
613                "CHIO-KERNEL-HOT-PATH-DEADLINE",
614                serde_json::json!({ "stage": stage, "budget_ms": budget_ms }),
615                "Raise the offending stage budget in [deadlines] if the workload is legitimately slow, or repair the slow guard, tool server, or writer. Do not retry blindly; the request already failed closed.",
616            ),
617            Self::ReceiptWriterUnavailable(reason) => self.report_with_context(
618                "CHIO-KERNEL-RECEIPT-WRITER-UNAVAILABLE",
619                serde_json::json!({ "reason": reason }),
620                "The receipt commit writer is not durably accepting writes; repair or restart the writer. Requests deny until liveness recovers.",
621            ),
622        }
623    }
624}
625
626impl From<crate::admission_operation::AdmissionOperationError> for KernelError {
627    fn from(error: crate::admission_operation::AdmissionOperationError) -> Self {
628        Self::DurableAdmission(error.to_string())
629    }
630}
631
632#[cfg(test)]
633mod overload_tests {
634    #![allow(clippy::unwrap_used, clippy::expect_used)]
635    use super::*;
636
637    #[test]
638    fn overloaded_reports_resource_and_fail_closed_code() {
639        let err = KernelError::Overloaded {
640            resource: OverloadResource::AdmissionKeys,
641        };
642        let report = err.report();
643        assert_eq!(report.code, "CHIO-KERNEL-OVERLOADED");
644        assert!(
645            err.to_string().contains("AdmissionKeys"),
646            "display must name the shed resource: {err}"
647        );
648        assert_eq!(
649            report.context.get("resource").and_then(|v| v.as_str()),
650            Some("AdmissionKeys")
651        );
652    }
653
654    #[test]
655    fn settlement_configuration_has_operator_guidance() {
656        let error = KernelError::SettlementConfiguration(
657            SettlementRuntimeConfigError::StoreBindingMismatch,
658        );
659        let report = error.report();
660
661        assert_eq!(report.code, "CHIO-KERNEL-SETTLEMENT-CONFIGURATION");
662        assert_eq!(
663            report
664                .context
665                .get("kind")
666                .and_then(serde_json::Value::as_str),
667            Some("store_binding_mismatch")
668        );
669        assert!(!report.suggested_fix.contains("kernel bug"));
670    }
671}
672
673#[cfg(test)]
674mod hot_path_error_taxonomy_tests {
675    use super::*;
676
677    #[test]
678    fn hot_path_stage_display_is_snake_case() {
679        assert_eq!(HotPathStage::GuardPipeline.to_string(), "guard_pipeline");
680        assert_eq!(HotPathStage::Dispatch.to_string(), "dispatch");
681        assert_eq!(HotPathStage::ReceiptAppend.to_string(), "receipt_append");
682    }
683
684    #[test]
685    fn hot_path_deadline_exceeded_reports_stable_code() {
686        let err = KernelError::HotPathDeadlineExceeded {
687            stage: HotPathStage::Dispatch,
688            budget_ms: 1500,
689        };
690        let report = err.report();
691        assert_eq!(report.code, "CHIO-KERNEL-HOT-PATH-DEADLINE");
692        assert!(err.to_string().contains("dispatch"));
693        assert!(err.to_string().contains("1500"));
694    }
695
696    #[test]
697    fn receipt_writer_unavailable_reports_stable_code() {
698        let err = KernelError::ReceiptWriterUnavailable("writer is Wedged".to_string());
699        assert_eq!(err.report().code, "CHIO-KERNEL-RECEIPT-WRITER-UNAVAILABLE");
700    }
701}