Skip to main content

chio_kernel/kernel/
mod.rs

1use std::sync::Arc;
2
3use chio_appraisal::VerifiedRuntimeAttestationRecord;
4use chio_core::receipt::metadata::GuardEvidence;
5use dashmap::DashMap;
6
7use crate::budget_store::BudgetCommitMetadata;
8use crate::*;
9
10#[path = "admission_coordinator.rs"]
11mod admission_coordinator;
12mod error;
13mod kernel_drop_guard;
14mod kernel_scopes;
15mod kernel_struct;
16
17pub use construction::KernelBuildError;
18pub use error::{
19    HotPathStage, KernelError, OverloadResource, SettlementRuntimeConfigError,
20    StructuredErrorReport,
21};
22pub use kernel_struct::{
23    ChioKernel, HotPathDeadlineConfig, HybridSigningConfig, KernelConfig, MemoryBudgetConfig,
24    DEFAULT_CHECKPOINT_BATCH_SIZE, DEFAULT_MAX_SIZE_BYTES, DEFAULT_MAX_STREAM_DURATION_SECS,
25    DEFAULT_MAX_STREAM_TOTAL_BYTES, DEFAULT_RECEIPT_APPEND_BUDGET_MS,
26    DEFAULT_RECEIPT_WRITER_POLL_MS, DEFAULT_RECEIPT_WRITER_STALL_MS, DEFAULT_RETENTION_DAYS,
27    MIN_RECEIPT_APPEND_BUDGET_MS,
28};
29
30pub(crate) use admission_coordinator::{
31    DurableAdmissionRuntime, DurableToolAdmission, DurableToolReturnInput,
32};
33pub(crate) use kernel_drop_guard::{PostAdmissionDropGuard, PostAdmissionReceiptContext};
34pub(crate) use kernel_scopes::{
35    current_scoped_receipt_federation_admission, current_scoped_receipt_tenant_id,
36    extract_tenant_id_from_auth_context, scope_receipt_federation_admission,
37    scope_receipt_tenant_id, ReceiptFederationAdmission, ScopedKernelReceiptFederationAdmission,
38    ScopedKernelReceiptTenantId,
39};
40pub(crate) use kernel_struct::{
41    capability_crypto_floor, receipt_crypto_floor, ReservedSiblingShare, RestartReservedHoldGate,
42};
43
44pub type AgentId = String;
45
46/// A string-typed capability identifier.
47pub type CapabilityId = String;
48
49/// A string-typed server identifier.
50pub type ServerId = String;
51
52/// Deny reason surfaced by every evaluate path when the emergency kill
53/// switch is engaged. Exposed as `pub` so HTTP adapters and SDKs can
54/// pattern-match on the exact string without drifting.
55pub const EMERGENCY_STOP_DENY_REASON: &str = "kernel emergency stop active";
56
57/// Context passed to optional runtime admission hooks after capability,
58/// request matching, governed-admission, and guard checks pass, but before
59/// dispatch and federation co-signing side effects.
60pub struct RuntimeAdmissionContext<'a> {
61    pub request: &'a ToolCallRequest,
62    pub extra_metadata: Option<&'a serde_json::Value>,
63    pub now_unix_secs: u64,
64    pub now_unix_ms: u64,
65    pub matched_grant_index: Option<usize>,
66    pub local_kernel_id: String,
67}
68
69/// Decision returned by a runtime admission hook.
70#[derive(Debug, Clone, PartialEq)]
71pub struct RuntimeAdmissionDecision {
72    pub allowed: bool,
73    pub reason: Option<String>,
74    pub metadata: Option<serde_json::Value>,
75}
76
77impl RuntimeAdmissionDecision {
78    #[must_use]
79    pub fn allow(metadata: Option<serde_json::Value>) -> Self {
80        Self {
81            allowed: true,
82            reason: None,
83            metadata,
84        }
85    }
86
87    #[must_use]
88    pub fn deny(reason: impl Into<String>, metadata: Option<serde_json::Value>) -> Self {
89        Self {
90            allowed: false,
91            reason: Some(reason.into()),
92            metadata,
93        }
94    }
95}
96
97/// Optional pre-dispatch admission hook for product-specific runtime gates.
98pub trait RuntimeAdmissionHook: Send + Sync {
99    fn name(&self) -> &str;
100
101    fn evaluate(
102        &self,
103        context: &RuntimeAdmissionContext<'_>,
104    ) -> Result<RuntimeAdmissionDecision, KernelError>;
105
106    fn release_reserved(&self, _metadata: &serde_json::Value) -> Result<(), KernelError> {
107        Ok(())
108    }
109}
110
111#[derive(Debug, serde::Deserialize)]
112#[serde(deny_unknown_fields)]
113struct KernelFederationTreatyDsseMetadata {
114    capability_lease_ref: chio_federation::bilateral_dsse::CapabilityLeaseRef,
115    policy_evaluation_summary: chio_federation::bilateral_dsse::PolicyEvaluationSummary,
116    #[serde(default)]
117    governance_receipt_ref: Option<chio_federation::bilateral_dsse::GovernanceReceiptRef>,
118    #[serde(default)]
119    consistency_anchor: Option<String>,
120    #[serde(default)]
121    consistency_model: Option<String>,
122    #[serde(default)]
123    cross_org_visibility: Option<String>,
124    treaty_binding_ref: chio_federation::bilateral_dsse::TreatyBindingRef,
125}
126
127#[derive(Debug)]
128pub(crate) struct ReceiptContent {
129    pub(crate) content_hash: String,
130    pub(crate) metadata: Option<serde_json::Value>,
131    /// The exact byte preimage `content_hash` was computed over, carried so the
132    /// signing boundary can independently recompute the hash and refuse to sign
133    /// on mismatch (WYSIWYS). For value outputs this is the RFC 8785
134    /// canonical JSON; for streams the concatenated per-chunk digest preimage;
135    /// for the empty output the literal `null` canonicalization.
136    pub(crate) canonical_content: Vec<u8>,
137}
138
139#[derive(Debug, Clone, Default)]
140pub(crate) struct ValidatedGovernedCallChainProof {
141    upstream_proof: Option<chio_core::capability::governance::GovernedUpstreamCallChainProof>,
142    continuation_token_id: Option<String>,
143    session_anchor_id: Option<String>,
144}
145
146#[derive(Debug, Clone, Default)]
147pub(crate) struct ValidatedGovernedAdmission {
148    call_chain_proof: Option<ValidatedGovernedCallChainProof>,
149    verified_runtime_attestation: Option<VerifiedRuntimeAttestationRecord>,
150    verified_payee_binding: Option<VerifiedGovernedPayeeBinding>,
151    approval_intent_hash: String,
152    approval_reservation: Option<VerifiedApprovalReservation>,
153}
154
155#[derive(Debug, Clone)]
156pub(crate) struct VerifiedApprovalReservation {
157    pub(crate) threshold_proposal_hash: String,
158    pub(crate) approval_set_hash: String,
159    pub(crate) threshold_replay: Option<ThresholdApprovalReplayReservationV1>,
160}
161
162#[derive(Debug)]
163pub(crate) struct VerifiedThresholdApprovalSet {
164    pub(crate) requirement: chio_core::capability::threshold_approval::ThresholdApprovalRequirement,
165    pub(crate) body: chio_core::capability::governance::VerifiedApprovalSetBody,
166    pub(crate) replay: ThresholdApprovalReplayReservationV1,
167}
168
169pub(crate) enum BudgetAdmissionOutcome {
170    Authorized {
171        grant_index: usize,
172        mutation: Box<PreExecutionBudgetMutation>,
173    },
174    PendingApproval {
175        grant_index: usize,
176        proposal: Box<chio_core::capability::governance::ThresholdApprovalProposal>,
177    },
178}
179
180#[cfg(test)]
181impl BudgetAdmissionOutcome {
182    pub(crate) fn into_authorized(
183        self,
184    ) -> Result<(usize, PreExecutionBudgetMutation), KernelError> {
185        match self {
186            Self::Authorized {
187                grant_index,
188                mutation,
189            } => Ok((grant_index, *mutation)),
190            Self::PendingApproval { .. } => Err(KernelError::Internal(
191                "budget admission remained pending approval".to_owned(),
192            )),
193        }
194    }
195}
196
197pub(crate) struct GovernedValidationContext<'a> {
198    parent_context: Option<&'a OperationContext>,
199    now: u64,
200}
201
202#[derive(Debug, Clone, PartialEq, Eq)]
203pub(crate) struct VerifiedGovernedPayeeBinding {
204    beneficiary_id: String,
205    settlement_destination_ref: String,
206    payee_binding_digest: String,
207    economic_intent_digest: String,
208    pre_action_authority_digest: String,
209}
210
211impl VerifiedGovernedPayeeBinding {
212    pub(in crate::kernel) fn new(
213        beneficiary_id: String,
214        settlement_destination_ref: String,
215        economic_intent_digest: String,
216        pre_action_authority_digest: String,
217    ) -> Result<Self, chio_credit::obligation::ObligationError> {
218        let payee_binding_digest = chio_credit::obligation::derive_obligation_payee_binding_digest(
219            &beneficiary_id,
220            &settlement_destination_ref,
221        )?;
222        Ok(Self {
223            beneficiary_id,
224            settlement_destination_ref,
225            payee_binding_digest,
226            economic_intent_digest,
227            pre_action_authority_digest,
228        })
229    }
230
231    #[cfg(test)]
232    pub(crate) fn for_test(
233        beneficiary_id: &str,
234        settlement_destination_ref: &str,
235        economic_intent_digest: &str,
236        pre_action_authority_digest: &str,
237    ) -> Result<Self, chio_credit::obligation::ObligationError> {
238        Self::new(
239            beneficiary_id.to_owned(),
240            settlement_destination_ref.to_owned(),
241            economic_intent_digest.to_owned(),
242            pre_action_authority_digest.to_owned(),
243        )
244    }
245
246    #[must_use]
247    pub(crate) fn beneficiary_id(&self) -> &str {
248        &self.beneficiary_id
249    }
250
251    #[must_use]
252    pub(crate) fn settlement_destination_ref(&self) -> &str {
253        &self.settlement_destination_ref
254    }
255
256    #[must_use]
257    pub(crate) fn payee_binding_digest(&self) -> &str {
258        &self.payee_binding_digest
259    }
260
261    #[must_use]
262    pub(crate) fn economic_intent_digest(&self) -> &str {
263        &self.economic_intent_digest
264    }
265
266    #[must_use]
267    pub(crate) fn pre_action_authority_digest(&self) -> &str {
268        &self.pre_action_authority_digest
269    }
270}
271
272#[derive(Debug, Clone)]
273pub(crate) enum LocalReceiptArtifact {
274    Tool(Box<chio_core::receipt::body::ChioReceipt>),
275    Child(Box<chio_core::receipt::lineage::ChildRequestReceipt>),
276}
277
278impl LocalReceiptArtifact {
279    fn verify_signature_with_floor(
280        &self,
281        floor: chio_core::receipt::crypto_floor::ReceiptCryptoFloor,
282    ) -> Result<bool, KernelError> {
283        match self {
284            Self::Tool(receipt) => receipt.verify_signature_with_floor(floor).map_err(|error| {
285                KernelError::GovernedTransactionDenied(format!(
286                    "governed call_chain parent receipt failed signature verification: {error}"
287                ))
288            }),
289            Self::Child(receipt) => receipt.verify_signature_with_floor(floor).map_err(|error| {
290                KernelError::GovernedTransactionDenied(format!(
291                    "governed call_chain parent receipt failed signature verification: {error}"
292                ))
293            }),
294        }
295    }
296
297    fn artifact_hash(&self) -> Result<String, KernelError> {
298        let canonical = match self {
299            Self::Tool(receipt) => canonical_json_bytes(receipt),
300            Self::Child(receipt) => canonical_json_bytes(receipt),
301        }
302        .map_err(|error| {
303            KernelError::GovernedTransactionDenied(format!(
304                "failed to hash governed call_chain parent receipt: {error}"
305            ))
306        })?;
307        Ok(sha256_hex(&canonical))
308    }
309
310    fn session_anchor_reference(&self) -> Option<chio_core::session::SessionAnchorReference> {
311        let metadata = match self {
312            Self::Tool(receipt) => receipt.metadata.as_ref(),
313            Self::Child(receipt) => receipt.metadata.as_ref(),
314        };
315        extract_session_anchor_reference_from_metadata(metadata)
316    }
317}
318
319/// Bridge a sync caller to the async tool-server dispatch path.
320///
321/// Calling `futures::executor::block_on` from inside a current-thread
322/// Tokio runtime parks the very thread that the runtime needs to drive
323/// its reactor / timer wheel, and any tool-server future that awaits
324/// Tokio I/O deadlocks silently. Tokio refuses
325/// to nest `block_on` calls precisely because of this, but
326/// `futures::executor::block_on` is a different executor that does not
327/// see the surrounding runtime, so the deadlock manifests as a hung
328/// tool call rather than a typed error.
329///
330/// Three cases are distinguished:
331///   1. Multi-thread runtime active: use `block_in_place` so Tokio can
332///      move the blocking work off the runtime threads. This is the
333///      supported path.
334///   2. Current-thread runtime active: refuse fail-closed with
335///      [`KernelError::SyncBridgeIncompatibleWithCurrentThreadRuntime`].
336///      Sync callers are expected to move the host to a multi-thread runtime
337///      or call an async-native kernel entrypoint instead of this bridge.
338///   3. No runtime active: drive the future with a non-tokio executor.
339///      No surrounding runtime exists to deadlock; tool-server impls
340///      that need Tokio I/O fail when they try to spawn
341///      tasks, which is the correct, observable failure mode.
342fn block_on_async_tool_dispatch<F, T>(future: F) -> Result<T, KernelError>
343where
344    F: std::future::Future<Output = Result<T, KernelError>>,
345{
346    match tokio::runtime::Handle::try_current() {
347        Ok(handle) if handle.runtime_flavor() == tokio::runtime::RuntimeFlavor::MultiThread => {
348            tokio::task::block_in_place(|| handle.block_on(future))
349        }
350        Ok(_handle) => {
351            // Current-thread runtime active. Bridging here would deadlock
352            // any tool-server future that awaits Tokio I/O because we
353            // would park the runtime's only worker thread. Surface a
354            // typed error so the caller sees the architectural
355            // incompatibility instead of a silent hang.
356            Err(KernelError::SyncBridgeIncompatibleWithCurrentThreadRuntime)
357        }
358        Err(_) => {
359            // No Tokio runtime active. The future cannot collide with a
360            // surrounding reactor; the non-tokio executor is the safe
361            // bridge. This is the path the in-process, compute-only
362            // tool servers used in unit tests rely on.
363            futures::executor::block_on(future)
364        }
365    }
366}
367
368fn extract_session_anchor_reference_from_metadata(
369    metadata: Option<&serde_json::Value>,
370) -> Option<chio_core::session::SessionAnchorReference> {
371    let metadata = metadata?;
372    let candidates = [
373        metadata
374            .get("governed_transaction")
375            .and_then(|value| value.get("call_chain")),
376        metadata.get("lineageReferences"),
377    ];
378
379    for candidate in candidates.into_iter().flatten() {
380        let Some(session_anchor_id) = candidate
381            .get("sessionAnchorId")
382            .and_then(serde_json::Value::as_str)
383            .filter(|value| !value.trim().is_empty())
384        else {
385            continue;
386        };
387        let Some(session_anchor_hash) = candidate
388            .get("sessionAnchorHash")
389            .and_then(serde_json::Value::as_str)
390            .filter(|value| !value.trim().is_empty())
391        else {
392            continue;
393        };
394        return Some(chio_core::session::SessionAnchorReference::new(
395            session_anchor_id,
396            session_anchor_hash,
397        ));
398    }
399
400    None
401}
402
403/// A policy guard that the kernel evaluates before forwarding a tool call.
404///
405/// A guard is a pluggable policy check, adapted for the Chio tool-call
406/// context. Each guard inspects the request and returns a verdict.
407#[derive(Debug, Clone)]
408pub struct GuardDecision {
409    pub verdict: Verdict,
410    pub evidence: Vec<GuardEvidence>,
411}
412
413impl GuardDecision {
414    #[must_use]
415    pub fn allow() -> Self {
416        Self {
417            verdict: Verdict::Allow,
418            evidence: Vec::new(),
419        }
420    }
421
422    #[must_use]
423    pub fn allow_with_evidence(evidence: Vec<GuardEvidence>) -> Self {
424        Self {
425            verdict: Verdict::Allow,
426            evidence,
427        }
428    }
429
430    #[must_use]
431    pub fn deny(evidence: Vec<GuardEvidence>) -> Self {
432        Self {
433            verdict: Verdict::Deny,
434            evidence,
435        }
436    }
437
438    #[must_use]
439    pub fn pending_approval(evidence: Vec<GuardEvidence>) -> Self {
440        Self {
441            verdict: Verdict::PendingApproval,
442            evidence,
443        }
444    }
445
446    #[must_use]
447    pub fn from_verdict(verdict: Verdict) -> Self {
448        match verdict {
449            Verdict::Allow => Self::allow(),
450            Verdict::Deny => Self::deny(Vec::new()),
451            Verdict::PendingApproval => Self::pending_approval(Vec::new()),
452        }
453    }
454}
455
456impl PartialEq<Verdict> for GuardDecision {
457    fn eq(&self, other: &Verdict) -> bool {
458        self.verdict == *other
459    }
460}
461
462impl PartialEq<GuardDecision> for Verdict {
463    fn eq(&self, other: &GuardDecision) -> bool {
464        *self == other.verdict
465    }
466}
467
468pub trait Guard: Send + Sync {
469    /// Human-readable guard name (e.g., "forbidden-path").
470    fn name(&self) -> &str;
471
472    /// Evaluate the guard against a tool call request.
473    ///
474    /// Returns an allow or deny decision with optional evidence, or `Err` on
475    /// internal failure (which the kernel treats as deny).
476    fn evaluate(&self, ctx: &GuardContext) -> Result<GuardDecision, KernelError>;
477}
478
479/// Context passed to guards during evaluation.
480pub struct GuardContext<'a> {
481    /// The tool call request being evaluated.
482    pub request: &'a ToolCallRequest,
483    /// The verified capability scope.
484    pub scope: &'a ChioScope,
485    /// The agent making the request.
486    pub agent_id: &'a AgentId,
487    /// The target server.
488    pub server_id: &'a ServerId,
489    /// Session-scoped enforceable filesystem roots, when the request is being
490    /// evaluated through the supported session-backed runtime path.
491    pub session_filesystem_roots: Option<&'a [String]>,
492    /// Index of the matched grant in the capability's scope, populated by
493    /// check_and_increment_budget before guards run.
494    pub matched_grant_index: Option<usize>,
495}
496
497/// Trait representing a resource provider.
498pub trait ResourceProvider: Send + Sync {
499    /// List the resources this provider exposes.
500    fn list_resources(&self) -> Vec<ResourceDefinition>;
501
502    /// List parameterized resource templates.
503    fn list_resource_templates(&self) -> Vec<ResourceTemplateDefinition> {
504        vec![]
505    }
506
507    /// Read a resource by URI. Returns `Ok(None)` when the provider does not own the URI.
508    fn read_resource(&self, uri: &str) -> Result<Option<Vec<ResourceContent>>, KernelError>;
509
510    /// Return completions for a resource template or URI reference.
511    fn complete_resource_argument(
512        &self,
513        _uri: &str,
514        _argument_name: &str,
515        _value: &str,
516        _context: &serde_json::Value,
517    ) -> Result<Option<CompletionResult>, KernelError> {
518        Ok(None)
519    }
520}
521
522/// Trait representing a prompt provider.
523pub trait PromptProvider: Send + Sync {
524    /// List available prompts.
525    fn list_prompts(&self) -> Vec<PromptDefinition>;
526
527    /// Retrieve a prompt by name. Returns `Ok(None)` when the provider does not own the prompt.
528    fn get_prompt(
529        &self,
530        name: &str,
531        arguments: serde_json::Value,
532    ) -> Result<Option<PromptResult>, KernelError>;
533
534    /// Return completions for a prompt argument.
535    fn complete_prompt_argument(
536        &self,
537        _name: &str,
538        _argument_name: &str,
539        _value: &str,
540        _context: &serde_json::Value,
541    ) -> Result<Option<CompletionResult>, KernelError> {
542        Ok(None)
543    }
544}
545
546/// Default capacity for a process-local receipt mirror when constructed without
547/// an explicit budget (tests / benches). The kernel construction path threads
548/// the configured `MemoryBudgetConfig::receipt_mirror_capacity` instead.
549const DEFAULT_RECEIPT_MIRROR_CAPACITY: usize = 4096;
550
551/// In-memory bounded ring of signed receipts. Process-local inspection mirror;
552/// a durable receipt store is authoritative for id lookups.
553///
554/// `Clone` yields a read-only snapshot (used by the `receipt_log()` accessor).
555#[derive(Clone)]
556pub struct ReceiptLog {
557    ring: chio_bounded::Ring<ChioReceipt>,
558}
559
560impl ReceiptLog {
561    pub fn new() -> Self {
562        Self::with_capacity(
563            DEFAULT_RECEIPT_MIRROR_CAPACITY,
564            chio_bounded::SizeGauge::new(),
565        )
566    }
567
568    pub fn with_capacity(capacity: usize, gauge: chio_bounded::SizeGauge) -> Self {
569        Self {
570            ring: chio_bounded::Ring::with_capacity(capacity, gauge),
571        }
572    }
573
574    pub fn append(&mut self, receipt: ChioReceipt) {
575        // Evicted receipts are already durably persisted (the store write in
576        // record_chio_receipt precedes this mirror append) or ephemeral by
577        // policy, so dropping the evicted item is safe. Caveat: for an
578        // append-only/remote store that does NOT implement point lookups, this
579        // mirror is the only lookup source, so eviction here
580        // makes an older receipt unresolvable and parent-receipt call-chain
581        // validation fails closed. Such deployments must implement
582        // ReceiptStore::load_chio_receipt (see has_local_receipt_id).
583        let _evicted = self.ring.push(receipt);
584    }
585
586    pub fn len(&self) -> usize {
587        self.ring.len()
588    }
589
590    pub fn is_empty(&self) -> bool {
591        self.ring.is_empty()
592    }
593
594    pub fn iter(&self) -> impl Iterator<Item = &ChioReceipt> {
595        self.ring.iter()
596    }
597
598    /// Cloned snapshot of the mirror (process-local inspection). Bounded by the
599    /// ring capacity.
600    pub fn receipts(&self) -> Vec<ChioReceipt> {
601        self.ring.iter().cloned().collect()
602    }
603
604    pub fn get(&self, index: usize) -> Option<&ChioReceipt> {
605        self.ring.iter().nth(index)
606    }
607}
608
609impl Default for ReceiptLog {
610    fn default() -> Self {
611        Self::new()
612    }
613}
614
615/// In-memory bounded ring of signed child-request receipts.
616#[derive(Clone)]
617pub struct ChildReceiptLog {
618    ring: chio_bounded::Ring<ChildRequestReceipt>,
619}
620
621impl ChildReceiptLog {
622    pub fn new() -> Self {
623        Self::with_capacity(
624            DEFAULT_RECEIPT_MIRROR_CAPACITY,
625            chio_bounded::SizeGauge::new(),
626        )
627    }
628
629    pub fn with_capacity(capacity: usize, gauge: chio_bounded::SizeGauge) -> Self {
630        Self {
631            ring: chio_bounded::Ring::with_capacity(capacity, gauge),
632        }
633    }
634
635    pub fn append(&mut self, receipt: ChildRequestReceipt) {
636        let _evicted = self.ring.push(receipt);
637    }
638
639    pub fn len(&self) -> usize {
640        self.ring.len()
641    }
642
643    pub fn is_empty(&self) -> bool {
644        self.ring.is_empty()
645    }
646
647    pub fn iter(&self) -> impl Iterator<Item = &ChildRequestReceipt> {
648        self.ring.iter()
649    }
650
651    /// Cloned snapshot of the mirror (process-local inspection). Bounded by the
652    /// ring capacity.
653    pub fn receipts(&self) -> Vec<ChildRequestReceipt> {
654        self.ring.iter().cloned().collect()
655    }
656
657    pub fn get(&self, index: usize) -> Option<&ChildRequestReceipt> {
658        self.ring.iter().nth(index)
659    }
660}
661
662impl Default for ChildReceiptLog {
663    fn default() -> Self {
664        Self::new()
665    }
666}
667
668#[derive(Clone, Copy)]
669pub(crate) struct MatchingGrant<'a> {
670    pub(crate) index: usize,
671    pub(crate) grant: &'a ToolGrant,
672    pub(crate) specificity: (u8, u8, usize),
673}
674
675/// Result of a monetary budget charge attempt.
676///
677/// Carries the accounting info needed to populate FinancialReceiptMetadata.
678pub(crate) struct BudgetChargeResult {
679    grant_index: usize,
680    cost_charged: u64,
681    currency: String,
682    budget_total: u64,
683    /// Running committed cost after this charge (used to compute budget_remaining).
684    new_committed_cost_units: u64,
685    budget_hold_id: String,
686    authorize_metadata: BudgetCommitMetadata,
687    invocation_capture: Option<Box<crate::budget_store::BudgetHoldMutationDecision>>,
688}
689
690impl BudgetChargeResult {
691    /// The rail/store hold id for the monetary budget charge, so a cleanup
692    /// fault can name the stuck budget hold that needs manual recovery.
693    fn reverse_event_id(&self) -> String {
694        let authorize_event_id = self
695            .authorize_metadata
696            .event_id
697            .as_deref()
698            .unwrap_or(&self.budget_hold_id);
699        let authorize_commit_index = self.authorize_metadata.budget_commit_index.unwrap_or(0);
700        format!("{authorize_event_id}:rollback:{authorize_commit_index}")
701    }
702
703    fn capture_invocation_event_id(&self) -> String {
704        let authorize_commit_index = self.authorize_metadata.budget_commit_index.unwrap_or(0);
705        format!(
706            "{}:capture-invocation:{authorize_commit_index}",
707            self.budget_hold_id
708        )
709    }
710
711    fn cancel_captured_before_dispatch_event_id(&self) -> String {
712        self.reverse_event_id()
713    }
714
715    fn reconcile_event_id(&self) -> String {
716        format!("{}:reconcile", self.budget_hold_id)
717    }
718}
719
720pub(crate) enum PreExecutionBudgetMutation {
721    None,
722    Invocation { grant_index: usize },
723    InvocationHold(BudgetChargeResult),
724    Charge(BudgetChargeResult),
725}
726
727impl PreExecutionBudgetMutation {
728    fn charge_result(&self) -> Option<&BudgetChargeResult> {
729        match self {
730            Self::Charge(charge) => Some(charge),
731            Self::None | Self::Invocation { .. } | Self::InvocationHold(_) => None,
732        }
733    }
734
735    fn durable_hold_result(&self) -> Option<&BudgetChargeResult> {
736        match self {
737            Self::Charge(charge) | Self::InvocationHold(charge) => Some(charge),
738            Self::None | Self::Invocation { .. } => None,
739        }
740    }
741
742    fn durable_hold_result_mut(&mut self) -> Option<&mut BudgetChargeResult> {
743        match self {
744            Self::Charge(charge) | Self::InvocationHold(charge) => Some(charge),
745            Self::Invocation { .. } | Self::None => None,
746        }
747    }
748
749    fn into_charge_result(self) -> Option<BudgetChargeResult> {
750        match self {
751            Self::Charge(charge) => Some(charge),
752            Self::None | Self::Invocation { .. } | Self::InvocationHold(_) => None,
753        }
754    }
755}
756
757struct SessionNestedFlowBridge<'a, C> {
758    sessions: &'a DashMap<SessionId, Arc<Session>>,
759    child_receipts: &'a mut Vec<ChildRequestReceipt>,
760    parent_context: &'a OperationContext,
761    allow_sampling: bool,
762    allow_sampling_tool_use: bool,
763    allow_elicitation: bool,
764    policy_hash: &'a str,
765    kernel_keypair: &'a Keypair,
766    client: &'a mut C,
767}
768
769impl<C> SessionNestedFlowBridge<'_, C> {
770    fn complete_child_request_with_receipt<T: serde::Serialize>(
771        &mut self,
772        child_context: &OperationContext,
773        operation_kind: OperationKind,
774        result: &Result<T, KernelError>,
775    ) -> Result<(), KernelError> {
776        let terminal_state = child_terminal_state(&child_context.request_id, result);
777        complete_session_request_with_terminal_state_in_sessions(
778            self.sessions,
779            &child_context.session_id,
780            &child_context.request_id,
781            terminal_state.clone(),
782        )?;
783
784        let receipt = build_child_request_receipt(
785            self.policy_hash,
786            self.kernel_keypair,
787            child_context,
788            operation_kind,
789            terminal_state,
790            child_outcome_payload(result)?,
791        )?;
792        self.child_receipts.push(receipt);
793        Ok(())
794    }
795}
796
797impl<C: NestedFlowClient> NestedFlowBridge for SessionNestedFlowBridge<'_, C> {
798    fn parent_request_id(&self) -> &RequestId {
799        &self.parent_context.request_id
800    }
801
802    fn poll_parent_cancellation(&mut self) -> Result<(), KernelError> {
803        self.client.poll_parent_cancellation(self.parent_context)
804    }
805
806    fn list_roots(&mut self) -> Result<Vec<RootDefinition>, KernelError> {
807        let (child_context, _start) = begin_child_request_in_sessions(
808            self.sessions,
809            self.parent_context,
810            nested_child_request_id(&self.parent_context.request_id, "roots"),
811            OperationKind::ListRoots,
812            None,
813            false,
814        )?;
815
816        let result = (|| {
817            let session = session_from_map(self.sessions, &child_context.session_id)?;
818            session.validate_context(&child_context)?;
819            session.ensure_operation_allowed(OperationKind::ListRoots)?;
820            if !session.peer_capabilities().supports_roots {
821                return Err(KernelError::RootsNotNegotiated);
822            }
823
824            let roots = self
825                .client
826                .list_roots(self.parent_context, &child_context)?;
827            session_from_map(self.sessions, &child_context.session_id)?
828                .replace_roots(roots.clone());
829            Ok(roots)
830        })();
831        if matches!(
832            &result,
833            Err(KernelError::RequestCancelled { request_id, .. })
834                if request_id == &child_context.request_id
835        ) {
836            session_from_map(self.sessions, &child_context.session_id)?
837                .request_cancellation(&child_context.request_id)?;
838        }
839        self.complete_child_request_with_receipt(
840            &child_context,
841            OperationKind::ListRoots,
842            &result,
843        )?;
844
845        result
846    }
847
848    fn create_message(
849        &mut self,
850        operation: CreateMessageOperation,
851    ) -> Result<CreateMessageResult, KernelError> {
852        let (child_context, _start) = begin_child_request_in_sessions(
853            self.sessions,
854            self.parent_context,
855            nested_child_request_id(&self.parent_context.request_id, "sample"),
856            OperationKind::CreateMessage,
857            None,
858            true,
859        )?;
860
861        let result = (|| {
862            validate_sampling_request_in_sessions(
863                self.sessions,
864                self.allow_sampling,
865                self.allow_sampling_tool_use,
866                &child_context,
867                &operation,
868            )?;
869            self.client
870                .create_message(self.parent_context, &child_context, &operation)
871        })();
872        if matches!(
873            &result,
874            Err(KernelError::RequestCancelled { request_id, .. })
875                if request_id == &child_context.request_id
876        ) {
877            session_from_map(self.sessions, &child_context.session_id)?
878                .request_cancellation(&child_context.request_id)?;
879        }
880        self.complete_child_request_with_receipt(
881            &child_context,
882            OperationKind::CreateMessage,
883            &result,
884        )?;
885
886        result
887    }
888
889    fn create_elicitation(
890        &mut self,
891        operation: CreateElicitationOperation,
892    ) -> Result<CreateElicitationResult, KernelError> {
893        let (child_context, _start) = begin_child_request_in_sessions(
894            self.sessions,
895            self.parent_context,
896            nested_child_request_id(&self.parent_context.request_id, "elicit"),
897            OperationKind::CreateElicitation,
898            None,
899            true,
900        )?;
901
902        let result = (|| {
903            validate_elicitation_request_in_sessions(
904                self.sessions,
905                self.allow_elicitation,
906                &child_context,
907                &operation,
908            )?;
909            self.client
910                .create_elicitation(self.parent_context, &child_context, &operation)
911        })();
912        if matches!(
913            &result,
914            Err(KernelError::RequestCancelled { request_id, .. })
915                if request_id == &child_context.request_id
916        ) {
917            session_from_map(self.sessions, &child_context.session_id)?
918                .request_cancellation(&child_context.request_id)?;
919        }
920        self.complete_child_request_with_receipt(
921            &child_context,
922            OperationKind::CreateElicitation,
923            &result,
924        )?;
925
926        result
927    }
928
929    fn notify_elicitation_completed(&mut self, elicitation_id: &str) -> Result<(), KernelError> {
930        let session = session_from_map(self.sessions, &self.parent_context.session_id)?;
931        session.validate_context(self.parent_context)?;
932        session.ensure_operation_allowed(OperationKind::ToolCall)?;
933
934        self.client
935            .notify_elicitation_completed(self.parent_context, elicitation_id)
936    }
937
938    fn notify_resource_updated(&mut self, uri: &str) -> Result<(), KernelError> {
939        let session = session_from_map(self.sessions, &self.parent_context.session_id)?;
940        session.validate_context(self.parent_context)?;
941        session.ensure_operation_allowed(OperationKind::ToolCall)?;
942
943        if !session.is_resource_subscribed(uri) {
944            return Ok(());
945        }
946
947        self.client
948            .notify_resource_updated(self.parent_context, uri)
949    }
950
951    fn notify_resources_list_changed(&mut self) -> Result<(), KernelError> {
952        let session = session_from_map(self.sessions, &self.parent_context.session_id)?;
953        session.validate_context(self.parent_context)?;
954        session.ensure_operation_allowed(OperationKind::ToolCall)?;
955
956        self.client
957            .notify_resources_list_changed(self.parent_context)
958    }
959}
960
961/// Extract a guard name from a `GuardDenied` error message shaped like
962/// `guard "<name>" denied the request` or `guard "<name>" error ...`.
963///
964/// Plan evaluation surfaces the offending guard in the per-step verdict
965/// so callers can target a specific guard when replanning. Parsing the
966/// name out of the canonical string is sufficient here; the structured
967/// denial payload is a tool-call response type and
968/// is not shared with plan evaluation.
969fn extract_guard_name(message: &str) -> Option<String> {
970    let start_marker = "guard \"";
971    let start = message.find(start_marker)? + start_marker.len();
972    let rest = &message[start..];
973    let end = rest.find('"')?;
974    Some(rest[..end].to_string())
975}
976
977fn scope_from_capability_snapshot(
978    snapshot: &crate::capability_lineage::CapabilitySnapshot,
979) -> Result<ChioScope, KernelError> {
980    serde_json::from_str(&snapshot.grants_json).map_err(|error| {
981        KernelError::Internal(format!(
982            "invalid capability snapshot scope for {}: {error}",
983            snapshot.capability_id
984        ))
985    })
986}
987
988fn validate_delegation_scope_step(
989    parent_capability_id: &str,
990    child_capability_id: &str,
991    parent_scope: &ChioScope,
992    child_scope: &ChioScope,
993    child_expires_at: u64,
994    link: &chio_core::capability::attenuation::DelegationLink,
995) -> Result<(), KernelError> {
996    validate_delegatable_subset(
997        parent_capability_id,
998        child_capability_id,
999        parent_scope,
1000        child_scope,
1001    )?;
1002    validate_declared_attenuations(child_capability_id, child_scope, child_expires_at, link)?;
1003    Ok(())
1004}
1005
1006fn validate_delegatable_subset(
1007    parent_capability_id: &str,
1008    child_capability_id: &str,
1009    parent_scope: &ChioScope,
1010    child_scope: &ChioScope,
1011) -> Result<(), KernelError> {
1012    for child_grant in &child_scope.grants {
1013        let allowed = parent_scope.grants.iter().any(|parent_grant| {
1014            parent_grant.operations.contains(&Operation::Delegate)
1015                && child_grant.is_subset_of(parent_grant)
1016        });
1017        if !allowed {
1018            return Err(KernelError::DelegationInvalid(format!(
1019                "parent capability {} does not authorize delegated tool grant {}/{} on child capability {}",
1020                parent_capability_id,
1021                child_grant.server_id,
1022                child_grant.tool_name,
1023                child_capability_id
1024            )));
1025        }
1026    }
1027
1028    for child_grant in &child_scope.resource_grants {
1029        let allowed = parent_scope.resource_grants.iter().any(|parent_grant| {
1030            parent_grant.operations.contains(&Operation::Delegate)
1031                && child_grant.is_subset_of(parent_grant)
1032        });
1033        if !allowed {
1034            return Err(KernelError::DelegationInvalid(format!(
1035                "parent capability {} does not authorize delegated resource grant {} on child capability {}",
1036                parent_capability_id, child_grant.uri_pattern, child_capability_id
1037            )));
1038        }
1039    }
1040
1041    for child_grant in &child_scope.prompt_grants {
1042        let allowed = parent_scope.prompt_grants.iter().any(|parent_grant| {
1043            parent_grant.operations.contains(&Operation::Delegate)
1044                && child_grant.is_subset_of(parent_grant)
1045        });
1046        if !allowed {
1047            return Err(KernelError::DelegationInvalid(format!(
1048                "parent capability {} does not authorize delegated prompt grant {} on child capability {}",
1049                parent_capability_id, child_grant.prompt_name, child_capability_id
1050            )));
1051        }
1052    }
1053
1054    Ok(())
1055}
1056
1057fn validate_declared_attenuations(
1058    child_capability_id: &str,
1059    child_scope: &ChioScope,
1060    child_expires_at: u64,
1061    link: &chio_core::capability::attenuation::DelegationLink,
1062) -> Result<(), KernelError> {
1063    for attenuation in &link.attenuations {
1064        match attenuation {
1065            chio_core::capability::attenuation::Attenuation::RemoveTool {
1066                server_id,
1067                tool_name,
1068            } => {
1069                if child_scope
1070                    .grants
1071                    .iter()
1072                    .any(|grant| tool_grant_covers_target(grant, server_id, tool_name))
1073                {
1074                    return Err(KernelError::DelegationInvalid(format!(
1075                        "child capability {} still grants removed tool {}/{}",
1076                        child_capability_id, server_id, tool_name
1077                    )));
1078                }
1079            }
1080            chio_core::capability::attenuation::Attenuation::RemoveOperation {
1081                server_id,
1082                tool_name,
1083                operation,
1084            } => {
1085                if child_scope.grants.iter().any(|grant| {
1086                    tool_grant_covers_target(grant, server_id, tool_name)
1087                        && grant.operations.contains(operation)
1088                }) {
1089                    return Err(KernelError::DelegationInvalid(format!(
1090                        "child capability {} still grants removed operation {:?} on {}/{}",
1091                        child_capability_id, operation, server_id, tool_name
1092                    )));
1093                }
1094            }
1095            chio_core::capability::attenuation::Attenuation::AddConstraint {
1096                server_id,
1097                tool_name,
1098                constraint,
1099            } => {
1100                if child_scope.grants.iter().any(|grant| {
1101                    tool_grant_covers_target(grant, server_id, tool_name)
1102                        && !grant.constraints.contains(constraint)
1103                }) {
1104                    return Err(KernelError::DelegationInvalid(format!(
1105                        "child capability {} is missing declared constraint on {}/{}",
1106                        child_capability_id, server_id, tool_name
1107                    )));
1108                }
1109            }
1110            chio_core::capability::attenuation::Attenuation::ReduceBudget {
1111                server_id,
1112                tool_name,
1113                max_invocations,
1114            } => {
1115                if child_scope.grants.iter().any(|grant| {
1116                    tool_grant_covers_target(grant, server_id, tool_name)
1117                        && grant
1118                            .max_invocations
1119                            .is_none_or(|value| value > *max_invocations)
1120                }) {
1121                    return Err(KernelError::DelegationInvalid(format!(
1122                        "child capability {} exceeds declared invocation budget on {}/{}",
1123                        child_capability_id, server_id, tool_name
1124                    )));
1125                }
1126            }
1127            chio_core::capability::attenuation::Attenuation::ShortenExpiry { new_expires_at } => {
1128                if child_expires_at > *new_expires_at {
1129                    return Err(KernelError::DelegationInvalid(format!(
1130                        "child capability {} expires after declared shortened expiry {}",
1131                        child_capability_id, new_expires_at
1132                    )));
1133                }
1134            }
1135            chio_core::capability::attenuation::Attenuation::ReduceCostPerInvocation {
1136                server_id,
1137                tool_name,
1138                max_cost_per_invocation,
1139            } => {
1140                if child_scope.grants.iter().any(|grant| {
1141                    tool_grant_covers_target(grant, server_id, tool_name)
1142                        && grant.max_cost_per_invocation.as_ref().is_none_or(|value| {
1143                            value.currency != max_cost_per_invocation.currency
1144                                || value.units > max_cost_per_invocation.units
1145                        })
1146                }) {
1147                    return Err(KernelError::DelegationInvalid(format!(
1148                        "child capability {} exceeds declared per-invocation cost ceiling on {}/{}",
1149                        child_capability_id, server_id, tool_name
1150                    )));
1151                }
1152            }
1153            chio_core::capability::attenuation::Attenuation::ReduceTotalCost {
1154                server_id,
1155                tool_name,
1156                max_total_cost,
1157            } => {
1158                if child_scope.grants.iter().any(|grant| {
1159                    tool_grant_covers_target(grant, server_id, tool_name)
1160                        && grant.max_total_cost.as_ref().is_none_or(|value| {
1161                            value.currency != max_total_cost.currency
1162                                || value.units > max_total_cost.units
1163                        })
1164                }) {
1165                    return Err(KernelError::DelegationInvalid(format!(
1166                        "child capability {} exceeds declared total-cost ceiling on {}/{}",
1167                        child_capability_id, server_id, tool_name
1168                    )));
1169                }
1170            }
1171        }
1172    }
1173
1174    Ok(())
1175}
1176
1177fn tool_grant_covers_target(grant: &ToolGrant, server_id: &str, tool_name: &str) -> bool {
1178    (grant.server_id == "*" || grant.server_id == server_id)
1179        && (grant.tool_name == "*" || grant.tool_name == tool_name)
1180}
1181
1182/// Parameters for building a receipt.
1183pub(crate) struct ReceiptParams<'a> {
1184    request_id: Option<&'a str>,
1185    capability_id: &'a str,
1186    tool_name: &'a str,
1187    server_id: &'a str,
1188    decision: Decision,
1189    action: ToolCallAction,
1190    content_hash: String,
1191    /// Byte preimage `content_hash` was computed over. The signing boundary
1192    /// recomputes `sha256_hex(canonical_content)` and refuses to sign when it
1193    /// disagrees with `content_hash` (WYSIWYS). Always sourced from
1194    /// the matching [`ReceiptContent::canonical_content`].
1195    canonical_content: Vec<u8>,
1196    metadata: Option<serde_json::Value>,
1197    timestamp: u64,
1198    /// Strength of kernel mediation for this evaluation. Defaults to
1199    /// `Mediated` (the safest baseline) when integration adapters do not
1200    /// override it.
1201    trust_level: chio_core::receipt::kinds::TrustLevel,
1202    /// Multi-tenant receipt isolation: explicit tenant tag for
1203    /// this receipt. `None` in virtually every call site -- the evaluate
1204    /// path plumbs the resolved tenant through
1205    /// [`scope_receipt_tenant_id`] so `build_and_sign_receipt` can pick it
1206    /// up without adding a parameter to every builder signature.
1207    ///
1208    /// MUST be derived from session / auth context, not caller-provided
1209    /// request fields (see `STRUCTURAL-SECURITY-FIXES.md` section 6).
1210    tenant_id: Option<String>,
1211}
1212
1213pub(crate) fn current_unix_timestamp() -> u64 {
1214    if let Some(now) = fixed_runtime_unix_secs_for_current_thread() {
1215        return now;
1216    }
1217    SystemTime::now()
1218        .duration_since(UNIX_EPOCH)
1219        .map(|d| d.as_secs())
1220        .unwrap_or(0)
1221}
1222
1223pub(crate) fn current_unix_timestamp_ms() -> u64 {
1224    if let Some(now) = fixed_runtime_unix_secs_for_current_thread() {
1225        return now.saturating_mul(1000);
1226    }
1227    SystemTime::now()
1228        .duration_since(UNIX_EPOCH)
1229        .map(|duration| u64::try_from(duration.as_millis()).unwrap_or(u64::MAX))
1230        .unwrap_or(0)
1231}
1232
1233#[cfg(feature = "delegation")]
1234#[path = "delegation.rs"]
1235pub(crate) mod delegation;
1236// Kernel construction and configuration surface. Holds the constructor,
1237// session/store accessors, and the `set_*` / `with_*` / `register_*`
1238// configuration setters.
1239#[path = "construction.rs"]
1240mod construction;
1241// Tool-call and plan evaluation path, including the long-form evaluation
1242// cores.
1243mod evaluation;
1244// Capability and budget validation.
1245#[path = "validation.rs"]
1246mod validation;
1247// Reconcile-by-nonce and reserved-hold TTL primitives (mediated spend path).
1248#[path = "reconciliation.rs"]
1249mod reconciliation;
1250// Governed-admission validation and call-chain receipt evidence.
1251#[path = "governed_validation.rs"]
1252mod governed_validation;
1253// Guard evaluation, runtime admission, and tool dispatch.
1254#[path = "dispatch.rs"]
1255mod dispatch;
1256#[path = "evaluator.rs"]
1257pub mod evaluator;
1258mod responses;
1259#[path = "session_ops.rs"]
1260mod session_ops;
1261// Settlement observer slot. Wires `chio-settle::SettlementHook` into
1262// the post-dispatch surface so finalized receipts can be routed through
1263// the existing `chio-settle/ops.rs` pipeline. The observer is strictly
1264// post-signing: hook failures never block the dispatch path.
1265#[path = "settlement_observer.rs"]
1266pub mod settlement_observer;
1267// Mpsc-backed signing task. Owns a clone of the kernel signing keypair and
1268// pulls signing requests from a bounded `tokio::sync::mpsc` channel so receipt
1269// signing leaves the synchronous critical path.
1270#[path = "signing_task.rs"]
1271pub(crate) mod signing_task;
1272// Receipt-writer liveness watchdog. Publishes the latest verdict the
1273// pre-dispatch readiness gate reads.
1274#[path = "receipt_writer_watchdog.rs"]
1275mod receipt_writer_watchdog;
1276#[cfg(test)]
1277#[path = "tests.rs"]
1278mod tests;