Skip to main content

chio_kernel/kernel/
mod.rs

1use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
2use std::sync::{Arc, Mutex, RwLock};
3
4use chio_appraisal::VerifiedRuntimeAttestationRecord;
5
6use self::responses::FinalizeToolOutputCostContext;
7use crate::budget_store::{
8    BudgetAuthorizeHoldDecision, BudgetAuthorizeHoldRequest, BudgetCommitMetadata,
9    BudgetEventAuthority, BudgetHoldMutationDecision, BudgetReconcileHoldDecision,
10    BudgetReconcileHoldRequest, BudgetReverseHoldDecision, BudgetReverseHoldRequest,
11};
12use crate::*;
13
14pub type AgentId = String;
15
16/// A string-typed capability identifier.
17pub type CapabilityId = String;
18
19/// A string-typed server identifier.
20pub type ServerId = String;
21
22/// Deny reason surfaced by every evaluate path when the emergency kill
23/// switch is engaged. Exposed as `pub` so HTTP adapters and SDKs can
24/// pattern-match on the exact string without drifting.
25pub const EMERGENCY_STOP_DENY_REASON: &str = "kernel emergency stop active";
26
27// ---------------------------------------------------------------------------
28// Phase 1.5 multi-tenant receipt isolation.
29//
30// The kernel must tag every receipt it signs with the tenant that the active
31// session belongs to. The natural place to derive that is the authenticated
32// session's `enterprise_identity.tenant_id`, but the existing response
33// builders accept only a `&ToolCallRequest` (no session handle) across ~40
34// call sites.  Rather than plumb a new parameter through every builder we
35// stash the resolved tenant_id in a thread-local scope for the duration of
36// one evaluate call; `build_and_sign_receipt` consults it when filling in the
37// receipt body.
38//
39// The scope is RAII: the guard resets the previous value on drop, which
40// keeps reentrant evaluations (e.g. a kernel that recursively evaluates a
41// sub-call inside the same thread) isolated.
42//
43// Tenant_id is NEVER extracted from the caller-provided `ToolCallRequest` --
44// allowing caller choice would defeat the isolation the store-level WHERE
45// clause enforces.
46thread_local! {
47    static RECEIPT_TENANT_ID_SCOPE: std::cell::RefCell<Option<String>> =
48        const { std::cell::RefCell::new(None) };
49}
50
51/// Guard returned by [`scope_receipt_tenant_id`]. Restores the previously
52/// active tenant scope when dropped.
53pub(crate) struct ScopedReceiptTenantId {
54    previous: Option<String>,
55}
56
57impl Drop for ScopedReceiptTenantId {
58    fn drop(&mut self) {
59        let previous = self.previous.take();
60        RECEIPT_TENANT_ID_SCOPE.with(|slot| {
61            *slot.borrow_mut() = previous;
62        });
63    }
64}
65
66/// Install `tenant_id` as the active scope for this thread until the
67/// returned guard is dropped. Passing `None` explicitly clears the scope
68/// (so a child evaluate that lacks a session cannot inherit a parent's
69/// tenant tag by accident).
70pub(crate) fn scope_receipt_tenant_id(tenant_id: Option<String>) -> ScopedReceiptTenantId {
71    let previous = RECEIPT_TENANT_ID_SCOPE.with(|slot| slot.replace(tenant_id));
72    ScopedReceiptTenantId { previous }
73}
74
75/// Read the tenant_id currently in scope on this thread.
76///
77/// Exposed to `build_and_sign_receipt` (in `responses.rs`) so the receipt
78/// body picks up the tag without rewiring every builder signature.
79pub(crate) fn current_scoped_receipt_tenant_id() -> Option<String> {
80    RECEIPT_TENANT_ID_SCOPE.with(|slot| slot.borrow().clone())
81}
82
83/// Extract tenant_id from a session's authenticated auth context.
84///
85/// Preference order:
86///   1. OAuth bearer `enterprise_identity.tenant_id` (the richer SSO
87///      claim, preferred because IdP integrations that surface full
88///      EnterpriseIdentityContext use this path).
89///   2. OAuth bearer `federated_claims.tenant_id` (the minimal OIDC
90///      claim set; populated when the IdP only emits `tid`).
91///
92/// Anonymous sessions and static-bearer sessions return `None`.
93pub(crate) fn extract_tenant_id_from_auth_context(
94    auth_context: &SessionAuthContext,
95) -> Option<String> {
96    if let chio_core::session::SessionAuthMethod::OAuthBearer {
97        enterprise_identity,
98        federated_claims,
99        ..
100    } = &auth_context.method
101    {
102        if let Some(identity) = enterprise_identity.as_ref() {
103            if let Some(id) = identity.tenant_id.as_ref() {
104                return Some(id.clone());
105            }
106        }
107        if let Some(id) = federated_claims.tenant_id.as_ref() {
108            return Some(id.clone());
109        }
110    }
111    None
112}
113
114#[derive(Debug)]
115pub(crate) struct ReceiptContent {
116    pub(crate) content_hash: String,
117    pub(crate) metadata: Option<serde_json::Value>,
118}
119
120#[derive(Debug, Clone, Default)]
121struct ValidatedGovernedCallChainProof {
122    upstream_proof: Option<chio_core::capability::GovernedUpstreamCallChainProof>,
123    continuation_token_id: Option<String>,
124    session_anchor_id: Option<String>,
125}
126
127#[derive(Debug, Clone, Default)]
128struct ValidatedGovernedAdmission {
129    call_chain_proof: Option<ValidatedGovernedCallChainProof>,
130    verified_runtime_attestation: Option<VerifiedRuntimeAttestationRecord>,
131}
132
133#[derive(Debug, Clone)]
134enum LocalReceiptArtifact {
135    Tool(chio_core::receipt::ChioReceipt),
136    Child(chio_core::receipt::ChildRequestReceipt),
137}
138
139impl LocalReceiptArtifact {
140    fn verify_signature(&self) -> Result<bool, KernelError> {
141        match self {
142            Self::Tool(receipt) => receipt.verify_signature().map_err(|error| {
143                KernelError::GovernedTransactionDenied(format!(
144                    "governed call_chain parent receipt failed signature verification: {error}"
145                ))
146            }),
147            Self::Child(receipt) => receipt.verify_signature().map_err(|error| {
148                KernelError::GovernedTransactionDenied(format!(
149                    "governed call_chain parent receipt failed signature verification: {error}"
150                ))
151            }),
152        }
153    }
154
155    fn artifact_hash(&self) -> Result<String, KernelError> {
156        let canonical = match self {
157            Self::Tool(receipt) => canonical_json_bytes(receipt),
158            Self::Child(receipt) => canonical_json_bytes(receipt),
159        }
160        .map_err(|error| {
161            KernelError::GovernedTransactionDenied(format!(
162                "failed to hash governed call_chain parent receipt: {error}"
163            ))
164        })?;
165        Ok(sha256_hex(&canonical))
166    }
167
168    fn session_anchor_reference(&self) -> Option<chio_core::session::SessionAnchorReference> {
169        let metadata = match self {
170            Self::Tool(receipt) => receipt.metadata.as_ref(),
171            Self::Child(receipt) => receipt.metadata.as_ref(),
172        };
173        extract_session_anchor_reference_from_metadata(metadata)
174    }
175}
176
177fn extract_session_anchor_reference_from_metadata(
178    metadata: Option<&serde_json::Value>,
179) -> Option<chio_core::session::SessionAnchorReference> {
180    let metadata = metadata?;
181    let candidates = [
182        metadata
183            .get("governed_transaction")
184            .and_then(|value| value.get("call_chain")),
185        metadata.get("lineageReferences"),
186    ];
187
188    for candidate in candidates.into_iter().flatten() {
189        let Some(session_anchor_id) = candidate
190            .get("sessionAnchorId")
191            .and_then(serde_json::Value::as_str)
192            .filter(|value| !value.trim().is_empty())
193        else {
194            continue;
195        };
196        let Some(session_anchor_hash) = candidate
197            .get("sessionAnchorHash")
198            .and_then(serde_json::Value::as_str)
199            .filter(|value| !value.trim().is_empty())
200        else {
201            continue;
202        };
203        return Some(chio_core::session::SessionAnchorReference::new(
204            session_anchor_id,
205            session_anchor_hash,
206        ));
207    }
208
209    None
210}
211
212#[derive(Clone, Debug, PartialEq, serde::Serialize)]
213pub struct StructuredErrorReport {
214    pub code: String,
215    pub message: String,
216    pub context: serde_json::Value,
217    pub suggested_fix: String,
218}
219
220impl StructuredErrorReport {
221    pub fn new(
222        code: impl Into<String>,
223        message: impl Into<String>,
224        context: serde_json::Value,
225        suggested_fix: impl Into<String>,
226    ) -> Self {
227        Self {
228            code: code.into(),
229            message: message.into(),
230            context,
231            suggested_fix: suggested_fix.into(),
232        }
233    }
234}
235
236/// Errors that can occur during kernel operations.
237#[derive(Debug, thiserror::Error)]
238pub enum KernelError {
239    #[error("unknown session: {0}")]
240    UnknownSession(SessionId),
241
242    #[error("session already exists: {0}")]
243    SessionAlreadyExists(SessionId),
244
245    #[error("session error: {0}")]
246    Session(#[from] SessionError),
247
248    #[error("capability has expired")]
249    CapabilityExpired,
250
251    #[error("capability not yet valid")]
252    CapabilityNotYetValid,
253
254    #[error("capability has been revoked: {0}")]
255    CapabilityRevoked(CapabilityId),
256
257    #[error("capability signature is invalid")]
258    InvalidSignature,
259
260    #[error("capability issuer is not a trusted CA")]
261    UntrustedIssuer,
262
263    #[error("capability issuance failed: {0}")]
264    CapabilityIssuanceFailed(String),
265
266    #[error("capability issuance denied: {0}")]
267    CapabilityIssuanceDenied(String),
268
269    #[error("requested tool {tool} on server {server} is not in capability scope")]
270    OutOfScope { tool: String, server: String },
271
272    #[error("requested resource {uri} is not in capability scope")]
273    OutOfScopeResource { uri: String },
274
275    #[error("requested prompt {prompt} is not in capability scope")]
276    OutOfScopePrompt { prompt: String },
277
278    #[error("invocation budget exhausted for capability {0}")]
279    BudgetExhausted(CapabilityId),
280
281    #[error("request agent {actual} does not match capability subject {expected}")]
282    SubjectMismatch { expected: String, actual: String },
283
284    #[error("delegation chain revoked at ancestor {0}")]
285    DelegationChainRevoked(CapabilityId),
286
287    #[error("delegation admission failed: {0}")]
288    DelegationInvalid(String),
289
290    #[error("invalid capability constraint: {0}")]
291    InvalidConstraint(String),
292
293    #[error("governed transaction denied: {0}")]
294    GovernedTransactionDenied(String),
295
296    #[error("guard denied the request: {0}")]
297    GuardDenied(String),
298
299    #[error("tool server error: {0}")]
300    ToolServerError(String),
301
302    #[error("request stream incomplete: {0}")]
303    RequestIncomplete(String),
304
305    #[error("tool not registered: {0}")]
306    ToolNotRegistered(String),
307
308    #[error("resource not registered: {0}")]
309    ResourceNotRegistered(String),
310
311    #[error("resource read denied by session roots for {uri}: {reason}")]
312    ResourceRootDenied { uri: String, reason: String },
313
314    #[error("prompt not registered: {0}")]
315    PromptNotRegistered(String),
316
317    #[error("sampling is disabled by policy")]
318    SamplingNotAllowedByPolicy,
319
320    #[error("sampling was not negotiated with the client")]
321    SamplingNotNegotiated,
322
323    #[error("sampling context inclusion is not supported by the client")]
324    SamplingContextNotSupported,
325
326    #[error("sampling tool use is disabled by policy")]
327    SamplingToolUseNotAllowedByPolicy,
328
329    #[error("sampling tool use was not negotiated with the client")]
330    SamplingToolUseNotNegotiated,
331
332    #[error("elicitation is disabled by policy")]
333    ElicitationNotAllowedByPolicy,
334
335    #[error("elicitation was not negotiated with the client")]
336    ElicitationNotNegotiated,
337
338    #[error("elicitation form mode is not supported by the client")]
339    ElicitationFormNotSupported,
340
341    #[error("elicitation URL mode was not negotiated with the client")]
342    ElicitationUrlNotSupported,
343
344    #[error("{message}")]
345    UrlElicitationsRequired {
346        message: String,
347        elicitations: Vec<CreateElicitationOperation>,
348    },
349
350    #[error("roots/list was not negotiated with the client")]
351    RootsNotNegotiated,
352
353    #[error("sampling child requests require a ready session-bound parent request")]
354    InvalidChildRequestParent,
355
356    #[error("request {request_id} was cancelled: {reason}")]
357    RequestCancelled {
358        request_id: RequestId,
359        reason: String,
360    },
361
362    #[error("receipt signing failed: {0}")]
363    ReceiptSigningFailed(String),
364
365    #[error("receipt persistence failed: {0}")]
366    ReceiptPersistence(#[from] ReceiptStoreError),
367
368    #[error("revocation store error: {0}")]
369    RevocationStore(#[from] RevocationStoreError),
370
371    #[error("budget store error: {0}")]
372    BudgetStore(#[from] BudgetStoreError),
373
374    #[error(
375        "cross-currency budget enforcement failed: no price oracle configured for {base}/{quote}"
376    )]
377    NoCrossCurrencyOracle { base: String, quote: String },
378
379    #[error("cross-currency budget enforcement failed: {0}")]
380    CrossCurrencyOracle(String),
381
382    #[error("web3 evidence prerequisites unavailable: {0}")]
383    Web3EvidenceUnavailable(String),
384
385    #[error("internal error: {0}")]
386    Internal(String),
387
388    #[error("DPoP proof verification failed: {0}")]
389    DpopVerificationFailed(String),
390
391    /// Phase 3.4: a human-in-the-loop approval token failed to satisfy
392    /// the pending approval contract (bad binding, bad signature,
393    /// expired, or replayed).
394    #[error("approval rejected: {0}")]
395    ApprovalRejected(String),
396}
397
398impl KernelError {
399    fn report_with_context(
400        &self,
401        code: &str,
402        context: serde_json::Value,
403        suggested_fix: impl Into<String>,
404    ) -> StructuredErrorReport {
405        StructuredErrorReport::new(code, self.to_string(), context, suggested_fix)
406    }
407
408    pub fn report(&self) -> StructuredErrorReport {
409        match self {
410            Self::UnknownSession(session_id) => self.report_with_context(
411                "CHIO-KERNEL-UNKNOWN-SESSION",
412                serde_json::json!({ "session_id": session_id.to_string() }),
413                "Create the session first or reuse a session ID returned by the kernel before issuing follow-up operations.",
414            ),
415            Self::SessionAlreadyExists(session_id) => self.report_with_context(
416                "CHIO-KERNEL-SESSION-ALREADY-EXISTS",
417                serde_json::json!({ "session_id": session_id.to_string() }),
418                "Use a fresh session ID or drop the duplicate restored record before opening the session.",
419            ),
420            Self::Session(error) => self.report_with_context(
421                "CHIO-KERNEL-SESSION",
422                serde_json::json!({ "session_error": error.to_string() }),
423                "Inspect the session lifecycle and ordering of operations, then recreate the session if it is no longer valid.",
424            ),
425            Self::CapabilityExpired => self.report_with_context(
426                "CHIO-KERNEL-CAPABILITY-EXPIRED",
427                serde_json::json!({}),
428                "Refresh or reissue the capability so its validity window includes the current time.",
429            ),
430            Self::CapabilityNotYetValid => self.report_with_context(
431                "CHIO-KERNEL-CAPABILITY-NOT-YET-VALID",
432                serde_json::json!({}),
433                "Use a capability whose validity window has started, or correct the issuer clock skew if timestamps are wrong.",
434            ),
435            Self::CapabilityRevoked(capability_id) => self.report_with_context(
436                "CHIO-KERNEL-CAPABILITY-REVOKED",
437                serde_json::json!({ "capability_id": capability_id }),
438                "Request a new non-revoked capability or inspect the revocation record for this capability lineage.",
439            ),
440            Self::InvalidSignature => self.report_with_context(
441                "CHIO-KERNEL-INVALID-SIGNATURE",
442                serde_json::json!({}),
443                "Reissue the capability or receipt with the correct signing key and verify the payload was not mutated in transit.",
444            ),
445            Self::UntrustedIssuer => self.report_with_context(
446                "CHIO-KERNEL-UNTRUSTED-ISSUER",
447                serde_json::json!({}),
448                "Configure the issuing CA public key in the kernel trust set or use a capability issued by a trusted authority.",
449            ),
450            Self::CapabilityIssuanceFailed(reason) => self.report_with_context(
451                "CHIO-KERNEL-CAPABILITY-ISSUANCE-FAILED",
452                serde_json::json!({ "reason": reason }),
453                "Inspect the issuance pipeline inputs and upstream stores, then retry once the issuing dependency is healthy.",
454            ),
455            Self::CapabilityIssuanceDenied(reason) => self.report_with_context(
456                "CHIO-KERNEL-CAPABILITY-ISSUANCE-DENIED",
457                serde_json::json!({ "reason": reason }),
458                "Adjust the issuance request so it satisfies the policy, score, or trust requirements enforced by the authority.",
459            ),
460            Self::OutOfScope { tool, server } => self.report_with_context(
461                "CHIO-KERNEL-OUT-OF-SCOPE-TOOL",
462                serde_json::json!({ "tool": tool, "server": server }),
463                "Issue a capability that grants this tool on this server, or call a tool already inside the granted scope.",
464            ),
465            Self::OutOfScopeResource { uri } => self.report_with_context(
466                "CHIO-KERNEL-OUT-OF-SCOPE-RESOURCE",
467                serde_json::json!({ "uri": uri }),
468                "Issue a capability/resource grant that matches this URI, or request a resource already inside scope.",
469            ),
470            Self::OutOfScopePrompt { prompt } => self.report_with_context(
471                "CHIO-KERNEL-OUT-OF-SCOPE-PROMPT",
472                serde_json::json!({ "prompt": prompt }),
473                "Issue a capability/prompt grant that matches this prompt, or request a prompt already inside scope.",
474            ),
475            Self::BudgetExhausted(capability_id) => self.report_with_context(
476                "CHIO-KERNEL-BUDGET-EXHAUSTED",
477                serde_json::json!({ "capability_id": capability_id }),
478                "Increase the capability budget, wait for the budget window to reset, or lower the cost of the requested operation.",
479            ),
480            Self::SubjectMismatch { expected, actual } => self.report_with_context(
481                "CHIO-KERNEL-SUBJECT-MISMATCH",
482                serde_json::json!({ "expected": expected, "actual": actual }),
483                "Use a capability issued to the requesting subject, or correct the agent identity bound to the request.",
484            ),
485            Self::DelegationChainRevoked(capability_id) => self.report_with_context(
486                "CHIO-KERNEL-DELEGATION-CHAIN-REVOKED",
487                serde_json::json!({ "capability_id": capability_id }),
488                "Inspect the capability lineage and reissue the chain from a non-revoked ancestor.",
489            ),
490            Self::DelegationInvalid(reason) => self.report_with_context(
491                "CHIO-KERNEL-DELEGATION-INVALID",
492                serde_json::json!({ "reason": reason }),
493                "Reissue the delegated capability with a valid ancestor snapshot chain, delegator binding, attenuation proof, and delegated scope ceiling.",
494            ),
495            Self::InvalidConstraint(reason) => self.report_with_context(
496                "CHIO-KERNEL-INVALID-CONSTRAINT",
497                serde_json::json!({ "reason": reason }),
498                "Fix the capability constraint payload so it matches the kernel's supported schema and value rules.",
499            ),
500            Self::GovernedTransactionDenied(reason) => self.report_with_context(
501                "CHIO-KERNEL-GOVERNED-TRANSACTION-DENIED",
502                serde_json::json!({ "reason": reason }),
503                "Adjust the governed transaction intent so it satisfies the configured approval and policy requirements.",
504            ),
505            Self::GuardDenied(reason) => self.report_with_context(
506                "CHIO-KERNEL-GUARD-DENIED",
507                serde_json::json!({ "reason": reason }),
508                "Adjust the request or policy/guard configuration so the request satisfies the active guard pipeline.",
509            ),
510            Self::ToolServerError(reason) => self.report_with_context(
511                "CHIO-KERNEL-TOOL-SERVER",
512                serde_json::json!({ "reason": reason }),
513                "Inspect the wrapped tool server logs and protocol compatibility, then retry once the server is healthy.",
514            ),
515            Self::RequestIncomplete(reason) => self.report_with_context(
516                "CHIO-KERNEL-REQUEST-INCOMPLETE",
517                serde_json::json!({ "reason": reason }),
518                "Resubmit the request with all required fields and protocol state transitions present.",
519            ),
520            Self::ToolNotRegistered(tool) => self.report_with_context(
521                "CHIO-KERNEL-TOOL-NOT-REGISTERED",
522                serde_json::json!({ "tool": tool }),
523                "Register the tool on the target server or update the request to reference an exposed tool.",
524            ),
525            Self::ResourceNotRegistered(uri) => self.report_with_context(
526                "CHIO-KERNEL-RESOURCE-NOT-REGISTERED",
527                serde_json::json!({ "uri": uri }),
528                "Register the resource provider for this URI or request a resource that is actually exposed by the runtime.",
529            ),
530            Self::ResourceRootDenied { uri, reason } => self.report_with_context(
531                "CHIO-KERNEL-RESOURCE-ROOT-DENIED",
532                serde_json::json!({ "uri": uri, "reason": reason }),
533                "Expand the session filesystem roots if the access is intentional, or request a resource inside the approved root set.",
534            ),
535            Self::PromptNotRegistered(prompt) => self.report_with_context(
536                "CHIO-KERNEL-PROMPT-NOT-REGISTERED",
537                serde_json::json!({ "prompt": prompt }),
538                "Register the prompt provider for this prompt name or request a prompt that is actually exposed.",
539            ),
540            Self::SamplingNotAllowedByPolicy => self.report_with_context(
541                "CHIO-KERNEL-SAMPLING-NOT-ALLOWED",
542                serde_json::json!({}),
543                "Enable sampling in policy if this workflow requires it, or retry without a sampling request.",
544            ),
545            Self::SamplingNotNegotiated => self.report_with_context(
546                "CHIO-KERNEL-SAMPLING-NOT-NEGOTIATED",
547                serde_json::json!({}),
548                "Negotiate sampling support with the client before issuing sampling operations.",
549            ),
550            Self::SamplingContextNotSupported => self.report_with_context(
551                "CHIO-KERNEL-SAMPLING-CONTEXT-NOT-SUPPORTED",
552                serde_json::json!({}),
553                "Disable sampling context inclusion or upgrade the client to one that supports the negotiated feature.",
554            ),
555            Self::SamplingToolUseNotAllowedByPolicy => self.report_with_context(
556                "CHIO-KERNEL-SAMPLING-TOOL-USE-NOT-ALLOWED",
557                serde_json::json!({}),
558                "Enable sampling tool use in policy or retry without delegated tool execution inside the sampling branch.",
559            ),
560            Self::SamplingToolUseNotNegotiated => self.report_with_context(
561                "CHIO-KERNEL-SAMPLING-TOOL-USE-NOT-NEGOTIATED",
562                serde_json::json!({}),
563                "Negotiate sampling tool-use support with the client before attempting tool execution inside sampling.",
564            ),
565            Self::ElicitationNotAllowedByPolicy => self.report_with_context(
566                "CHIO-KERNEL-ELICITATION-NOT-ALLOWED",
567                serde_json::json!({}),
568                "Enable elicitation in policy or retry without requesting user input through the kernel.",
569            ),
570            Self::ElicitationNotNegotiated => self.report_with_context(
571                "CHIO-KERNEL-ELICITATION-NOT-NEGOTIATED",
572                serde_json::json!({}),
573                "Negotiate elicitation support with the client before attempting elicitation operations.",
574            ),
575            Self::ElicitationFormNotSupported => self.report_with_context(
576                "CHIO-KERNEL-ELICITATION-FORM-NOT-SUPPORTED",
577                serde_json::json!({}),
578                "Switch to a supported elicitation mode or upgrade the client to one that supports form-mode elicitation.",
579            ),
580            Self::ElicitationUrlNotSupported => self.report_with_context(
581                "CHIO-KERNEL-ELICITATION-URL-NOT-SUPPORTED",
582                serde_json::json!({}),
583                "Switch to a supported elicitation mode or negotiate URL-based elicitation support with the client.",
584            ),
585            Self::UrlElicitationsRequired {
586                message,
587                elicitations,
588            } => self.report_with_context(
589                "CHIO-KERNEL-URL-ELICITATIONS-REQUIRED",
590                serde_json::json!({
591                    "message": message,
592                    "elicitation_count": elicitations.len()
593                }),
594                "Complete the required URL-based elicitation flow and resubmit the request afterward.",
595            ),
596            Self::RootsNotNegotiated => self.report_with_context(
597                "CHIO-KERNEL-ROOTS-NOT-NEGOTIATED",
598                serde_json::json!({}),
599                "Negotiate roots/list support with the client before using root-scoped resource protections.",
600            ),
601            Self::InvalidChildRequestParent => self.report_with_context(
602                "CHIO-KERNEL-INVALID-CHILD-REQUEST-PARENT",
603                serde_json::json!({}),
604                "Create the child request from a ready session-bound parent request that is currently in flight.",
605            ),
606            Self::RequestCancelled { request_id, reason } => self.report_with_context(
607                "CHIO-KERNEL-REQUEST-CANCELLED",
608                serde_json::json!({ "request_id": request_id.to_string(), "reason": reason }),
609                "Stop using the cancelled request ID and restart the operation if the workflow still needs to continue.",
610            ),
611            Self::ReceiptSigningFailed(reason) => self.report_with_context(
612                "CHIO-KERNEL-RECEIPT-SIGNING-FAILED",
613                serde_json::json!({ "reason": reason }),
614                "Inspect the kernel signing key configuration and signing payload integrity, then retry receipt generation.",
615            ),
616            Self::ReceiptPersistence(error) => self.report_with_context(
617                "CHIO-KERNEL-RECEIPT-PERSISTENCE",
618                serde_json::json!({ "source": error.to_string() }),
619                "Check the configured receipt store connectivity, permissions, and schema health before retrying.",
620            ),
621            Self::RevocationStore(error) => self.report_with_context(
622                "CHIO-KERNEL-REVOCATION-STORE",
623                serde_json::json!({ "source": error.to_string() }),
624                "Check the configured revocation store connectivity, permissions, and schema health before retrying.",
625            ),
626            Self::BudgetStore(error) => self.report_with_context(
627                "CHIO-KERNEL-BUDGET-STORE",
628                serde_json::json!({ "source": error.to_string() }),
629                "Check the configured budget store connectivity, permissions, and schema health before retrying.",
630            ),
631            Self::NoCrossCurrencyOracle { base, quote } => self.report_with_context(
632                "CHIO-KERNEL-NO-CROSS-CURRENCY-ORACLE",
633                serde_json::json!({ "base": base, "quote": quote }),
634                "Configure a price oracle for this currency pair or avoid a cross-currency budget path for this request.",
635            ),
636            Self::CrossCurrencyOracle(reason) => self.report_with_context(
637                "CHIO-KERNEL-CROSS-CURRENCY-ORACLE",
638                serde_json::json!({ "reason": reason }),
639                "Inspect the price-oracle configuration and upstream quote availability for the requested currency conversion.",
640            ),
641            Self::Web3EvidenceUnavailable(reason) => self.report_with_context(
642                "CHIO-KERNEL-WEB3-EVIDENCE-UNAVAILABLE",
643                serde_json::json!({ "reason": reason }),
644                "Enable the required receipt-store, checkpoint, and oracle prerequisites before running the web3 evidence path.",
645            ),
646            Self::Internal(reason) => self.report_with_context(
647                "CHIO-KERNEL-INTERNAL",
648                serde_json::json!({ "reason": reason }),
649                "Capture the error report and kernel logs, then treat this as a reproducible kernel bug if it persists.",
650            ),
651            Self::DpopVerificationFailed(reason) => self.report_with_context(
652                "CHIO-KERNEL-DPOP-VERIFICATION-FAILED",
653                serde_json::json!({ "reason": reason }),
654                "Attach a valid DPoP proof bound to the current capability, request, server, and tool before retrying.",
655            ),
656            Self::ApprovalRejected(reason) => self.report_with_context(
657                "CHIO-KERNEL-APPROVAL-REJECTED",
658                serde_json::json!({ "reason": reason }),
659                "Obtain a fresh approval token bound to this exact request and retry once a human approver has signed it.",
660            ),
661        }
662    }
663}
664
665/// A policy guard that the kernel evaluates before forwarding a tool call.
666///
667/// Guards are the same concept as ClawdStrike's `Guard` trait, adapted for
668/// the Chio tool-call context. Each guard inspects the request and returns
669/// a verdict.
670pub trait Guard: Send + Sync {
671    /// Human-readable guard name (e.g., "forbidden-path").
672    fn name(&self) -> &str;
673
674    /// Evaluate the guard against a tool call request.
675    ///
676    /// Returns `Ok(Verdict::Allow)` to pass, `Ok(Verdict::Deny)` to block,
677    /// or `Err` on internal failure (which the kernel treats as deny).
678    fn evaluate(&self, ctx: &GuardContext) -> Result<Verdict, KernelError>;
679}
680
681/// Context passed to guards during evaluation.
682pub struct GuardContext<'a> {
683    /// The tool call request being evaluated.
684    pub request: &'a ToolCallRequest,
685    /// The verified capability scope.
686    pub scope: &'a ChioScope,
687    /// The agent making the request.
688    pub agent_id: &'a AgentId,
689    /// The target server.
690    pub server_id: &'a ServerId,
691    /// Session-scoped enforceable filesystem roots, when the request is being
692    /// evaluated through the supported session-backed runtime path.
693    pub session_filesystem_roots: Option<&'a [String]>,
694    /// Index of the matched grant in the capability's scope, populated by
695    /// check_and_increment_budget before guards run.
696    pub matched_grant_index: Option<usize>,
697}
698
699/// Trait representing a resource provider.
700pub trait ResourceProvider: Send + Sync {
701    /// List the resources this provider exposes.
702    fn list_resources(&self) -> Vec<ResourceDefinition>;
703
704    /// List parameterized resource templates.
705    fn list_resource_templates(&self) -> Vec<ResourceTemplateDefinition> {
706        vec![]
707    }
708
709    /// Read a resource by URI. Returns `Ok(None)` when the provider does not own the URI.
710    fn read_resource(&self, uri: &str) -> Result<Option<Vec<ResourceContent>>, KernelError>;
711
712    /// Return completions for a resource template or URI reference.
713    fn complete_resource_argument(
714        &self,
715        _uri: &str,
716        _argument_name: &str,
717        _value: &str,
718        _context: &serde_json::Value,
719    ) -> Result<Option<CompletionResult>, KernelError> {
720        Ok(None)
721    }
722}
723
724/// Trait representing a prompt provider.
725pub trait PromptProvider: Send + Sync {
726    /// List available prompts.
727    fn list_prompts(&self) -> Vec<PromptDefinition>;
728
729    /// Retrieve a prompt by name. Returns `Ok(None)` when the provider does not own the prompt.
730    fn get_prompt(
731        &self,
732        name: &str,
733        arguments: serde_json::Value,
734    ) -> Result<Option<PromptResult>, KernelError>;
735
736    /// Return completions for a prompt argument.
737    fn complete_prompt_argument(
738        &self,
739        _name: &str,
740        _argument_name: &str,
741        _value: &str,
742        _context: &serde_json::Value,
743    ) -> Result<Option<CompletionResult>, KernelError> {
744        Ok(None)
745    }
746}
747
748/// In-memory append-only log of signed receipts.
749///
750/// This remains useful for process-local inspection even when a durable
751/// backend is configured.
752#[derive(Clone, Default)]
753pub struct ReceiptLog {
754    receipts: Vec<ChioReceipt>,
755}
756
757impl ReceiptLog {
758    pub fn new() -> Self {
759        Self::default()
760    }
761
762    pub fn append(&mut self, receipt: ChioReceipt) {
763        self.receipts.push(receipt);
764    }
765
766    pub fn len(&self) -> usize {
767        self.receipts.len()
768    }
769
770    pub fn is_empty(&self) -> bool {
771        self.receipts.is_empty()
772    }
773
774    pub fn receipts(&self) -> &[ChioReceipt] {
775        &self.receipts
776    }
777
778    pub fn get(&self, index: usize) -> Option<&ChioReceipt> {
779        self.receipts.get(index)
780    }
781}
782
783/// In-memory append-only log of signed child-request receipts.
784#[derive(Clone, Default)]
785pub struct ChildReceiptLog {
786    receipts: Vec<ChildRequestReceipt>,
787}
788
789impl ChildReceiptLog {
790    pub fn new() -> Self {
791        Self::default()
792    }
793
794    pub fn append(&mut self, receipt: ChildRequestReceipt) {
795        self.receipts.push(receipt);
796    }
797
798    pub fn len(&self) -> usize {
799        self.receipts.len()
800    }
801
802    pub fn is_empty(&self) -> bool {
803        self.receipts.is_empty()
804    }
805
806    pub fn receipts(&self) -> &[ChildRequestReceipt] {
807        &self.receipts
808    }
809
810    pub fn get(&self, index: usize) -> Option<&ChildRequestReceipt> {
811        self.receipts.get(index)
812    }
813}
814
815/// Configuration for the Chio Runtime Kernel.
816pub struct KernelConfig {
817    /// Ed25519 keypair for signing receipts and issuing capabilities.
818    pub keypair: Keypair,
819
820    /// Public keys of trusted Capability Authorities.
821    pub ca_public_keys: Vec<chio_core::PublicKey>,
822
823    /// Maximum allowed delegation depth.
824    pub max_delegation_depth: u32,
825
826    /// SHA-256 hash of the active policy (embedded in receipts).
827    pub policy_hash: String,
828
829    /// Whether nested sampling requests are allowed at all.
830    pub allow_sampling: bool,
831
832    /// Whether sampling requests may include tool-use affordances.
833    pub allow_sampling_tool_use: bool,
834
835    /// Whether nested elicitation requests are allowed.
836    pub allow_elicitation: bool,
837
838    /// Maximum total wall-clock duration permitted for one streamed tool result.
839    pub max_stream_duration_secs: u64,
840
841    /// Maximum total canonical payload size permitted for one streamed tool result.
842    pub max_stream_total_bytes: u64,
843
844    /// Whether durable receipts and kernel-signed checkpoints are mandatory
845    /// prerequisites for this deployment.
846    pub require_web3_evidence: bool,
847
848    /// Number of receipts between Merkle checkpoint snapshots. Default: 100.
849    ///
850    /// Set to 0 to disable automatic checkpointing for deployments that do not
851    /// require web3 evidence.
852    pub checkpoint_batch_size: u64,
853
854    /// Optional receipt retention configuration.
855    ///
856    /// When `None` (default), retention is disabled and receipts accumulate
857    /// indefinitely. When `Some(config)`, the kernel will archive receipts
858    /// that exceed the time or size threshold.
859    pub retention_config: Option<crate::receipt_store::RetentionConfig>,
860}
861
862pub const DEFAULT_MAX_STREAM_DURATION_SECS: u64 = 300;
863pub const DEFAULT_MAX_STREAM_TOTAL_BYTES: u64 = 256 * 1024 * 1024;
864pub const DEFAULT_CHECKPOINT_BATCH_SIZE: u64 = 100;
865pub const DEFAULT_RETENTION_DAYS: u64 = 90;
866pub const DEFAULT_MAX_SIZE_BYTES: u64 = 10_737_418_240;
867
868/// The Chio Runtime Kernel.
869///
870/// This is the central component of the Chio protocol. It validates capabilities,
871/// runs guards, dispatches tool calls, and signs receipts.
872///
873/// The kernel is designed to be the sole trusted mediator. It never exposes its
874/// signing key, address, or internal state to the agent.
875pub struct ChioKernel {
876    config: KernelConfig,
877    guards: Vec<Box<dyn Guard>>,
878    post_invocation_pipeline: crate::post_invocation::PostInvocationPipeline,
879    budget_store: Mutex<Box<dyn BudgetStore>>,
880    revocation_store: Mutex<Box<dyn RevocationStore>>,
881    capability_authority: Box<dyn CapabilityAuthority>,
882    tool_servers: HashMap<ServerId, Box<dyn ToolServerConnection>>,
883    resource_providers: Vec<Box<dyn ResourceProvider>>,
884    prompt_providers: Vec<Box<dyn PromptProvider>>,
885    sessions: RwLock<HashMap<SessionId, Session>>,
886    receipt_log: Mutex<ReceiptLog>,
887    child_receipt_log: Mutex<ChildReceiptLog>,
888    receipt_store: Option<Mutex<Box<dyn ReceiptStore>>>,
889    payment_adapter: Option<Box<dyn PaymentAdapter>>,
890    price_oracle: Option<Box<dyn PriceOracle>>,
891    attestation_trust_policy: Option<AttestationTrustPolicy>,
892    /// How many receipts per Merkle checkpoint batch. Default: 100.
893    checkpoint_batch_size: u64,
894    /// Monotonic counter for checkpoint_seq values.
895    checkpoint_seq_counter: AtomicU64,
896    /// seq of the last receipt included in the previous checkpoint batch.
897    last_checkpoint_seq: AtomicU64,
898    /// Nonce replay store for DPoP proof verification. Required when any grant has dpop_required.
899    dpop_nonce_store: Option<dpop::DpopNonceStore>,
900    /// Configuration for DPoP proof verification TTLs and clock skew.
901    dpop_config: Option<dpop::DpopConfig>,
902    /// Phase 1.1 execution-nonce config (TTL, capacity, strict-mode flag).
903    /// When `None`, no nonce is minted on allow and strict verification is
904    /// disabled (legacy deployments keep working).
905    execution_nonce_config: Option<crate::execution_nonce::ExecutionNonceConfig>,
906    /// Phase 1.1 replay-prevention store for execution nonces. Shared with
907    /// any tool server that delegates verification to the kernel. Boxed
908    /// trait object so SQLite-backed stores can be plugged in.
909    execution_nonce_store: Option<Box<dyn crate::execution_nonce::ExecutionNonceStore>>,
910    /// Replay store for governed approval tokens. Prevents a signed approval
911    /// from being consumed more than once. Uses the same LRU + TTL pattern as
912    /// DPoP nonce verification. Key: (request_id, governed_intent_hash).
913    approval_replay_store: Option<dpop::DpopNonceStore>,
914    /// Emergency kill switch. When `true`, every evaluate entry point returns
915    /// `Verdict::Deny` without performing capability validation or guard
916    /// evaluation. Flipped by `emergency_stop` / `emergency_resume`.
917    ///
918    /// Reads use `Ordering::SeqCst` even on the hot path. The emergency check
919    /// is a single atomic load per evaluate call (negligible cost relative to
920    /// the guard pipeline) and `SeqCst` is the safest default for a rarely
921    /// taken control path.
922    emergency_stopped: AtomicBool,
923    /// Unix timestamp (seconds) at which the kill switch was last engaged.
924    /// `0` means "never engaged" or "currently resumed". Written with
925    /// `SeqCst` before `emergency_stopped` is set to `true`, cleared to `0`
926    /// after `emergency_stopped` is set to `false`.
927    emergency_stopped_since: AtomicU64,
928    /// Operator-supplied reason for the most recent emergency stop. Set on
929    /// `emergency_stop`, cleared on `emergency_resume`. Guarded by a mutex
930    /// because the payload is a heap-allocated `String`; callers that only
931    /// need presence information can read `emergency_stopped` instead.
932    emergency_stop_reason: Mutex<Option<String>>,
933    /// Phase 18.2 memory-provenance chain. When installed, every
934    /// governed `MemoryWrite` action appends an entry after the allow
935    /// receipt is signed, and every `MemoryRead` attaches the latest
936    /// entry (or an `Unverified` marker) to its receipt as
937    /// `memory_provenance` evidence metadata. `None` keeps the kernel
938    /// backward-compatible: memory-shaped tool calls behave exactly as
939    /// they did before Phase 18.2.
940    memory_provenance: Option<Arc<dyn crate::memory_provenance::MemoryProvenanceStore>>,
941    /// Phase 20.3 cross-kernel federation peer set. When a request
942    /// carries a `federated_origin_kernel_id` and that peer is pinned
943    /// here (fresh), the kernel invokes `federation_cosigner` after
944    /// locally signing the receipt to obtain the origin kernel's
945    /// co-signature. Absent in non-federated deployments.
946    federation_peers: RwLock<HashMap<String, chio_federation::FederationPeer>>,
947    /// Phase 20.3 bilateral co-signer. Separate from the peer set so
948    /// runtime can install it independently -- for instance, a deployment
949    /// can declare peers while still using a mock cosigner in tests.
950    federation_cosigner: Option<Arc<dyn chio_federation::BilateralCoSigningProtocol>>,
951    /// Phase 20.3 locally-signed dual receipts, indexed by ChioReceipt.id.
952    /// Populated only when the post-sign hook fires successfully. Kept
953    /// in-memory; persistent storage plugs in via the federation-state
954    /// APIs already in chio-federation.
955    federation_dual_receipts: Mutex<HashMap<String, chio_federation::DualSignedReceipt>>,
956    /// Phase 20.3 operator-declared kernel identifier used as the
957    /// `org_b_kernel_id` in bilateral co-signing. Defaults to the hex
958    /// encoding of the kernel's signing public key, but operators can
959    /// override it to a stable DNS name via `with_federation_peers`.
960    federation_local_kernel_id: Mutex<Option<String>>,
961}
962
963#[derive(Clone, Copy)]
964pub(crate) struct MatchingGrant<'a> {
965    pub(crate) index: usize,
966    pub(crate) grant: &'a ToolGrant,
967    pub(crate) specificity: (u8, u8, usize),
968}
969
970/// Result of a monetary budget charge attempt.
971///
972/// Carries the accounting info needed to populate FinancialReceiptMetadata.
973pub(crate) struct BudgetChargeResult {
974    grant_index: usize,
975    cost_charged: u64,
976    currency: String,
977    budget_total: u64,
978    /// Running committed cost after this charge (used to compute budget_remaining).
979    new_committed_cost_units: u64,
980    budget_hold_id: String,
981    authorize_metadata: BudgetCommitMetadata,
982}
983
984impl BudgetChargeResult {
985    fn reverse_event_id(&self) -> String {
986        format!("{}:reverse", self.budget_hold_id)
987    }
988
989    fn reconcile_event_id(&self) -> String {
990        format!("{}:reconcile", self.budget_hold_id)
991    }
992}
993
994struct SessionNestedFlowBridge<'a, C> {
995    sessions: &'a mut HashMap<SessionId, Session>,
996    child_receipts: &'a mut Vec<ChildRequestReceipt>,
997    parent_context: &'a OperationContext,
998    allow_sampling: bool,
999    allow_sampling_tool_use: bool,
1000    allow_elicitation: bool,
1001    policy_hash: &'a str,
1002    kernel_keypair: &'a Keypair,
1003    client: &'a mut C,
1004}
1005
1006impl<C> SessionNestedFlowBridge<'_, C> {
1007    fn complete_child_request_with_receipt<T: serde::Serialize>(
1008        &mut self,
1009        child_context: &OperationContext,
1010        operation_kind: OperationKind,
1011        result: &Result<T, KernelError>,
1012    ) -> Result<(), KernelError> {
1013        let terminal_state = child_terminal_state(&child_context.request_id, result);
1014        complete_session_request_with_terminal_state_in_sessions(
1015            self.sessions,
1016            &child_context.session_id,
1017            &child_context.request_id,
1018            terminal_state.clone(),
1019        )?;
1020
1021        let receipt = build_child_request_receipt(
1022            self.policy_hash,
1023            self.kernel_keypair,
1024            child_context,
1025            operation_kind,
1026            terminal_state,
1027            child_outcome_payload(result)?,
1028        )?;
1029        self.child_receipts.push(receipt);
1030        Ok(())
1031    }
1032}
1033
1034impl<C: NestedFlowClient> NestedFlowBridge for SessionNestedFlowBridge<'_, C> {
1035    fn parent_request_id(&self) -> &RequestId {
1036        &self.parent_context.request_id
1037    }
1038
1039    fn poll_parent_cancellation(&mut self) -> Result<(), KernelError> {
1040        self.client.poll_parent_cancellation(self.parent_context)
1041    }
1042
1043    fn list_roots(&mut self) -> Result<Vec<RootDefinition>, KernelError> {
1044        let child_context = begin_child_request_in_sessions(
1045            self.sessions,
1046            self.parent_context,
1047            nested_child_request_id(&self.parent_context.request_id, "roots"),
1048            OperationKind::ListRoots,
1049            None,
1050            false,
1051        )?;
1052
1053        let result = (|| {
1054            let session = session_from_map(self.sessions, &child_context.session_id)?;
1055            session.validate_context(&child_context)?;
1056            session.ensure_operation_allowed(OperationKind::ListRoots)?;
1057            if !session.peer_capabilities().supports_roots {
1058                return Err(KernelError::RootsNotNegotiated);
1059            }
1060
1061            let roots = self
1062                .client
1063                .list_roots(self.parent_context, &child_context)?;
1064            session_mut_from_map(self.sessions, &child_context.session_id)?
1065                .replace_roots(roots.clone());
1066            Ok(roots)
1067        })();
1068        if matches!(
1069            &result,
1070            Err(KernelError::RequestCancelled { request_id, .. })
1071                if request_id == &child_context.request_id
1072        ) {
1073            session_mut_from_map(self.sessions, &child_context.session_id)?
1074                .request_cancellation(&child_context.request_id)?;
1075        }
1076        self.complete_child_request_with_receipt(
1077            &child_context,
1078            OperationKind::ListRoots,
1079            &result,
1080        )?;
1081
1082        result
1083    }
1084
1085    fn create_message(
1086        &mut self,
1087        operation: CreateMessageOperation,
1088    ) -> Result<CreateMessageResult, KernelError> {
1089        let child_context = begin_child_request_in_sessions(
1090            self.sessions,
1091            self.parent_context,
1092            nested_child_request_id(&self.parent_context.request_id, "sample"),
1093            OperationKind::CreateMessage,
1094            None,
1095            true,
1096        )?;
1097
1098        let result = (|| {
1099            validate_sampling_request_in_sessions(
1100                self.sessions,
1101                self.allow_sampling,
1102                self.allow_sampling_tool_use,
1103                &child_context,
1104                &operation,
1105            )?;
1106            self.client
1107                .create_message(self.parent_context, &child_context, &operation)
1108        })();
1109        if matches!(
1110            &result,
1111            Err(KernelError::RequestCancelled { request_id, .. })
1112                if request_id == &child_context.request_id
1113        ) {
1114            session_mut_from_map(self.sessions, &child_context.session_id)?
1115                .request_cancellation(&child_context.request_id)?;
1116        }
1117        self.complete_child_request_with_receipt(
1118            &child_context,
1119            OperationKind::CreateMessage,
1120            &result,
1121        )?;
1122
1123        result
1124    }
1125
1126    fn create_elicitation(
1127        &mut self,
1128        operation: CreateElicitationOperation,
1129    ) -> Result<CreateElicitationResult, KernelError> {
1130        let child_context = begin_child_request_in_sessions(
1131            self.sessions,
1132            self.parent_context,
1133            nested_child_request_id(&self.parent_context.request_id, "elicit"),
1134            OperationKind::CreateElicitation,
1135            None,
1136            true,
1137        )?;
1138
1139        let result = (|| {
1140            validate_elicitation_request_in_sessions(
1141                self.sessions,
1142                self.allow_elicitation,
1143                &child_context,
1144                &operation,
1145            )?;
1146            self.client
1147                .create_elicitation(self.parent_context, &child_context, &operation)
1148        })();
1149        if matches!(
1150            &result,
1151            Err(KernelError::RequestCancelled { request_id, .. })
1152                if request_id == &child_context.request_id
1153        ) {
1154            session_mut_from_map(self.sessions, &child_context.session_id)?
1155                .request_cancellation(&child_context.request_id)?;
1156        }
1157        self.complete_child_request_with_receipt(
1158            &child_context,
1159            OperationKind::CreateElicitation,
1160            &result,
1161        )?;
1162
1163        result
1164    }
1165
1166    fn notify_elicitation_completed(&mut self, elicitation_id: &str) -> Result<(), KernelError> {
1167        let session = session_from_map(self.sessions, &self.parent_context.session_id)?;
1168        session.validate_context(self.parent_context)?;
1169        session.ensure_operation_allowed(OperationKind::ToolCall)?;
1170
1171        self.client
1172            .notify_elicitation_completed(self.parent_context, elicitation_id)
1173    }
1174
1175    fn notify_resource_updated(&mut self, uri: &str) -> Result<(), KernelError> {
1176        let session = session_from_map(self.sessions, &self.parent_context.session_id)?;
1177        session.validate_context(self.parent_context)?;
1178        session.ensure_operation_allowed(OperationKind::ToolCall)?;
1179
1180        if !session.is_resource_subscribed(uri) {
1181            return Ok(());
1182        }
1183
1184        self.client
1185            .notify_resource_updated(self.parent_context, uri)
1186    }
1187
1188    fn notify_resources_list_changed(&mut self) -> Result<(), KernelError> {
1189        let session = session_from_map(self.sessions, &self.parent_context.session_id)?;
1190        session.validate_context(self.parent_context)?;
1191        session.ensure_operation_allowed(OperationKind::ToolCall)?;
1192
1193        self.client
1194            .notify_resources_list_changed(self.parent_context)
1195    }
1196}
1197
1198impl ChioKernel {
1199    fn with_sessions_read<R>(
1200        &self,
1201        f: impl FnOnce(&HashMap<SessionId, Session>) -> Result<R, KernelError>,
1202    ) -> Result<R, KernelError> {
1203        let sessions = self
1204            .sessions
1205            .read()
1206            .map_err(|_| KernelError::Internal("session state lock poisoned".to_string()))?;
1207        f(&sessions)
1208    }
1209
1210    fn with_sessions_write<R>(
1211        &self,
1212        f: impl FnOnce(&mut HashMap<SessionId, Session>) -> Result<R, KernelError>,
1213    ) -> Result<R, KernelError> {
1214        let mut sessions = self
1215            .sessions
1216            .write()
1217            .map_err(|_| KernelError::Internal("session state lock poisoned".to_string()))?;
1218        f(&mut sessions)
1219    }
1220
1221    fn with_session<R>(
1222        &self,
1223        session_id: &SessionId,
1224        f: impl FnOnce(&Session) -> Result<R, KernelError>,
1225    ) -> Result<R, KernelError> {
1226        self.with_sessions_read(|sessions| {
1227            let session = sessions
1228                .get(session_id)
1229                .ok_or_else(|| KernelError::UnknownSession(session_id.clone()))?;
1230            f(session)
1231        })
1232    }
1233
1234    /// Phase 1.5: resolve the tenant_id for a given session by walking its
1235    /// authenticated auth context into the enterprise identity's tenant
1236    /// claim. Returns `None` for single-tenant / anonymous sessions, for
1237    /// unknown session IDs, or when the caller did not supply one.
1238    ///
1239    /// Tenant_id is taken from the OAuth bearer's `enterprise_identity`
1240    /// (preferred, richer SSO claims) and falls back to the OAuth
1241    /// `federated_claims.tenant_id` when the IdP surfaces only the minimal
1242    /// federated claim set. Both sources originate from the authenticated
1243    /// session -- never from caller-provided request fields, because
1244    /// caller choice would defeat the isolation guarantee.
1245    pub(crate) fn resolve_tenant_id_for_session(
1246        &self,
1247        session_id: Option<&SessionId>,
1248    ) -> Option<String> {
1249        let id = session_id?;
1250        self.with_session(id, |session| {
1251            Ok(extract_tenant_id_from_auth_context(session.auth_context()))
1252        })
1253        .ok()
1254        .flatten()
1255    }
1256
1257    fn with_session_mut<R>(
1258        &self,
1259        session_id: &SessionId,
1260        f: impl FnOnce(&mut Session) -> Result<R, KernelError>,
1261    ) -> Result<R, KernelError> {
1262        self.with_sessions_write(|sessions| {
1263            let session = sessions
1264                .get_mut(session_id)
1265                .ok_or_else(|| KernelError::UnknownSession(session_id.clone()))?;
1266            f(session)
1267        })
1268    }
1269
1270    fn with_budget_store<R>(
1271        &self,
1272        f: impl FnOnce(&mut dyn BudgetStore) -> Result<R, KernelError>,
1273    ) -> Result<R, KernelError> {
1274        let mut store = self
1275            .budget_store
1276            .lock()
1277            .map_err(|_| KernelError::Internal("budget store lock poisoned".to_string()))?;
1278        f(store.as_mut())
1279    }
1280
1281    fn with_revocation_store<R>(
1282        &self,
1283        f: impl FnOnce(&mut dyn RevocationStore) -> Result<R, KernelError>,
1284    ) -> Result<R, KernelError> {
1285        let mut store = self
1286            .revocation_store
1287            .lock()
1288            .map_err(|_| KernelError::Internal("revocation store lock poisoned".to_string()))?;
1289        f(store.as_mut())
1290    }
1291
1292    fn with_receipt_store<R>(
1293        &self,
1294        f: impl FnOnce(&mut dyn ReceiptStore) -> Result<R, KernelError>,
1295    ) -> Result<Option<R>, KernelError> {
1296        let Some(store) = self.receipt_store.as_ref() else {
1297            return Ok(None);
1298        };
1299        let mut store = store
1300            .lock()
1301            .map_err(|_| KernelError::Internal("receipt store lock poisoned".to_string()))?;
1302        f(store.as_mut()).map(Some)
1303    }
1304
1305    pub fn new(config: KernelConfig) -> Self {
1306        info!("initializing Chio kernel");
1307        let authority_keypair = config.keypair.clone();
1308        let checkpoint_batch_size = config.checkpoint_batch_size;
1309        Self {
1310            config,
1311            guards: Vec::new(),
1312            post_invocation_pipeline: crate::post_invocation::PostInvocationPipeline::new(),
1313            budget_store: Mutex::new(Box::new(InMemoryBudgetStore::new())),
1314            revocation_store: Mutex::new(Box::new(InMemoryRevocationStore::new())),
1315            capability_authority: Box::new(LocalCapabilityAuthority::new(authority_keypair)),
1316            tool_servers: HashMap::new(),
1317            resource_providers: Vec::new(),
1318            prompt_providers: Vec::new(),
1319            sessions: RwLock::new(HashMap::new()),
1320            receipt_log: Mutex::new(ReceiptLog::new()),
1321            child_receipt_log: Mutex::new(ChildReceiptLog::new()),
1322            receipt_store: None,
1323            payment_adapter: None,
1324            price_oracle: None,
1325            attestation_trust_policy: None,
1326            checkpoint_batch_size,
1327            checkpoint_seq_counter: AtomicU64::new(0),
1328            last_checkpoint_seq: AtomicU64::new(0),
1329            dpop_nonce_store: None,
1330            dpop_config: None,
1331            execution_nonce_config: None,
1332            execution_nonce_store: None,
1333            approval_replay_store: Some(dpop::DpopNonceStore::new(
1334                8192,
1335                std::time::Duration::from_secs(3600),
1336            )),
1337            emergency_stopped: AtomicBool::new(false),
1338            emergency_stopped_since: AtomicU64::new(0),
1339            emergency_stop_reason: Mutex::new(None),
1340            memory_provenance: None,
1341            federation_peers: RwLock::new(HashMap::new()),
1342            federation_cosigner: None,
1343            federation_dual_receipts: Mutex::new(HashMap::new()),
1344            federation_local_kernel_id: Mutex::new(None),
1345        }
1346    }
1347
1348    pub fn set_receipt_store(&mut self, receipt_store: Box<dyn ReceiptStore>) {
1349        self.receipt_store = Some(Mutex::new(receipt_store));
1350    }
1351
1352    pub fn set_payment_adapter(&mut self, payment_adapter: Box<dyn PaymentAdapter>) {
1353        self.payment_adapter = Some(payment_adapter);
1354    }
1355
1356    pub fn set_price_oracle(&mut self, price_oracle: Box<dyn PriceOracle>) {
1357        self.price_oracle = Some(price_oracle);
1358    }
1359
1360    pub fn set_attestation_trust_policy(
1361        &mut self,
1362        attestation_trust_policy: AttestationTrustPolicy,
1363    ) {
1364        self.attestation_trust_policy = Some(attestation_trust_policy);
1365    }
1366
1367    pub fn set_revocation_store(&mut self, revocation_store: Box<dyn RevocationStore>) {
1368        self.revocation_store = Mutex::new(revocation_store);
1369    }
1370
1371    pub fn set_capability_authority(&mut self, capability_authority: Box<dyn CapabilityAuthority>) {
1372        self.capability_authority = capability_authority;
1373    }
1374
1375    pub fn set_budget_store(&mut self, budget_store: Box<dyn BudgetStore>) {
1376        self.budget_store = Mutex::new(budget_store);
1377    }
1378
1379    pub fn set_post_invocation_pipeline(
1380        &mut self,
1381        pipeline: crate::post_invocation::PostInvocationPipeline,
1382    ) {
1383        self.post_invocation_pipeline = pipeline;
1384    }
1385
1386    pub fn add_post_invocation_hook(
1387        &mut self,
1388        hook: Box<dyn crate::post_invocation::PostInvocationHook>,
1389    ) {
1390        self.post_invocation_pipeline.add(hook);
1391    }
1392
1393    /// Phase 18.2: install a memory-provenance chain.
1394    ///
1395    /// Once installed, every governed `MemoryWrite`-shaped tool call
1396    /// appends an entry to the chain after the allow receipt is
1397    /// signed. A chain-store failure on that path is fatal: the call
1398    /// surfaces `KernelError::Internal(...)` so operators can detect
1399    /// and repair the drift rather than silently shipping a write
1400    /// without provenance evidence.
1401    ///
1402    /// Every `MemoryRead`-shaped tool call looks the entry up by
1403    /// `(store, key)` and attaches the result to the receipt as
1404    /// `memory_provenance` evidence metadata. Reads with no chain
1405    /// entry or with a tampered chain surface as
1406    /// [`crate::memory_provenance::ProvenanceVerification::Unverified`]
1407    /// so the receipt unambiguously records the gap.
1408    pub fn set_memory_provenance_store(
1409        &mut self,
1410        store: Arc<dyn crate::memory_provenance::MemoryProvenanceStore>,
1411    ) {
1412        self.memory_provenance = Some(store);
1413    }
1414
1415    /// Return a clone of the active memory-provenance store handle,
1416    /// or `None` when no provenance chain has been installed.
1417    ///
1418    /// Useful for integration tests that want to assert on the chain
1419    /// state directly after driving `evaluate_tool_call`.
1420    #[must_use]
1421    pub fn memory_provenance_store(
1422        &self,
1423    ) -> Option<Arc<dyn crate::memory_provenance::MemoryProvenanceStore>> {
1424        self.memory_provenance.as_ref().map(Arc::clone)
1425    }
1426
1427    /// Phase 20.3: install a set of [`chio_federation::FederationPeer`]s
1428    /// this kernel trusts for bilateral co-signing. Overwrites any
1429    /// previously declared set. Callers typically obtain these peers
1430    /// from [`chio_federation::KernelTrustExchange::accept_envelope`]
1431    /// after a successful mTLS handshake.
1432    ///
1433    /// Builder-style so deployments can chain `.with_federation_peers(...)`
1434    /// onto `ChioKernel::new(config)`.
1435    #[must_use]
1436    pub fn with_federation_peers(self, peers: Vec<chio_federation::FederationPeer>) -> Self {
1437        if let Ok(mut map) = self.federation_peers.write() {
1438            map.clear();
1439            for peer in peers {
1440                map.insert(peer.kernel_id.clone(), peer);
1441            }
1442        }
1443        self
1444    }
1445
1446    /// Phase 20.3: install the bilateral cosigner responsible for
1447    /// contacting a peer kernel to obtain a co-signature. Production
1448    /// deployments plug in an mTLS-backed RPC client; tests can use
1449    /// [`chio_federation::InProcessCoSigner`].
1450    pub fn set_federation_cosigner(
1451        &mut self,
1452        cosigner: Arc<dyn chio_federation::BilateralCoSigningProtocol>,
1453    ) {
1454        self.federation_cosigner = Some(cosigner);
1455    }
1456
1457    /// Phase 20.3: advertise this kernel's stable identifier as seen by
1458    /// remote federation peers. When unset, the hex encoding of the
1459    /// signing public key is used. Setting this is recommended in
1460    /// production so receipts reference DNS names rather than raw keys.
1461    pub fn set_federation_local_kernel_id(&self, kernel_id: impl Into<String>) {
1462        if let Ok(mut slot) = self.federation_local_kernel_id.lock() {
1463            *slot = Some(kernel_id.into());
1464        }
1465    }
1466
1467    /// Phase 20.3: resolve the active federation peer for
1468    /// `remote_kernel_id`, refusing stale pins fail-closed.
1469    pub fn federation_peer(
1470        &self,
1471        remote_kernel_id: &str,
1472        now: u64,
1473    ) -> Option<chio_federation::FederationPeer> {
1474        let map = self.federation_peers.read().ok()?;
1475        let peer = map.get(remote_kernel_id)?.clone();
1476        if peer.is_fresh(now) {
1477            Some(peer)
1478        } else {
1479            None
1480        }
1481    }
1482
1483    /// Phase 20.3: snapshot the currently-pinned federation peer set.
1484    pub fn federation_peers_snapshot(&self) -> Vec<chio_federation::FederationPeer> {
1485        self.federation_peers
1486            .read()
1487            .map(|map| map.values().cloned().collect())
1488            .unwrap_or_default()
1489    }
1490
1491    /// Phase 20.3: look up a dual-signed receipt by the underlying
1492    /// [`chio_core::receipt::ChioReceipt`] id. Returns `None` when the
1493    /// receipt did not cross a federation boundary or when the
1494    /// co-signing hook has not yet produced a dual-signed artifact
1495    /// for it.
1496    pub fn dual_signed_receipt(
1497        &self,
1498        receipt_id: &str,
1499    ) -> Option<chio_federation::DualSignedReceipt> {
1500        self.federation_dual_receipts
1501            .lock()
1502            .ok()?
1503            .get(receipt_id)
1504            .cloned()
1505    }
1506
1507    /// Local kernel identifier used in bilateral co-signing. Falls back
1508    /// to the hex encoding of the signing public key.
1509    pub fn federation_local_kernel_id(&self) -> String {
1510        if let Ok(slot) = self.federation_local_kernel_id.lock() {
1511            if let Some(id) = slot.as_ref() {
1512                return id.clone();
1513            }
1514        }
1515        self.config.keypair.public_key().to_hex()
1516    }
1517
1518    /// Phase 20.3 post-sign hook. Invoked immediately after
1519    /// [`Self::build_and_sign_receipt`] so the local (tool-host)
1520    /// signature has already landed in the `ChioReceipt`. When
1521    /// `federated_origin_kernel_id` is set and the peer is pinned fresh,
1522    /// this dispatches the receipt to the cosigner, assembles a
1523    /// [`chio_federation::DualSignedReceipt`], and stashes it for
1524    /// retrieval via [`Self::dual_signed_receipt`].
1525    ///
1526    /// Fail-closed: any error from the peer lookup or the cosigner is
1527    /// surfaced as a [`KernelError::Internal`] so operators see the
1528    /// federation drift rather than silently shipping a receipt without
1529    /// the remote signature. Non-federated requests (`None` origin) are
1530    /// a no-op.
1531    pub(crate) fn apply_federation_cosign(
1532        &self,
1533        request: &crate::runtime::ToolCallRequest,
1534        receipt: &chio_core::receipt::ChioReceipt,
1535    ) -> Result<(), KernelError> {
1536        let Some(origin_kernel_id) = request.federated_origin_kernel_id.as_ref() else {
1537            return Ok(());
1538        };
1539        let Some(cosigner) = self.federation_cosigner.as_ref() else {
1540            return Err(KernelError::Internal(format!(
1541                "federation cosigner missing for request {request_id} bound to origin kernel {origin_kernel_id}",
1542                request_id = request.request_id,
1543            )));
1544        };
1545        let now = current_unix_timestamp();
1546        let Some(peer) = self.federation_peer(origin_kernel_id, now) else {
1547            return Err(KernelError::Internal(format!(
1548                "federation peer {origin_kernel_id} is not pinned or has gone stale"
1549            )));
1550        };
1551
1552        let local_kernel_id = self.federation_local_kernel_id();
1553        let dual = chio_federation::co_sign_with_origin(
1554            origin_kernel_id,
1555            &peer.public_key,
1556            &local_kernel_id,
1557            &self.config.keypair,
1558            receipt.clone(),
1559            cosigner.as_ref(),
1560        )
1561        .map_err(|e| KernelError::Internal(format!("bilateral co-sign failed: {e}")))?;
1562
1563        let mut map = self
1564            .federation_dual_receipts
1565            .lock()
1566            .map_err(|_| KernelError::Internal("federation dual receipts lock poisoned".into()))?;
1567        map.insert(receipt.id.clone(), dual);
1568        Ok(())
1569    }
1570
1571    /// Engage the emergency kill switch.
1572    ///
1573    /// After this call, every `evaluate_tool_call*` path returns a signed
1574    /// deny receipt with reason `"kernel emergency stop active"` before
1575    /// touching capability validation or the guard pipeline. The kernel
1576    /// remains running so orchestrators and health probes see a live
1577    /// process; it is simply inert.
1578    ///
1579    /// The active capability set is NOT purged from the revocation store:
1580    /// the current `RevocationStore` trait has no bulk revoke API and
1581    /// capability expiration plus the kill-switch flag together satisfy
1582    /// Phase 1.4 acceptance. When a future revision adds `revoke_all`,
1583    /// this method should call it; until then, capability revocation is
1584    /// delegated to natural expiration.
1585    ///
1586    /// Fails closed: if the reason mutex is poisoned we still leave the
1587    /// kernel in the stopped state (the flag is set before any fallible
1588    /// step) and surface the poison to the caller.
1589    pub fn emergency_stop(&self, reason: &str) -> Result<(), KernelError> {
1590        let now = current_unix_timestamp();
1591        // Record the timestamp first so any concurrent reader that observes
1592        // `emergency_stopped == true` sees a non-zero `since` value.
1593        self.emergency_stopped_since.store(now, Ordering::SeqCst);
1594        self.emergency_stopped.store(true, Ordering::SeqCst);
1595
1596        warn!(
1597            reason = %reason,
1598            timestamp = now,
1599            "emergency stop engaged -- all evaluations will be denied"
1600        );
1601
1602        let mut slot = self.emergency_stop_reason.lock().map_err(|_| {
1603            KernelError::Internal("emergency stop reason mutex poisoned".to_string())
1604        })?;
1605        *slot = Some(reason.to_string());
1606        Ok(())
1607    }
1608
1609    /// Disengage the emergency kill switch and resume normal operation.
1610    ///
1611    /// Subsequent `evaluate_tool_call*` calls follow the full validation
1612    /// pipeline again. Capabilities that naturally expired while the
1613    /// kernel was stopped remain expired; the kill switch does not
1614    /// retroactively grant anything.
1615    pub fn emergency_resume(&self) -> Result<(), KernelError> {
1616        self.emergency_stopped.store(false, Ordering::SeqCst);
1617        self.emergency_stopped_since.store(0, Ordering::SeqCst);
1618
1619        warn!("emergency stop disengaged -- evaluations will resume");
1620
1621        let mut slot = self.emergency_stop_reason.lock().map_err(|_| {
1622            KernelError::Internal("emergency stop reason mutex poisoned".to_string())
1623        })?;
1624        *slot = None;
1625        Ok(())
1626    }
1627
1628    /// Return `true` when the emergency kill switch is engaged.
1629    #[must_use]
1630    pub fn is_emergency_stopped(&self) -> bool {
1631        self.emergency_stopped.load(Ordering::SeqCst)
1632    }
1633
1634    /// Return the unix timestamp (seconds) at which the kill switch was
1635    /// engaged, or `None` when the kernel is currently running normally.
1636    #[must_use]
1637    pub fn emergency_stopped_since(&self) -> Option<u64> {
1638        if !self.is_emergency_stopped() {
1639            return None;
1640        }
1641        let since = self.emergency_stopped_since.load(Ordering::SeqCst);
1642        if since == 0 {
1643            None
1644        } else {
1645            Some(since)
1646        }
1647    }
1648
1649    /// Return the operator-supplied reason for the current emergency stop,
1650    /// or `None` when the kernel is running normally or the mutex is
1651    /// poisoned (fail-closed readers should treat `None` as "no reason
1652    /// available").
1653    #[must_use]
1654    pub fn emergency_stop_reason(&self) -> Option<String> {
1655        if !self.is_emergency_stopped() {
1656            return None;
1657        }
1658        let guard = self.emergency_stop_reason.lock().ok()?;
1659        guard.clone()
1660    }
1661
1662    /// Install a DPoP nonce replay store and verification config.
1663    ///
1664    /// Once installed, any invocation whose matched grant has `dpop_required == Some(true)`
1665    /// must carry a valid `DpopProof` on the `ToolCallRequest`. Requests that lack a proof
1666    /// or whose proof fails verification are denied fail-closed.
1667    pub fn set_dpop_store(&mut self, nonce_store: dpop::DpopNonceStore, config: dpop::DpopConfig) {
1668        self.dpop_nonce_store = Some(nonce_store);
1669        self.dpop_config = Some(config);
1670    }
1671
1672    /// Phase 1.1: install an execution-nonce config and replay store.
1673    ///
1674    /// Once installed, every `Verdict::Allow` carries a short-lived signed
1675    /// nonce on `ToolCallResponse::execution_nonce`. Tool servers re-present
1676    /// that nonce via `ToolCallRequest::execution_nonce` and the kernel's
1677    /// `verify_presented_execution_nonce` helper (or directly via the
1678    /// free-standing `verify_execution_nonce` function) before executing.
1679    ///
1680    /// Set `config.require_nonce = true` to put the kernel into strict mode:
1681    /// any call that reaches `require_presented_execution_nonce` without a
1682    /// nonce is denied. When `require_nonce == false` the feature is opt-in
1683    /// per tool server and non-nonce callers continue to work (backward
1684    /// compatibility).
1685    pub fn set_execution_nonce_store(
1686        &mut self,
1687        config: crate::execution_nonce::ExecutionNonceConfig,
1688        store: Box<dyn crate::execution_nonce::ExecutionNonceStore>,
1689    ) {
1690        self.execution_nonce_config = Some(config);
1691        self.execution_nonce_store = Some(store);
1692    }
1693
1694    /// Returns `true` when execution-nonce strict mode is active.
1695    ///
1696    /// Strict mode requires every presented tool call to carry a fresh,
1697    /// valid, single-use nonce. When `false` the kernel is either not
1698    /// minting nonces at all (no config installed) or is in opt-in mode
1699    /// where tool servers can verify presented nonces but non-nonce calls
1700    /// are not outright rejected.
1701    #[must_use]
1702    pub fn execution_nonce_required(&self) -> bool {
1703        self.execution_nonce_config
1704            .as_ref()
1705            .is_some_and(|cfg| cfg.require_nonce)
1706    }
1707
1708    /// Phase 1.1: mint a signed execution nonce for an allow verdict.
1709    ///
1710    /// Returns `Ok(None)` when no config is installed (nonces disabled);
1711    /// returns `Ok(Some(nonce))` once configured. The nonce binding is
1712    /// derived from the capability subject, capability ID, target
1713    /// server/tool, and the canonical parameter hash embedded in the
1714    /// just-signed allow receipt so the verify-time check is always
1715    /// comparing apples to apples.
1716    pub(crate) fn mint_execution_nonce_for_allow(
1717        &self,
1718        request: &ToolCallRequest,
1719        cap: &CapabilityToken,
1720        receipt: &ChioReceipt,
1721    ) -> Result<Option<Box<crate::execution_nonce::SignedExecutionNonce>>, KernelError> {
1722        let Some(config) = self.execution_nonce_config.as_ref() else {
1723            return Ok(None);
1724        };
1725        let now = i64::try_from(current_unix_timestamp()).unwrap_or(i64::MAX);
1726        let binding = crate::execution_nonce::NonceBinding {
1727            subject_id: cap.subject.to_hex(),
1728            capability_id: cap.id.clone(),
1729            tool_server: request.server_id.clone(),
1730            tool_name: request.tool_name.clone(),
1731            parameter_hash: receipt.action.parameter_hash.clone(),
1732        };
1733        let signed = crate::execution_nonce::mint_execution_nonce(
1734            &self.config.keypair,
1735            binding,
1736            config,
1737            now,
1738        )?;
1739        Ok(Some(Box::new(signed)))
1740    }
1741
1742    /// Phase 1.1: verify a caller-presented execution nonce against the
1743    /// expected binding, consuming it in the replay store on success.
1744    ///
1745    /// Returns `Ok(())` when the nonce is fresh, correctly bound, signed
1746    /// by this kernel, and has not been consumed. Returns an error
1747    /// wrapping `ExecutionNonceError` on any failure (expired, tampered,
1748    /// replayed, binding mismatch, store unreachable).
1749    pub fn verify_presented_execution_nonce(
1750        &self,
1751        presented: &crate::execution_nonce::SignedExecutionNonce,
1752        expected: &crate::execution_nonce::NonceBinding,
1753    ) -> Result<(), crate::execution_nonce::ExecutionNonceError> {
1754        let store = self.execution_nonce_store.as_deref().ok_or_else(|| {
1755            crate::execution_nonce::ExecutionNonceError::Store(
1756                "execution nonce store is not installed".to_string(),
1757            )
1758        })?;
1759        let now = i64::try_from(current_unix_timestamp()).unwrap_or(i64::MAX);
1760        crate::execution_nonce::verify_execution_nonce(
1761            presented,
1762            &self.config.keypair.public_key(),
1763            expected,
1764            now,
1765            store,
1766        )
1767    }
1768
1769    /// Phase 1.1: strict-mode gate. Denies the call fail-closed when the
1770    /// kernel is configured to require nonces on every execution-bound
1771    /// tool call but the caller did not present one.
1772    ///
1773    /// `presented` is the nonce the tool server forwarded with the
1774    /// execution attempt (for example, lifted from the
1775    /// `X-Chio-Execution-Nonce` header). Passing the nonce as a separate
1776    /// argument keeps `ToolCallRequest` wire-stable: every other call
1777    /// site that builds a request (guards, adapters, tests) continues to
1778    /// compile unchanged, and strict mode is gated on the integration
1779    /// layer that knows how to shuttle the nonce header.
1780    ///
1781    /// Returns `Ok(())` when:
1782    /// * strict mode is disabled (backward-compat path), OR
1783    /// * a nonce is presented, signed by this kernel, correctly bound,
1784    ///   non-expired, and has not been consumed.
1785    ///
1786    /// Returns `Err(KernelError::Internal(...))` fail-closed otherwise.
1787    pub fn require_presented_execution_nonce(
1788        &self,
1789        request: &ToolCallRequest,
1790        cap: &CapabilityToken,
1791        presented: Option<&crate::execution_nonce::SignedExecutionNonce>,
1792    ) -> Result<(), KernelError> {
1793        if !self.execution_nonce_required() {
1794            return Ok(());
1795        }
1796        let presented = presented.ok_or_else(|| {
1797            KernelError::Internal(
1798                "execution nonce required but not presented on tool call".to_string(),
1799            )
1800        })?;
1801        let parameter_hash =
1802            chio_core::receipt::ToolCallAction::from_parameters(request.arguments.clone())
1803                .map_err(|e| {
1804                    KernelError::ReceiptSigningFailed(format!("failed to hash parameters: {e}"))
1805                })?
1806                .parameter_hash;
1807        let expected = crate::execution_nonce::NonceBinding {
1808            subject_id: cap.subject.to_hex(),
1809            capability_id: cap.id.clone(),
1810            tool_server: request.server_id.clone(),
1811            tool_name: request.tool_name.clone(),
1812            parameter_hash,
1813        };
1814        self.verify_presented_execution_nonce(presented, &expected)
1815            .map_err(|e| KernelError::Internal(format!("{e}")))
1816    }
1817
1818    pub fn requires_web3_evidence(&self) -> bool {
1819        self.config.require_web3_evidence
1820    }
1821
1822    pub fn validate_web3_evidence_prerequisites(&self) -> Result<(), KernelError> {
1823        if !self.requires_web3_evidence() {
1824            return Ok(());
1825        }
1826
1827        let Some(supports_kernel_signed_checkpoints) =
1828            self.with_receipt_store(|store| Ok(store.supports_kernel_signed_checkpoints()))?
1829        else {
1830            return Err(KernelError::Web3EvidenceUnavailable(
1831                "web3-enabled deployments require a durable receipt store".to_string(),
1832            ));
1833        };
1834
1835        if self.checkpoint_batch_size == 0 {
1836            return Err(KernelError::Web3EvidenceUnavailable(
1837                "web3-enabled deployments require checkpoint_batch_size > 0".to_string(),
1838            ));
1839        }
1840
1841        if !supports_kernel_signed_checkpoints {
1842            return Err(KernelError::Web3EvidenceUnavailable(
1843                "web3-enabled deployments require local receipt persistence with kernel-signed checkpoint support; append-only remote receipt mirrors are unsupported".to_string(),
1844            ));
1845        }
1846
1847        Ok(())
1848    }
1849
1850    /// Register a policy guard. Guards are evaluated in registration order.
1851    /// If any guard denies, the request is denied.
1852    pub fn add_guard(&mut self, guard: Box<dyn Guard>) {
1853        self.guards.push(guard);
1854    }
1855
1856    /// Register a tool server connection.
1857    pub fn register_tool_server(&mut self, connection: Box<dyn ToolServerConnection>) {
1858        let id = connection.server_id().to_owned();
1859        info!(server_id = %id, "registering tool server");
1860        self.tool_servers.insert(id, connection);
1861    }
1862
1863    /// Register a resource provider.
1864    pub fn register_resource_provider(&mut self, provider: Box<dyn ResourceProvider>) {
1865        info!("registering resource provider");
1866        self.resource_providers.push(provider);
1867    }
1868
1869    /// Register a prompt provider.
1870    pub fn register_prompt_provider(&mut self, provider: Box<dyn PromptProvider>) {
1871        info!("registering prompt provider");
1872        self.prompt_providers.push(provider);
1873    }
1874
1875    /// Open a new logical session for an agent and bind any capabilities that
1876    /// were issued during setup to that session.
1877    fn validate_non_tool_capability(
1878        &self,
1879        capability: &CapabilityToken,
1880        agent_id: &str,
1881    ) -> Result<(), KernelError> {
1882        // Phase 1.4 emergency kill switch: resource/prompt operations that go
1883        // through this helper must also deny-fast so the kill switch applies
1884        // to every capability-backed surface, not just tool calls.
1885        if self.is_emergency_stopped() {
1886            return Err(KernelError::GuardDenied(
1887                EMERGENCY_STOP_DENY_REASON.to_string(),
1888            ));
1889        }
1890        self.verify_capability_signature(capability)
1891            .map_err(|_| KernelError::InvalidSignature)?;
1892        check_time_bounds(capability, current_unix_timestamp())?;
1893        self.check_revocation(capability)?;
1894        self.validate_delegation_admission(capability)?;
1895        check_subject_binding(capability, agent_id)?;
1896        Ok(())
1897    }
1898
1899    /// Evaluate a tool call request.
1900    ///
1901    /// This is the kernel's main entry point. It performs the full validation
1902    /// pipeline:
1903    ///
1904    /// 1. Verify capability signature against known CA public keys.
1905    /// 2. Check time bounds (not expired, not-before satisfied).
1906    /// 3. Check revocation status of the capability and its delegation chain.
1907    /// 4. Verify the requested tool is within the capability's scope.
1908    /// 5. Check and decrement invocation budget.
1909    /// 6. Run all registered guards.
1910    /// 7. If all pass: forward to tool server, sign allow receipt.
1911    /// 8. If any fail: sign deny receipt.
1912    ///
1913    /// Every call -- whether allowed or denied -- produces exactly one signed
1914    /// receipt.
1915    pub async fn evaluate_tool_call(
1916        &self,
1917        request: &ToolCallRequest,
1918    ) -> Result<ToolCallResponse, KernelError> {
1919        self.evaluate_tool_call_sync_with_session_roots(request, None, None)
1920    }
1921
1922    pub fn evaluate_tool_call_blocking(
1923        &self,
1924        request: &ToolCallRequest,
1925    ) -> Result<ToolCallResponse, KernelError> {
1926        self.evaluate_tool_call_sync_with_session_roots(request, None, None)
1927    }
1928
1929    pub fn evaluate_tool_call_blocking_with_metadata(
1930        &self,
1931        request: &ToolCallRequest,
1932        extra_metadata: Option<serde_json::Value>,
1933    ) -> Result<ToolCallResponse, KernelError> {
1934        self.evaluate_tool_call_sync_with_session_roots(request, None, extra_metadata)
1935    }
1936
1937    pub fn sign_planned_deny_response(
1938        &self,
1939        request: &ToolCallRequest,
1940        reason: &str,
1941        extra_metadata: Option<serde_json::Value>,
1942    ) -> Result<ToolCallResponse, KernelError> {
1943        self.build_deny_response_with_metadata(
1944            request,
1945            reason,
1946            current_unix_timestamp(),
1947            None,
1948            extra_metadata,
1949        )
1950    }
1951
1952    /// Phase 2.4 plan-level evaluation.
1953    ///
1954    /// Takes an ordered list of planned tool calls under a single
1955    /// capability token and evaluates every step INDEPENDENTLY against
1956    /// the pre-invocation portion of the evaluation pipeline: capability
1957    /// signature / time-bound / revocation / subject binding, the
1958    /// request-matching pass (scope + constraints + model constraint),
1959    /// and the registered guard pipeline. No tool-server dispatch, no
1960    /// budget mutation, no receipt emission, and no cross-step state
1961    /// propagation take place: this is a stateless pre-flight check.
1962    ///
1963    /// Dependencies between planned steps are advisory metadata only in
1964    /// v1: the kernel does not topologically sort the graph, refuse on
1965    /// cycles, or short-circuit downstream steps when an earlier step
1966    /// denies. Callers are expected to make that decision themselves
1967    /// once they have the per-step verdict list.
1968    ///
1969    /// Guards that require post-invocation output (response-shaping,
1970    /// streaming sanitizers, etc.) are inherently skipped because no
1971    /// tool output exists; in Phase 2.4 every registered guard is
1972    /// invoked against the synthesised pre-flight request, matching the
1973    /// set of guards that run in `evaluate_tool_call` before dispatch.
1974    ///
1975    /// Receipt emission is deferred to a future phase. The kernel emits
1976    /// structured trace spans for the plan and every per-step verdict
1977    /// so operators can correlate plan evaluations with subsequent
1978    /// tool-call receipts.
1979    pub async fn evaluate_plan(
1980        &self,
1981        req: chio_core_types::PlanEvaluationRequest,
1982    ) -> chio_core_types::PlanEvaluationResponse {
1983        self.evaluate_plan_blocking(&req)
1984    }
1985
1986    /// Synchronous variant of [`Self::evaluate_plan`] for substrate
1987    /// adapters that do not run on an async runtime.
1988    ///
1989    /// Plan evaluation never touches the network, so the async method
1990    /// is a thin wrapper over this blocking implementation.
1991    pub fn evaluate_plan_blocking(
1992        &self,
1993        req: &chio_core_types::PlanEvaluationRequest,
1994    ) -> chio_core_types::PlanEvaluationResponse {
1995        use chio_core_types::{PlanEvaluationResponse, PlanVerdict, StepVerdict, StepVerdictKind};
1996
1997        debug!(
1998            plan_id = %req.plan_id,
1999            planner_capability_id = %req.planner_capability_id,
2000            step_count = req.steps.len(),
2001            "evaluating plan"
2002        );
2003
2004        let mut step_verdicts = Vec::with_capacity(req.steps.len());
2005
2006        // Reject capability-id mismatches once, up front: every step is
2007        // evaluated under the same token so a mismatch is fatal for the
2008        // whole plan. Fail-closed: every step is flagged denied.
2009        if req.planner_capability.id != req.planner_capability_id {
2010            let reason = format!(
2011                "planner_capability_id {} does not match embedded token id {}",
2012                req.planner_capability_id, req.planner_capability.id
2013            );
2014            for (index, _) in req.steps.iter().enumerate() {
2015                step_verdicts.push(StepVerdict {
2016                    step_index: index,
2017                    verdict: StepVerdictKind::Denied,
2018                    reason: Some(reason.clone()),
2019                    guard: None,
2020                });
2021            }
2022            let plan_verdict = if step_verdicts.is_empty() {
2023                PlanVerdict::FullyDenied
2024            } else {
2025                PlanEvaluationResponse::aggregate(&step_verdicts)
2026            };
2027            return PlanEvaluationResponse {
2028                plan_id: req.plan_id.clone(),
2029                plan_verdict,
2030                step_verdicts,
2031            };
2032        }
2033
2034        // Emergency stop applies to plan evaluation too: a stopped kernel
2035        // must not leak any information about what the plan might allow.
2036        if self.is_emergency_stopped() {
2037            warn!(
2038                plan_id = %req.plan_id,
2039                "emergency stop active -- denying evaluate_plan"
2040            );
2041            for (index, _) in req.steps.iter().enumerate() {
2042                step_verdicts.push(StepVerdict {
2043                    step_index: index,
2044                    verdict: StepVerdictKind::Denied,
2045                    reason: Some(EMERGENCY_STOP_DENY_REASON.to_string()),
2046                    guard: None,
2047                });
2048            }
2049            let plan_verdict = if step_verdicts.is_empty() {
2050                PlanVerdict::FullyDenied
2051            } else {
2052                PlanEvaluationResponse::aggregate(&step_verdicts)
2053            };
2054            return PlanEvaluationResponse {
2055                plan_id: req.plan_id.clone(),
2056                plan_verdict,
2057                step_verdicts,
2058            };
2059        }
2060
2061        for (index, step) in req.steps.iter().enumerate() {
2062            let verdict = self.evaluate_plan_step(req, step, index);
2063            step_verdicts.push(verdict);
2064        }
2065
2066        let plan_verdict = PlanEvaluationResponse::aggregate(&step_verdicts);
2067
2068        debug!(
2069            plan_id = %req.plan_id,
2070            plan_verdict = ?plan_verdict,
2071            "plan evaluation complete"
2072        );
2073
2074        PlanEvaluationResponse {
2075            plan_id: req.plan_id.clone(),
2076            plan_verdict,
2077            step_verdicts,
2078        }
2079    }
2080
2081    fn evaluate_plan_step(
2082        &self,
2083        req: &chio_core_types::PlanEvaluationRequest,
2084        step: &chio_core_types::PlannedToolCall,
2085        index: usize,
2086    ) -> chio_core_types::StepVerdict {
2087        use chio_core_types::{StepVerdict, StepVerdictKind};
2088
2089        let now = current_unix_timestamp();
2090        let cap = &req.planner_capability;
2091
2092        // Capability-wide checks repeat per-step so a failure here is
2093        // still reflected in every step's verdict, keeping the per-step
2094        // output self-contained.
2095        if let Err(reason) = self.verify_capability_signature(cap) {
2096            return StepVerdict {
2097                step_index: index,
2098                verdict: StepVerdictKind::Denied,
2099                reason: Some(format!("signature verification failed: {reason}")),
2100                guard: None,
2101            };
2102        }
2103        if let Err(error) = check_time_bounds(cap, now) {
2104            return StepVerdict {
2105                step_index: index,
2106                verdict: StepVerdictKind::Denied,
2107                reason: Some(error.to_string()),
2108                guard: None,
2109            };
2110        }
2111        if let Err(error) = self.check_revocation(cap) {
2112            return StepVerdict {
2113                step_index: index,
2114                verdict: StepVerdictKind::Denied,
2115                reason: Some(error.to_string()),
2116                guard: None,
2117            };
2118        }
2119        if let Err(error) = check_subject_binding(cap, &req.agent_id) {
2120            return StepVerdict {
2121                step_index: index,
2122                verdict: StepVerdictKind::Denied,
2123                reason: Some(error.to_string()),
2124                guard: None,
2125            };
2126        }
2127
2128        // Synthesise a ToolCallRequest so the same request-matching and
2129        // guard machinery applies to plan steps as to runtime calls. No
2130        // DPoP / governed-intent / approval-token shape is carried: plan
2131        // evaluation is a pre-flight check and is not a substitute for
2132        // those runtime-only proofs.
2133        let synthesised = ToolCallRequest {
2134            request_id: step.request_id.clone(),
2135            capability: cap.clone(),
2136            tool_name: step.tool_name.clone(),
2137            server_id: step.server_id.clone(),
2138            agent_id: req.agent_id.clone(),
2139            arguments: step.parameters.clone(),
2140            dpop_proof: None,
2141            governed_intent: None,
2142            approval_token: None,
2143            model_metadata: step.model_metadata.clone(),
2144            federated_origin_kernel_id: None,
2145        };
2146
2147        let matching_grants = match resolve_matching_grants(
2148            cap,
2149            &synthesised.tool_name,
2150            &synthesised.server_id,
2151            &synthesised.arguments,
2152            synthesised.model_metadata.as_ref(),
2153        ) {
2154            Ok(grants) if !grants.is_empty() => grants,
2155            Ok(_) => {
2156                let error = KernelError::OutOfScope {
2157                    tool: synthesised.tool_name.clone(),
2158                    server: synthesised.server_id.clone(),
2159                };
2160                return StepVerdict {
2161                    step_index: index,
2162                    verdict: StepVerdictKind::Denied,
2163                    reason: Some(error.to_string()),
2164                    guard: None,
2165                };
2166            }
2167            Err(error) => {
2168                return StepVerdict {
2169                    step_index: index,
2170                    verdict: StepVerdictKind::Denied,
2171                    reason: Some(error.to_string()),
2172                    guard: None,
2173                };
2174            }
2175        };
2176
2177        let matched_grant_index = matching_grants
2178            .first()
2179            .map(|matching| matching.index)
2180            .unwrap_or(0);
2181
2182        // run_guards returns Ok(()) on allow and Err(GuardDenied(...))
2183        // on deny. Fail-closed: any guard error reads as a denial so the
2184        // caller still sees a per-step reason string.
2185        if let Err(error) =
2186            self.run_guards(&synthesised, &cap.scope, None, Some(matched_grant_index))
2187        {
2188            // Attempt to extract the offending guard name from the
2189            // canonical `guard "<name>" denied the request` format
2190            // emitted by run_guards.
2191            let message = error.to_string();
2192            let guard = extract_guard_name(&message);
2193            return StepVerdict {
2194                step_index: index,
2195                verdict: StepVerdictKind::Denied,
2196                reason: Some(message),
2197                guard,
2198            };
2199        }
2200
2201        StepVerdict {
2202            step_index: index,
2203            verdict: StepVerdictKind::Allowed,
2204            reason: None,
2205            guard: None,
2206        }
2207    }
2208
2209    fn evaluate_tool_call_sync_with_session_roots(
2210        &self,
2211        request: &ToolCallRequest,
2212        session_filesystem_roots: Option<&[String]>,
2213        extra_metadata: Option<serde_json::Value>,
2214    ) -> Result<ToolCallResponse, KernelError> {
2215        self.evaluate_tool_call_sync_with_session_context(
2216            request,
2217            session_filesystem_roots,
2218            extra_metadata,
2219            None,
2220        )
2221    }
2222
2223    /// Evaluate a tool call sync path with access to the owning session,
2224    /// so the kernel can tag the resulting receipt with the session's
2225    /// tenant_id (Phase 1.5 multi-tenant receipt isolation).
2226    ///
2227    /// `session_id` is the session that authenticated the caller, used only
2228    /// to resolve the tenant from `auth_context().enterprise_identity`. The
2229    /// tenant_id is NEVER read from `request` itself -- accepting a caller-
2230    /// provided tenant would defeat the isolation guarantee.
2231    fn evaluate_tool_call_sync_with_session_context(
2232        &self,
2233        request: &ToolCallRequest,
2234        session_filesystem_roots: Option<&[String]>,
2235        extra_metadata: Option<serde_json::Value>,
2236        session_id: Option<&SessionId>,
2237    ) -> Result<ToolCallResponse, KernelError> {
2238        // Resolve tenant_id from the session's enterprise identity context
2239        // (if any) and install it for the remainder of this evaluation so
2240        // every receipt `build_and_sign_receipt` signs picks up the tag.
2241        let tenant_id = self.resolve_tenant_id_for_session(session_id);
2242        let _tenant_scope = scope_receipt_tenant_id(tenant_id);
2243
2244        let now = current_unix_timestamp();
2245
2246        // Phase 1.4 emergency kill switch: every evaluate path checks the flag
2247        // BEFORE capability validation, guard evaluation, or budget mutation so
2248        // a stopped kernel cannot be coerced into doing any work.
2249        if self.is_emergency_stopped() {
2250            warn!(
2251                request_id = %request.request_id,
2252                "emergency stop active -- denying evaluate_tool_call"
2253            );
2254            return self.build_deny_response_with_metadata(
2255                request,
2256                EMERGENCY_STOP_DENY_REASON,
2257                now,
2258                None,
2259                extra_metadata.clone(),
2260            );
2261        }
2262
2263        self.validate_web3_evidence_prerequisites()?;
2264
2265        debug!(
2266            request_id = %request.request_id,
2267            tool = %request.tool_name,
2268            server = %request.server_id,
2269            "evaluating tool call"
2270        );
2271
2272        let cap = &request.capability;
2273
2274        if let Err(reason) = self.verify_capability_signature(cap) {
2275            let msg = format!("signature verification failed: {reason}");
2276            warn!(request_id = %request.request_id, %msg, "capability rejected");
2277            return self.build_deny_response_with_metadata(
2278                request,
2279                &msg,
2280                now,
2281                None,
2282                extra_metadata.clone(),
2283            );
2284        }
2285
2286        if let Err(e) = check_time_bounds(cap, now) {
2287            let msg = e.to_string();
2288            warn!(request_id = %request.request_id, reason = %msg, "capability rejected");
2289            return self.build_deny_response_with_metadata(
2290                request,
2291                &msg,
2292                now,
2293                None,
2294                extra_metadata.clone(),
2295            );
2296        }
2297
2298        if let Err(e) = self.check_revocation(cap) {
2299            let msg = e.to_string();
2300            warn!(request_id = %request.request_id, reason = %msg, "capability rejected");
2301            return self.build_deny_response_with_metadata(
2302                request,
2303                &msg,
2304                now,
2305                None,
2306                extra_metadata.clone(),
2307            );
2308        }
2309
2310        if let Err(e) = self.validate_delegation_admission(cap) {
2311            let msg = e.to_string();
2312            warn!(request_id = %request.request_id, reason = %msg, "capability rejected");
2313            return self.build_deny_response_with_metadata(
2314                request,
2315                &msg,
2316                now,
2317                None,
2318                extra_metadata.clone(),
2319            );
2320        }
2321
2322        if let Err(e) = check_subject_binding(cap, &request.agent_id) {
2323            let msg = e.to_string();
2324            warn!(request_id = %request.request_id, reason = %msg, "capability rejected");
2325            return self.build_deny_response_with_metadata(
2326                request,
2327                &msg,
2328                now,
2329                None,
2330                extra_metadata.clone(),
2331            );
2332        }
2333
2334        let matching_grants = match resolve_matching_grants(
2335            cap,
2336            &request.tool_name,
2337            &request.server_id,
2338            &request.arguments,
2339            request.model_metadata.as_ref(),
2340        ) {
2341            Ok(grants) if !grants.is_empty() => grants,
2342            Ok(_) => {
2343                let e = KernelError::OutOfScope {
2344                    tool: request.tool_name.clone(),
2345                    server: request.server_id.clone(),
2346                };
2347                let msg = e.to_string();
2348                warn!(request_id = %request.request_id, reason = %msg, "capability rejected");
2349                return self.build_deny_response_with_metadata(
2350                    request,
2351                    &msg,
2352                    now,
2353                    None,
2354                    extra_metadata.clone(),
2355                );
2356            }
2357            Err(e) => {
2358                let msg = e.to_string();
2359                warn!(request_id = %request.request_id, reason = %msg, "capability rejected");
2360                return self.build_deny_response_with_metadata(
2361                    request,
2362                    &msg,
2363                    now,
2364                    None,
2365                    extra_metadata.clone(),
2366                );
2367            }
2368        };
2369
2370        // DPoP enforcement before budget charge: if any matching grant requires
2371        // DPoP, verify the proof now so an attacker cannot drain the budget with
2372        // a valid capability token but missing or invalid DPoP proof.
2373        if matching_grants
2374            .iter()
2375            .any(|m| m.grant.dpop_required == Some(true))
2376        {
2377            if let Err(e) = self.verify_dpop_for_request(request, cap) {
2378                let msg = e.to_string();
2379                warn!(request_id = %request.request_id, reason = %msg, "DPoP verification failed");
2380                return self.build_deny_response_with_metadata(
2381                    request,
2382                    &msg,
2383                    now,
2384                    None,
2385                    extra_metadata.clone(),
2386                );
2387            }
2388        }
2389
2390        if let Err(e) = self.ensure_registered_tool_target(request) {
2391            let msg = e.to_string();
2392            warn!(request_id = %request.request_id, reason = %msg, "tool target not registered");
2393            return self.build_deny_response_with_metadata(
2394                request,
2395                &msg,
2396                now,
2397                None,
2398                extra_metadata.clone(),
2399            );
2400        }
2401
2402        if let Err(error) = self.record_observed_capability_snapshot(cap) {
2403            let msg = error.to_string();
2404            warn!(request_id = %request.request_id, reason = %msg, "failed to persist capability lineage");
2405            return self.build_deny_response_with_metadata(
2406                request,
2407                &msg,
2408                now,
2409                None,
2410                extra_metadata.clone(),
2411            );
2412        }
2413
2414        let (matched_grant_index, charge_result) =
2415            match self.check_and_increment_budget(&request.request_id, cap, &matching_grants) {
2416                Ok(result) => result,
2417                Err(e) => {
2418                    let msg = e.to_string();
2419                    warn!(request_id = %request.request_id, reason = %msg, "capability rejected");
2420                    // For monetary budget exhaustion, build a denial receipt with financial metadata.
2421                    return self.build_monetary_deny_response_with_metadata(
2422                        request,
2423                        &msg,
2424                        now,
2425                        &matching_grants,
2426                        cap,
2427                        self.merge_budget_receipt_metadata(
2428                            extra_metadata.clone(),
2429                            self.budget_backend_receipt_metadata()?,
2430                        ),
2431                    );
2432                }
2433            };
2434
2435        let matched_grant = matching_grants
2436            .iter()
2437            .find(|matching| matching.index == matched_grant_index)
2438            .map(|matching| matching.grant)
2439            .ok_or_else(|| {
2440                KernelError::Internal(format!(
2441                    "matched grant index {matched_grant_index} missing from candidate set"
2442                ))
2443            })?;
2444
2445        let validated_governed_admission = match self.validate_governed_transaction(
2446            request,
2447            cap,
2448            matched_grant,
2449            charge_result.as_ref(),
2450            None,
2451            now,
2452        ) {
2453            Ok(validated_governed_admission) => validated_governed_admission,
2454            Err(error) => {
2455                let msg = error.to_string();
2456                warn!(request_id = %request.request_id, reason = %msg, "governed transaction denied");
2457                if let Some(ref charge) = charge_result {
2458                    let reverse = self.reverse_budget_charge(&cap.id, charge)?;
2459                    return self.build_pre_execution_monetary_deny_response_with_metadata(
2460                        request,
2461                        &msg,
2462                        now,
2463                        charge,
2464                        reverse.committed_cost_units_after,
2465                        cap,
2466                        self.merge_budget_receipt_metadata(
2467                            extra_metadata.clone(),
2468                            self.budget_execution_receipt_metadata(
2469                                charge,
2470                                Some(("reversed", &reverse)),
2471                            ),
2472                        ),
2473                    );
2474                }
2475                return self.build_deny_response_with_metadata(
2476                    request,
2477                    &msg,
2478                    now,
2479                    Some(matched_grant_index),
2480                    extra_metadata.clone(),
2481                );
2482            }
2483        };
2484        let _governed_runtime_attestation_receipt_scope =
2485            scope_governed_runtime_attestation_receipt_record(
2486                validated_governed_admission
2487                    .as_ref()
2488                    .and_then(|admission| admission.verified_runtime_attestation.clone()),
2489            );
2490        let _governed_call_chain_receipt_evidence_scope =
2491            scope_governed_call_chain_receipt_evidence(
2492                self.governed_call_chain_receipt_evidence(
2493                    request,
2494                    cap,
2495                    None,
2496                    validated_governed_admission
2497                        .as_ref()
2498                        .and_then(|admission| admission.call_chain_proof.clone()),
2499                ),
2500            );
2501
2502        if let Err(e) = self.run_guards(
2503            request,
2504            &cap.scope,
2505            session_filesystem_roots,
2506            Some(matched_grant_index),
2507        ) {
2508            let msg = e.to_string();
2509            warn!(request_id = %request.request_id, reason = %msg, "guard denied");
2510            if let Some(ref charge) = charge_result {
2511                let reverse = self.reverse_budget_charge(&cap.id, charge)?;
2512                return self.build_pre_execution_monetary_deny_response_with_metadata(
2513                    request,
2514                    &msg,
2515                    now,
2516                    charge,
2517                    reverse.committed_cost_units_after,
2518                    cap,
2519                    self.merge_budget_receipt_metadata(
2520                        extra_metadata.clone(),
2521                        self.budget_execution_receipt_metadata(
2522                            charge,
2523                            Some(("reversed", &reverse)),
2524                        ),
2525                    ),
2526                );
2527            }
2528            return self.build_deny_response_with_metadata(
2529                request,
2530                &msg,
2531                now,
2532                Some(matched_grant_index),
2533                extra_metadata.clone(),
2534            );
2535        }
2536
2537        let payment_authorization =
2538            match self.authorize_payment_if_needed(request, charge_result.as_ref()) {
2539                Ok(authorization) => authorization,
2540                Err(error) => {
2541                    let msg = format!("payment authorization failed: {error}");
2542                    warn!(request_id = %request.request_id, reason = %msg, "payment denied");
2543                    if let Some(ref charge) = charge_result {
2544                        let reverse = self.reverse_budget_charge(&cap.id, charge)?;
2545                        return self.build_pre_execution_monetary_deny_response_with_metadata(
2546                            request,
2547                            &msg,
2548                            now,
2549                            charge,
2550                            reverse.committed_cost_units_after,
2551                            cap,
2552                            self.merge_budget_receipt_metadata(
2553                                extra_metadata.clone(),
2554                                self.budget_execution_receipt_metadata(
2555                                    charge,
2556                                    Some(("reversed", &reverse)),
2557                                ),
2558                            ),
2559                        );
2560                    }
2561                    return self.build_deny_response_with_metadata(
2562                        request,
2563                        &msg,
2564                        now,
2565                        Some(matched_grant_index),
2566                        extra_metadata.clone(),
2567                    );
2568                }
2569            };
2570
2571        let tool_started_at = Instant::now();
2572        let has_monetary = charge_result.is_some();
2573        let (tool_output, reported_cost) =
2574            match self.dispatch_tool_call_with_cost(request, has_monetary) {
2575                Ok(result) => result,
2576                Err(error @ KernelError::UrlElicitationsRequired { .. }) => {
2577                    let _ = self.unwind_aborted_monetary_invocation(
2578                        request,
2579                        cap,
2580                        charge_result.as_ref(),
2581                        payment_authorization.as_ref(),
2582                    )?;
2583                    warn!(
2584                        request_id = %request.request_id,
2585                        reason = %error,
2586                        "tool call requires URL elicitation"
2587                    );
2588                    return Err(error);
2589                }
2590                Err(KernelError::RequestCancelled { reason, .. }) => {
2591                    let unwind = self.unwind_aborted_monetary_invocation(
2592                        request,
2593                        cap,
2594                        charge_result.as_ref(),
2595                        payment_authorization.as_ref(),
2596                    )?;
2597                    warn!(
2598                        request_id = %request.request_id,
2599                        reason = %reason,
2600                        "tool call cancelled"
2601                    );
2602                    return self.build_cancelled_response_with_metadata(
2603                        request,
2604                        &reason,
2605                        now,
2606                        Some(matched_grant_index),
2607                        match (charge_result.as_ref(), unwind.as_ref()) {
2608                            (Some(charge), Some(reverse)) => self.merge_budget_receipt_metadata(
2609                                extra_metadata.clone(),
2610                                self.budget_execution_receipt_metadata(
2611                                    charge,
2612                                    Some(("reversed", reverse)),
2613                                ),
2614                            ),
2615                            _ => extra_metadata.clone(),
2616                        },
2617                    );
2618                }
2619                Err(KernelError::RequestIncomplete(reason)) => {
2620                    let unwind = self.unwind_aborted_monetary_invocation(
2621                        request,
2622                        cap,
2623                        charge_result.as_ref(),
2624                        payment_authorization.as_ref(),
2625                    )?;
2626                    warn!(
2627                        request_id = %request.request_id,
2628                        reason = %reason,
2629                        "tool call incomplete"
2630                    );
2631                    return self.build_incomplete_response_with_output_and_metadata(
2632                        request,
2633                        None,
2634                        &reason,
2635                        now,
2636                        Some(matched_grant_index),
2637                        match (charge_result.as_ref(), unwind.as_ref()) {
2638                            (Some(charge), Some(reverse)) => self.merge_budget_receipt_metadata(
2639                                extra_metadata.clone(),
2640                                self.budget_execution_receipt_metadata(
2641                                    charge,
2642                                    Some(("reversed", reverse)),
2643                                ),
2644                            ),
2645                            _ => extra_metadata.clone(),
2646                        },
2647                    );
2648                }
2649                Err(e) => {
2650                    let unwind = self.unwind_aborted_monetary_invocation(
2651                        request,
2652                        cap,
2653                        charge_result.as_ref(),
2654                        payment_authorization.as_ref(),
2655                    )?;
2656                    let msg = e.to_string();
2657                    warn!(request_id = %request.request_id, reason = %msg, "tool server error");
2658                    return self.build_deny_response_with_metadata(
2659                        request,
2660                        &msg,
2661                        now,
2662                        Some(matched_grant_index),
2663                        match (charge_result.as_ref(), unwind.as_ref()) {
2664                            (Some(charge), Some(reverse)) => self.merge_budget_receipt_metadata(
2665                                extra_metadata.clone(),
2666                                self.budget_execution_receipt_metadata(
2667                                    charge,
2668                                    Some(("reversed", reverse)),
2669                                ),
2670                            ),
2671                            _ => extra_metadata.clone(),
2672                        },
2673                    );
2674                }
2675            };
2676        self.finalize_budgeted_tool_output_with_cost_and_metadata(
2677            request,
2678            tool_output,
2679            tool_started_at.elapsed(),
2680            now,
2681            matched_grant_index,
2682            FinalizeToolOutputCostContext {
2683                charge_result,
2684                reported_cost,
2685                payment_authorization,
2686                cap,
2687            },
2688            extra_metadata,
2689        )
2690    }
2691
2692    fn evaluate_tool_call_with_nested_flow_client<C: NestedFlowClient>(
2693        &self,
2694        parent_context: &OperationContext,
2695        request: &ToolCallRequest,
2696        client: &mut C,
2697    ) -> Result<ToolCallResponse, KernelError> {
2698        // Phase 1.5: install the parent session's tenant_id so every
2699        // receipt signed while this nested-flow evaluation is in flight
2700        // carries the correct tenant tag.
2701        let tenant_id = self.resolve_tenant_id_for_session(Some(&parent_context.session_id));
2702        let _tenant_scope = scope_receipt_tenant_id(tenant_id);
2703
2704        let now = current_unix_timestamp();
2705
2706        // Phase 1.4 emergency kill switch: the nested-flow path also deny-fast
2707        // so sampling/elicitation-bearing tool calls cannot slip past while
2708        // the kernel is stopped.
2709        if self.is_emergency_stopped() {
2710            warn!(
2711                request_id = %request.request_id,
2712                "emergency stop active -- denying evaluate_tool_call (nested flow)"
2713            );
2714            return self.build_deny_response(request, EMERGENCY_STOP_DENY_REASON, now, None);
2715        }
2716
2717        self.validate_web3_evidence_prerequisites()?;
2718
2719        debug!(
2720            request_id = %request.request_id,
2721            tool = %request.tool_name,
2722            server = %request.server_id,
2723            "evaluating tool call with nested-flow bridge"
2724        );
2725
2726        let cap = &request.capability;
2727
2728        if let Err(reason) = self.verify_capability_signature(cap) {
2729            let msg = format!("signature verification failed: {reason}");
2730            warn!(request_id = %request.request_id, %msg, "capability rejected");
2731            return self.build_deny_response(request, &msg, now, None);
2732        }
2733
2734        if let Err(e) = check_time_bounds(cap, now) {
2735            let msg = e.to_string();
2736            warn!(request_id = %request.request_id, reason = %msg, "capability rejected");
2737            return self.build_deny_response(request, &msg, now, None);
2738        }
2739
2740        if let Err(e) = self.check_revocation(cap) {
2741            let msg = e.to_string();
2742            warn!(request_id = %request.request_id, reason = %msg, "capability rejected");
2743            return self.build_deny_response(request, &msg, now, None);
2744        }
2745
2746        if let Err(e) = self.validate_delegation_admission(cap) {
2747            let msg = e.to_string();
2748            warn!(request_id = %request.request_id, reason = %msg, "capability rejected");
2749            return self.build_deny_response(request, &msg, now, None);
2750        }
2751
2752        if let Err(e) = check_subject_binding(cap, &request.agent_id) {
2753            let msg = e.to_string();
2754            warn!(request_id = %request.request_id, reason = %msg, "capability rejected");
2755            return self.build_deny_response(request, &msg, now, None);
2756        }
2757
2758        let matching_grants = match resolve_matching_grants(
2759            cap,
2760            &request.tool_name,
2761            &request.server_id,
2762            &request.arguments,
2763            request.model_metadata.as_ref(),
2764        ) {
2765            Ok(grants) if !grants.is_empty() => grants,
2766            Ok(_) => {
2767                let e = KernelError::OutOfScope {
2768                    tool: request.tool_name.clone(),
2769                    server: request.server_id.clone(),
2770                };
2771                let msg = e.to_string();
2772                warn!(request_id = %request.request_id, reason = %msg, "capability rejected");
2773                return self.build_deny_response(request, &msg, now, None);
2774            }
2775            Err(e) => {
2776                let msg = e.to_string();
2777                warn!(request_id = %request.request_id, reason = %msg, "capability rejected");
2778                return self.build_deny_response(request, &msg, now, None);
2779            }
2780        };
2781
2782        // DPoP enforcement before budget charge: if any matching grant requires
2783        // DPoP, verify the proof now so an attacker cannot drain the budget with
2784        // a valid capability token but missing or invalid DPoP proof.
2785        if matching_grants
2786            .iter()
2787            .any(|m| m.grant.dpop_required == Some(true))
2788        {
2789            if let Err(e) = self.verify_dpop_for_request(request, cap) {
2790                let msg = e.to_string();
2791                warn!(request_id = %request.request_id, reason = %msg, "DPoP verification failed");
2792                return self.build_deny_response(request, &msg, now, None);
2793            }
2794        }
2795
2796        if let Err(e) = self.ensure_registered_tool_target(request) {
2797            let msg = e.to_string();
2798            warn!(request_id = %request.request_id, reason = %msg, "tool target not registered");
2799            return self.build_deny_response(request, &msg, now, None);
2800        }
2801
2802        if let Err(error) = self.record_observed_capability_snapshot(cap) {
2803            let msg = error.to_string();
2804            warn!(request_id = %request.request_id, reason = %msg, "failed to persist capability lineage");
2805            return self.build_deny_response(request, &msg, now, None);
2806        }
2807
2808        let (matched_grant_index, charge_result) =
2809            match self.check_and_increment_budget(&request.request_id, cap, &matching_grants) {
2810                Ok(result) => result,
2811                Err(e) => {
2812                    let msg = e.to_string();
2813                    warn!(request_id = %request.request_id, reason = %msg, "capability rejected");
2814                    return self.build_monetary_deny_response_with_metadata(
2815                        request,
2816                        &msg,
2817                        now,
2818                        &matching_grants,
2819                        cap,
2820                        Some(self.budget_backend_receipt_metadata()?),
2821                    );
2822                }
2823            };
2824
2825        let matched_grant = matching_grants
2826            .iter()
2827            .find(|matching| matching.index == matched_grant_index)
2828            .map(|matching| matching.grant)
2829            .ok_or_else(|| {
2830                KernelError::Internal(format!(
2831                    "matched grant index {matched_grant_index} missing from candidate set"
2832                ))
2833            })?;
2834
2835        let validated_governed_admission = match self.validate_governed_transaction(
2836            request,
2837            cap,
2838            matched_grant,
2839            charge_result.as_ref(),
2840            Some(parent_context),
2841            now,
2842        ) {
2843            Ok(validated_governed_admission) => validated_governed_admission,
2844            Err(error) => {
2845                let msg = error.to_string();
2846                warn!(request_id = %request.request_id, reason = %msg, "governed transaction denied");
2847                if let Some(ref charge) = charge_result {
2848                    let reverse = self.reverse_budget_charge(&cap.id, charge)?;
2849                    return self.build_pre_execution_monetary_deny_response_with_metadata(
2850                        request,
2851                        &msg,
2852                        now,
2853                        charge,
2854                        reverse.committed_cost_units_after,
2855                        cap,
2856                        Some(self.budget_execution_receipt_metadata(
2857                            charge,
2858                            Some(("reversed", &reverse)),
2859                        )),
2860                    );
2861                }
2862                return self.build_deny_response(request, &msg, now, Some(matched_grant_index));
2863            }
2864        };
2865        let _governed_runtime_attestation_receipt_scope =
2866            scope_governed_runtime_attestation_receipt_record(
2867                validated_governed_admission
2868                    .as_ref()
2869                    .and_then(|admission| admission.verified_runtime_attestation.clone()),
2870            );
2871        let _governed_call_chain_receipt_evidence_scope =
2872            scope_governed_call_chain_receipt_evidence(
2873                self.governed_call_chain_receipt_evidence(
2874                    request,
2875                    cap,
2876                    Some(parent_context),
2877                    validated_governed_admission
2878                        .as_ref()
2879                        .and_then(|admission| admission.call_chain_proof.clone()),
2880                ),
2881            );
2882
2883        let session_roots =
2884            self.session_enforceable_filesystem_root_paths_owned(&parent_context.session_id)?;
2885
2886        if let Err(e) = self.run_guards(
2887            request,
2888            &cap.scope,
2889            Some(session_roots.as_slice()),
2890            Some(matched_grant_index),
2891        ) {
2892            let msg = e.to_string();
2893            warn!(request_id = %request.request_id, reason = %msg, "guard denied");
2894            if let Some(ref charge) = charge_result {
2895                let reverse = self.reverse_budget_charge(&cap.id, charge)?;
2896                return self.build_pre_execution_monetary_deny_response_with_metadata(
2897                    request,
2898                    &msg,
2899                    now,
2900                    charge,
2901                    reverse.committed_cost_units_after,
2902                    cap,
2903                    Some(
2904                        self.budget_execution_receipt_metadata(
2905                            charge,
2906                            Some(("reversed", &reverse)),
2907                        ),
2908                    ),
2909                );
2910            }
2911            return self.build_deny_response(request, &msg, now, Some(matched_grant_index));
2912        }
2913
2914        let payment_authorization =
2915            match self.authorize_payment_if_needed(request, charge_result.as_ref()) {
2916                Ok(authorization) => authorization,
2917                Err(error) => {
2918                    let msg = format!("payment authorization failed: {error}");
2919                    warn!(request_id = %request.request_id, reason = %msg, "payment denied");
2920                    if let Some(ref charge) = charge_result {
2921                        let reverse = self.reverse_budget_charge(&cap.id, charge)?;
2922                        return self.build_pre_execution_monetary_deny_response_with_metadata(
2923                            request,
2924                            &msg,
2925                            now,
2926                            charge,
2927                            reverse.committed_cost_units_after,
2928                            cap,
2929                            Some(self.budget_execution_receipt_metadata(
2930                                charge,
2931                                Some(("reversed", &reverse)),
2932                            )),
2933                        );
2934                    }
2935                    return self.build_deny_response(request, &msg, now, Some(matched_grant_index));
2936                }
2937            };
2938
2939        let tool_started_at = Instant::now();
2940        let mut child_receipts = Vec::new();
2941        let tool_output_result = {
2942            let server = self.tool_servers.get(&request.server_id).ok_or_else(|| {
2943                KernelError::ToolNotRegistered(format!(
2944                    "server \"{}\" / tool \"{}\"",
2945                    request.server_id, request.tool_name
2946                ))
2947            })?;
2948            let mut sessions = self
2949                .sessions
2950                .write()
2951                .map_err(|_| KernelError::Internal("session state lock poisoned".to_string()))?;
2952            let mut bridge = SessionNestedFlowBridge {
2953                sessions: &mut sessions,
2954                child_receipts: &mut child_receipts,
2955                parent_context,
2956                allow_sampling: self.config.allow_sampling,
2957                allow_sampling_tool_use: self.config.allow_sampling_tool_use,
2958                allow_elicitation: self.config.allow_elicitation,
2959                policy_hash: &self.config.policy_hash,
2960                kernel_keypair: &self.config.keypair,
2961                client,
2962            };
2963
2964            match server.invoke_stream(
2965                &request.tool_name,
2966                request.arguments.clone(),
2967                Some(&mut bridge),
2968            ) {
2969                Ok(Some(stream)) => Ok(ToolServerOutput::Stream(stream)),
2970                Ok(None) => match server.invoke(
2971                    &request.tool_name,
2972                    request.arguments.clone(),
2973                    Some(&mut bridge),
2974                ) {
2975                    Ok(result) => Ok(ToolServerOutput::Value(result)),
2976                    Err(error) => Err(error),
2977                },
2978                Err(error) => Err(error),
2979            }
2980        };
2981        self.record_child_receipts(child_receipts)?;
2982        let tool_output = match tool_output_result {
2983            Ok(output) => output,
2984            Err(error @ KernelError::UrlElicitationsRequired { .. }) => {
2985                let _ = self.unwind_aborted_monetary_invocation(
2986                    request,
2987                    cap,
2988                    charge_result.as_ref(),
2989                    payment_authorization.as_ref(),
2990                )?;
2991                warn!(
2992                    request_id = %request.request_id,
2993                    reason = %error,
2994                    "tool call requires URL elicitation"
2995                );
2996                return Err(error);
2997            }
2998            Err(KernelError::RequestCancelled { request_id, reason }) => {
2999                let unwind = self.unwind_aborted_monetary_invocation(
3000                    request,
3001                    cap,
3002                    charge_result.as_ref(),
3003                    payment_authorization.as_ref(),
3004                )?;
3005                if request_id == parent_context.request_id {
3006                    self.with_session_mut(&parent_context.session_id, |session| {
3007                        session.request_cancellation(&parent_context.request_id)?;
3008                        Ok(())
3009                    })?;
3010                }
3011                warn!(
3012                    request_id = %request.request_id,
3013                    reason = %reason,
3014                    "tool call cancelled"
3015                );
3016                return self.build_cancelled_response_with_metadata(
3017                    request,
3018                    &reason,
3019                    now,
3020                    Some(matched_grant_index),
3021                    match (charge_result.as_ref(), unwind.as_ref()) {
3022                        (Some(charge), Some(reverse)) => {
3023                            Some(self.budget_execution_receipt_metadata(
3024                                charge,
3025                                Some(("reversed", reverse)),
3026                            ))
3027                        }
3028                        _ => None,
3029                    },
3030                );
3031            }
3032            Err(KernelError::RequestIncomplete(reason)) => {
3033                let unwind = self.unwind_aborted_monetary_invocation(
3034                    request,
3035                    cap,
3036                    charge_result.as_ref(),
3037                    payment_authorization.as_ref(),
3038                )?;
3039                warn!(
3040                    request_id = %request.request_id,
3041                    reason = %reason,
3042                    "tool call incomplete"
3043                );
3044                return self.build_incomplete_response_with_output_and_metadata(
3045                    request,
3046                    None,
3047                    &reason,
3048                    now,
3049                    Some(matched_grant_index),
3050                    match (charge_result.as_ref(), unwind.as_ref()) {
3051                        (Some(charge), Some(reverse)) => {
3052                            Some(self.budget_execution_receipt_metadata(
3053                                charge,
3054                                Some(("reversed", reverse)),
3055                            ))
3056                        }
3057                        _ => None,
3058                    },
3059                );
3060            }
3061            Err(error) => {
3062                let unwind = self.unwind_aborted_monetary_invocation(
3063                    request,
3064                    cap,
3065                    charge_result.as_ref(),
3066                    payment_authorization.as_ref(),
3067                )?;
3068                let msg = error.to_string();
3069                warn!(request_id = %request.request_id, reason = %msg, "tool server error");
3070                return self.build_deny_response_with_metadata(
3071                    request,
3072                    &msg,
3073                    now,
3074                    Some(matched_grant_index),
3075                    match (charge_result.as_ref(), unwind.as_ref()) {
3076                        (Some(charge), Some(reverse)) => {
3077                            Some(self.budget_execution_receipt_metadata(
3078                                charge,
3079                                Some(("reversed", reverse)),
3080                            ))
3081                        }
3082                        _ => None,
3083                    },
3084                );
3085            }
3086        };
3087        self.finalize_budgeted_tool_output_with_cost_and_metadata(
3088            request,
3089            tool_output,
3090            tool_started_at.elapsed(),
3091            now,
3092            matched_grant_index,
3093            FinalizeToolOutputCostContext {
3094                charge_result,
3095                reported_cost: None,
3096                payment_authorization,
3097                cap,
3098            },
3099            None,
3100        )
3101    }
3102
3103    /// Issue a new capability for an agent.
3104    ///
3105    /// The kernel delegates issuance to the configured capability authority.
3106    pub fn issue_capability(
3107        &self,
3108        subject: &chio_core::PublicKey,
3109        scope: ChioScope,
3110        ttl_seconds: u64,
3111    ) -> Result<CapabilityToken, KernelError> {
3112        let capability = self
3113            .capability_authority
3114            .issue_capability(subject, scope, ttl_seconds)?;
3115
3116        info!(
3117            capability_id = %capability.id,
3118            subject = %subject.to_hex(),
3119            ttl = ttl_seconds,
3120            issuer = %capability.issuer.to_hex(),
3121            "issuing capability"
3122        );
3123
3124        self.record_observed_capability_snapshot(&capability)?;
3125
3126        Ok(capability)
3127    }
3128
3129    /// Revoke a capability and all descendants in its delegation subtree.
3130    ///
3131    /// When a root capability is revoked, every capability whose
3132    /// `delegation_chain` contains the revoked ID will also be rejected
3133    /// on presentation (the kernel checks all chain entries against the
3134    /// revocation store).
3135    pub fn revoke_capability(&self, capability_id: &CapabilityId) -> Result<(), KernelError> {
3136        info!(capability_id = %capability_id, "revoking capability");
3137        let _ = self.with_revocation_store(|store| Ok(store.revoke(capability_id)?))?;
3138        Ok(())
3139    }
3140
3141    /// Read-only access to the receipt log.
3142    pub fn receipt_log(&self) -> ReceiptLog {
3143        match self.receipt_log.lock() {
3144            Ok(log) => log.clone(),
3145            Err(_) => panic!("receipt log lock poisoned"),
3146        }
3147    }
3148
3149    pub fn child_receipt_log(&self) -> ChildReceiptLog {
3150        match self.child_receipt_log.lock() {
3151            Ok(log) => log.clone(),
3152            Err(_) => panic!("child receipt log lock poisoned"),
3153        }
3154    }
3155
3156    pub fn guard_count(&self) -> usize {
3157        self.guards.len()
3158    }
3159
3160    #[must_use]
3161    pub fn post_invocation_hook_count(&self) -> usize {
3162        self.post_invocation_pipeline.len()
3163    }
3164
3165    pub fn drain_tool_server_events(&self) -> Vec<ToolServerEvent> {
3166        let mut events = Vec::new();
3167        for (server_id, server) in &self.tool_servers {
3168            match server.drain_events() {
3169                Ok(mut server_events) => events.append(&mut server_events),
3170                Err(error) => warn!(
3171                    server_id = %server_id,
3172                    reason = %error,
3173                    "failed to drain tool server events"
3174                ),
3175            }
3176        }
3177        events
3178    }
3179
3180    pub fn register_session_pending_url_elicitation(
3181        &self,
3182        session_id: &SessionId,
3183        elicitation_id: impl Into<String>,
3184        related_task_id: Option<String>,
3185    ) -> Result<(), KernelError> {
3186        self.with_session_mut(session_id, |session| {
3187            session.register_pending_url_elicitation(elicitation_id, related_task_id);
3188            Ok(())
3189        })
3190    }
3191
3192    pub fn register_session_required_url_elicitations(
3193        &self,
3194        session_id: &SessionId,
3195        elicitations: &[CreateElicitationOperation],
3196        related_task_id: Option<&str>,
3197    ) -> Result<(), KernelError> {
3198        self.with_session_mut(session_id, |session| {
3199            session.register_required_url_elicitations(elicitations, related_task_id);
3200            Ok(())
3201        })
3202    }
3203
3204    pub fn queue_session_elicitation_completion(
3205        &self,
3206        session_id: &SessionId,
3207        elicitation_id: &str,
3208    ) -> Result<(), KernelError> {
3209        self.with_session_mut(session_id, |session| {
3210            session.queue_elicitation_completion(elicitation_id);
3211            Ok(())
3212        })
3213    }
3214
3215    pub fn queue_session_late_event(
3216        &self,
3217        session_id: &SessionId,
3218        event: LateSessionEvent,
3219    ) -> Result<(), KernelError> {
3220        self.with_session_mut(session_id, |session| {
3221            session.queue_late_event(event);
3222            Ok(())
3223        })
3224    }
3225
3226    pub fn queue_session_tool_server_event(
3227        &self,
3228        session_id: &SessionId,
3229        event: ToolServerEvent,
3230    ) -> Result<(), KernelError> {
3231        self.with_session_mut(session_id, |session| {
3232            session.queue_tool_server_event(event);
3233            Ok(())
3234        })
3235    }
3236
3237    pub fn queue_session_tool_server_events(
3238        &self,
3239        session_id: &SessionId,
3240    ) -> Result<(), KernelError> {
3241        let events = self.drain_tool_server_events();
3242        self.with_session_mut(session_id, |session| {
3243            for event in events {
3244                session.queue_tool_server_event(event);
3245            }
3246            Ok(())
3247        })
3248    }
3249
3250    pub fn drain_session_late_events(
3251        &self,
3252        session_id: &SessionId,
3253    ) -> Result<Vec<LateSessionEvent>, KernelError> {
3254        self.with_session_mut(session_id, |session| Ok(session.take_late_events()))
3255    }
3256
3257    pub fn ca_count(&self) -> usize {
3258        self.config.ca_public_keys.len()
3259    }
3260
3261    pub fn public_key(&self) -> chio_core::PublicKey {
3262        self.config.keypair.public_key()
3263    }
3264
3265    pub fn capability_issuer_is_trusted(&self, issuer: &chio_core::PublicKey) -> bool {
3266        self.trusted_issuer_keys().contains(issuer)
3267    }
3268
3269    /// Verify the capability's signature against the trusted CA keys or the
3270    /// kernel's own key (for locally-issued capabilities).
3271    /// Resolve the trusted-issuer set for capability verification.
3272    ///
3273    /// This combines the configured CA public keys, the capability
3274    /// authority's trusted keys, and the kernel's own public key. The
3275    /// method is also used by the chio-kernel-core delegation path
3276    /// so the portable TCB verifier sees the same trust set as the
3277    /// legacy inline check.
3278    pub(crate) fn trusted_issuer_keys(&self) -> Vec<chio_core::PublicKey> {
3279        let mut trusted = self.config.ca_public_keys.clone();
3280        for authority_pk in self.capability_authority.trusted_public_keys() {
3281            if !trusted.contains(&authority_pk) {
3282                trusted.push(authority_pk);
3283            }
3284        }
3285        let kernel_pk = self.config.keypair.public_key();
3286        if !trusted.contains(&kernel_pk) {
3287            trusted.push(kernel_pk);
3288        }
3289        trusted
3290    }
3291
3292    fn verify_capability_signature(&self, cap: &CapabilityToken) -> Result<(), String> {
3293        let trusted = self.trusted_issuer_keys();
3294
3295        if !trusted.contains(&cap.issuer) {
3296            return Err("signer public key not found among trusted CAs".to_string());
3297        }
3298
3299        match cap.verify_signature() {
3300            Ok(true) => Ok(()),
3301            Ok(false) => Err("signature did not verify".to_string()),
3302            Err(e) => Err(e.to_string()),
3303        }
3304    }
3305
3306    /// Phase 14.1 -- run the portable pure-compute verdict path provided by
3307    /// `chio-kernel-core`.
3308    ///
3309    /// This exposes the same synchronous checks the core kernel performs
3310    /// (capability signature, issuer trust, time bounds, subject binding,
3311    /// scope match, sync guard pipeline) in isolation from the
3312    /// `chio-kernel`-only concerns (budget mutation, revocation lookup,
3313    /// governed-transaction evaluation, tool dispatch, receipt
3314    /// persistence).
3315    ///
3316    /// Adapters that run the kernel on constrained platforms (wasm32,
3317    /// edge workers, mobile via FFI) should prefer this entry point --
3318    /// it does not require a tokio runtime, a sqlite database, or any
3319    /// IO adapter. The full `evaluate_tool_call_*` API remains the
3320    /// authoritative path for the desktop sidecar.
3321    ///
3322    /// Verified-core boundary note:
3323    /// `formal/proof-manifest.toml` treats this shell method as the one
3324    /// `chio-kernel` entrypoint inside the current bounded verified core,
3325    /// because it delegates directly to `chio_kernel_core::evaluate` after
3326    /// supplying trusted issuers and portable guard/context wiring.
3327    pub fn evaluate_portable_verdict<'a>(
3328        &self,
3329        capability: &'a CapabilityToken,
3330        request: &chio_kernel_core::PortableToolCallRequest,
3331        guards: &'a [&'a dyn chio_kernel_core::Guard],
3332        clock: &'a dyn chio_kernel_core::Clock,
3333        session_filesystem_roots: Option<&'a [String]>,
3334    ) -> chio_kernel_core::EvaluationVerdict {
3335        let trusted = self.trusted_issuer_keys();
3336        chio_kernel_core::evaluate(chio_kernel_core::EvaluateInput {
3337            request,
3338            capability,
3339            trusted_issuers: &trusted,
3340            clock,
3341            guards,
3342            session_filesystem_roots,
3343        })
3344    }
3345
3346    /// Check the revocation store for the capability and its entire
3347    /// delegation chain. If any ancestor is revoked, the capability is
3348    /// rejected.
3349    fn check_revocation(&self, cap: &CapabilityToken) -> Result<(), KernelError> {
3350        if self.with_revocation_store(|store| Ok(store.is_revoked(&cap.id)?))? {
3351            return Err(KernelError::CapabilityRevoked(cap.id.clone()));
3352        }
3353        for link in &cap.delegation_chain {
3354            if self.with_revocation_store(|store| Ok(store.is_revoked(&link.capability_id)?))? {
3355                return Err(KernelError::DelegationChainRevoked(
3356                    link.capability_id.clone(),
3357                ));
3358            }
3359        }
3360        Ok(())
3361    }
3362
3363    fn validate_delegation_admission(&self, cap: &CapabilityToken) -> Result<(), KernelError> {
3364        if cap.delegation_chain.is_empty() {
3365            return Ok(());
3366        }
3367
3368        chio_core::capability::validate_delegation_chain(
3369            &cap.delegation_chain,
3370            Some(self.config.max_delegation_depth),
3371        )
3372        .map_err(|error| KernelError::DelegationInvalid(error.to_string()))?;
3373
3374        let Some(last_link) = cap.delegation_chain.last() else {
3375            return Err(KernelError::DelegationInvalid(
3376                "delegation chain disappeared after validation".to_string(),
3377            ));
3378        };
3379        if last_link.delegatee != cap.subject {
3380            return Err(KernelError::DelegationInvalid(format!(
3381                "leaf capability subject {} does not match final delegation delegatee {}",
3382                cap.subject.to_hex(),
3383                last_link.delegatee.to_hex()
3384            )));
3385        }
3386
3387        let mut ancestor_snapshots = Vec::with_capacity(cap.delegation_chain.len());
3388        for (index, link) in cap.delegation_chain.iter().enumerate() {
3389            let snapshot = self
3390                .with_receipt_store(
3391                    |store| Ok(store.get_capability_snapshot(&link.capability_id)?),
3392                )?
3393                .flatten()
3394                .ok_or_else(|| {
3395                    KernelError::DelegationInvalid(format!(
3396                        "missing capability snapshot for delegation ancestor {} at link index {}",
3397                        link.capability_id, index
3398                    ))
3399                })?;
3400            let expected_depth = index as u64;
3401            if snapshot.delegation_depth != expected_depth {
3402                return Err(KernelError::DelegationInvalid(format!(
3403                    "delegation ancestor {} at link index {} has stored depth {}, expected {}",
3404                    snapshot.capability_id, index, snapshot.delegation_depth, expected_depth
3405                )));
3406            }
3407
3408            let expected_parent_capability_id = index
3409                .checked_sub(1)
3410                .map(|parent_index| cap.delegation_chain[parent_index].capability_id.as_str());
3411            if snapshot.parent_capability_id.as_deref() != expected_parent_capability_id {
3412                let observed_parent = snapshot.parent_capability_id.as_deref().unwrap_or("<root>");
3413                let expected_parent = expected_parent_capability_id.unwrap_or("<root>");
3414                return Err(KernelError::DelegationInvalid(format!(
3415                    "delegation ancestor {} at link index {} is lineage-linked to {}, expected {}",
3416                    snapshot.capability_id, index, observed_parent, expected_parent
3417                )));
3418            }
3419
3420            ancestor_snapshots.push(snapshot);
3421        }
3422
3423        for (index, link) in cap.delegation_chain.iter().enumerate() {
3424            let parent_snapshot = &ancestor_snapshots[index];
3425            let parent_scope = scope_from_capability_snapshot(parent_snapshot)?;
3426
3427            if parent_snapshot.subject_key != link.delegator.to_hex() {
3428                return Err(KernelError::DelegationInvalid(format!(
3429                    "delegation link {} delegator {} does not match parent capability subject {}",
3430                    index,
3431                    link.delegator.to_hex(),
3432                    parent_snapshot.subject_key
3433                )));
3434            }
3435            if link.timestamp < parent_snapshot.issued_at
3436                || link.timestamp >= parent_snapshot.expires_at
3437            {
3438                return Err(KernelError::DelegationInvalid(format!(
3439                    "delegation link {} timestamp {} is outside parent capability {} validity window [{} , {})",
3440                    index,
3441                    link.timestamp,
3442                    parent_snapshot.capability_id,
3443                    parent_snapshot.issued_at,
3444                    parent_snapshot.expires_at
3445                )));
3446            }
3447
3448            let (
3449                child_capability_id,
3450                child_subject_key,
3451                child_scope,
3452                child_issued_at,
3453                child_expires_at,
3454                child_parent_capability_id,
3455            ) = if let Some(next_snapshot) = ancestor_snapshots.get(index + 1) {
3456                (
3457                    next_snapshot.capability_id.clone(),
3458                    next_snapshot.subject_key.clone(),
3459                    scope_from_capability_snapshot(next_snapshot)?,
3460                    next_snapshot.issued_at,
3461                    next_snapshot.expires_at,
3462                    next_snapshot.parent_capability_id.clone(),
3463                )
3464            } else {
3465                (
3466                    cap.id.clone(),
3467                    cap.subject.to_hex(),
3468                    cap.scope.clone(),
3469                    cap.issued_at,
3470                    cap.expires_at,
3471                    Some(link.capability_id.clone()),
3472                )
3473            };
3474
3475            if child_subject_key != link.delegatee.to_hex() {
3476                return Err(KernelError::DelegationInvalid(format!(
3477                    "delegation link {} delegatee {} does not match child capability subject {}",
3478                    index,
3479                    link.delegatee.to_hex(),
3480                    child_subject_key
3481                )));
3482            }
3483            if child_parent_capability_id.as_deref() != Some(link.capability_id.as_str()) {
3484                return Err(KernelError::DelegationInvalid(format!(
3485                    "child capability {} is not lineage-linked to parent capability {}",
3486                    child_capability_id, link.capability_id
3487                )));
3488            }
3489            if child_issued_at < link.timestamp {
3490                return Err(KernelError::DelegationInvalid(format!(
3491                    "child capability {} was issued before delegation link {} timestamp",
3492                    child_capability_id, index
3493                )));
3494            }
3495            if child_issued_at < parent_snapshot.issued_at {
3496                return Err(KernelError::DelegationInvalid(format!(
3497                    "child capability {} predates parent capability {} issuance",
3498                    child_capability_id, parent_snapshot.capability_id
3499                )));
3500            }
3501            if child_expires_at > parent_snapshot.expires_at {
3502                return Err(KernelError::DelegationInvalid(format!(
3503                    "child capability {} expires after parent capability {}",
3504                    child_capability_id, parent_snapshot.capability_id
3505                )));
3506            }
3507
3508            validate_delegation_scope_step(
3509                &parent_snapshot.capability_id,
3510                &child_capability_id,
3511                &parent_scope,
3512                &child_scope,
3513                child_expires_at,
3514                link,
3515            )?;
3516        }
3517
3518        Ok(())
3519    }
3520
3521    fn local_budget_event_authority(&self) -> BudgetEventAuthority {
3522        BudgetEventAuthority {
3523            authority_id: format!("kernel:{}", self.config.keypair.public_key().to_hex()),
3524            lease_id: "single-node".to_string(),
3525            lease_epoch: 0,
3526        }
3527    }
3528
3529    fn budget_backend_receipt_metadata(&self) -> Result<serde_json::Value, KernelError> {
3530        let (guarantee_level, authority_profile, metering_profile) =
3531            self.with_budget_store(|store| {
3532                Ok((
3533                    store.budget_guarantee_level().as_str().to_string(),
3534                    store.budget_authority_profile().as_str().to_string(),
3535                    store.budget_metering_profile().as_str().to_string(),
3536                ))
3537            })?;
3538        Ok(serde_json::json!({
3539            "budget_authority": {
3540                "guarantee_level": guarantee_level,
3541                "authority_profile": authority_profile,
3542                "metering_profile": metering_profile,
3543            }
3544        }))
3545    }
3546
3547    fn budget_execution_receipt_metadata(
3548        &self,
3549        charge: &BudgetChargeResult,
3550        terminal_event: Option<(&str, &BudgetHoldMutationDecision)>,
3551    ) -> serde_json::Value {
3552        let mut budget_authority = serde_json::Map::new();
3553        budget_authority.insert(
3554            "guarantee_level".to_string(),
3555            serde_json::json!(charge.authorize_metadata.guarantee_level.as_str()),
3556        );
3557        budget_authority.insert(
3558            "authority_profile".to_string(),
3559            serde_json::json!(charge.authorize_metadata.budget_profile.as_str()),
3560        );
3561        budget_authority.insert(
3562            "metering_profile".to_string(),
3563            serde_json::json!(charge.authorize_metadata.metering_profile.as_str()),
3564        );
3565        budget_authority.insert(
3566            "hold_id".to_string(),
3567            serde_json::json!(&charge.budget_hold_id),
3568        );
3569        if let Some(budget_term) = charge.authorize_metadata.budget_term() {
3570            budget_authority.insert("budget_term".to_string(), serde_json::json!(budget_term));
3571        }
3572        if let Some(authority) = charge.authorize_metadata.authority.as_ref() {
3573            budget_authority.insert(
3574                "authority".to_string(),
3575                serde_json::json!({
3576                    "authority_id": &authority.authority_id,
3577                    "lease_id": &authority.lease_id,
3578                    "lease_epoch": authority.lease_epoch,
3579                }),
3580            );
3581        }
3582
3583        let mut authorize = serde_json::Map::new();
3584        if let Some(event_id) = charge.authorize_metadata.event_id.as_ref() {
3585            authorize.insert("event_id".to_string(), serde_json::json!(event_id));
3586        }
3587        if let Some(commit_index) = charge.authorize_metadata.budget_commit_index {
3588            authorize.insert(
3589                "budget_commit_index".to_string(),
3590                serde_json::json!(commit_index),
3591            );
3592        }
3593        authorize.insert(
3594            "exposure_units".to_string(),
3595            serde_json::json!(charge.cost_charged),
3596        );
3597        authorize.insert(
3598            "committed_cost_units_after".to_string(),
3599            serde_json::json!(charge.new_committed_cost_units),
3600        );
3601        budget_authority.insert(
3602            "authorize".to_string(),
3603            serde_json::Value::Object(authorize),
3604        );
3605
3606        if let Some((disposition, terminal_event)) = terminal_event {
3607            let mut terminal = serde_json::Map::new();
3608            terminal.insert("disposition".to_string(), serde_json::json!(disposition));
3609            if let Some(event_id) = terminal_event.metadata.event_id.as_ref() {
3610                terminal.insert("event_id".to_string(), serde_json::json!(event_id));
3611            }
3612            if let Some(commit_index) = terminal_event.metadata.budget_commit_index {
3613                terminal.insert(
3614                    "budget_commit_index".to_string(),
3615                    serde_json::json!(commit_index),
3616                );
3617            }
3618            terminal.insert(
3619                "exposure_units".to_string(),
3620                serde_json::json!(terminal_event.exposure_units),
3621            );
3622            terminal.insert(
3623                "realized_spend_units".to_string(),
3624                serde_json::json!(terminal_event.realized_spend_units),
3625            );
3626            terminal.insert(
3627                "committed_cost_units_after".to_string(),
3628                serde_json::json!(terminal_event.committed_cost_units_after),
3629            );
3630            budget_authority.insert("terminal".to_string(), serde_json::Value::Object(terminal));
3631        }
3632
3633        serde_json::json!({ "budget_authority": budget_authority })
3634    }
3635
3636    fn merge_budget_receipt_metadata(
3637        &self,
3638        extra_metadata: Option<serde_json::Value>,
3639        budget_metadata: serde_json::Value,
3640    ) -> Option<serde_json::Value> {
3641        merge_metadata_objects(extra_metadata, Some(budget_metadata))
3642    }
3643
3644    /// Check and decrement the invocation budget for a capability.
3645    ///
3646    /// Returns `(matched_grant_index, Option<BudgetChargeResult>)`.
3647    /// The charge result is populated only for monetary grants.
3648    fn check_and_increment_budget(
3649        &self,
3650        request_id: &str,
3651        cap: &CapabilityToken,
3652        matching_grants: &[MatchingGrant<'_>],
3653    ) -> Result<(usize, Option<BudgetChargeResult>), KernelError> {
3654        let mut saw_exhausted_budget = false;
3655
3656        for matching in matching_grants {
3657            let grant = matching.grant;
3658            let has_monetary =
3659                grant.max_cost_per_invocation.is_some() || grant.max_total_cost.is_some();
3660
3661            if has_monetary {
3662                // Use worst-case max_cost_per_invocation as the pre-execution debit.
3663                let cost_units = grant
3664                    .max_cost_per_invocation
3665                    .as_ref()
3666                    .map(|m| m.units)
3667                    .unwrap_or(0);
3668                let currency = grant
3669                    .max_cost_per_invocation
3670                    .as_ref()
3671                    .map(|m| m.currency.clone())
3672                    .or_else(|| grant.max_total_cost.as_ref().map(|m| m.currency.clone()))
3673                    .unwrap_or_else(|| "USD".to_string());
3674                let max_total = grant.max_total_cost.as_ref().map(|m| m.units);
3675                let max_per = grant.max_cost_per_invocation.as_ref().map(|m| m.units);
3676                let budget_total = max_total.unwrap_or(u64::MAX);
3677                let budget_hold_id =
3678                    format!("budget-hold:{}:{}:{}", request_id, cap.id, matching.index);
3679                let authorize_event_id = format!("{budget_hold_id}:authorize");
3680                let authority = self.local_budget_event_authority();
3681
3682                let decision = self.with_budget_store(|store| {
3683                    Ok(store.authorize_budget_hold(BudgetAuthorizeHoldRequest {
3684                        capability_id: cap.id.clone(),
3685                        grant_index: matching.index,
3686                        max_invocations: grant.max_invocations,
3687                        requested_exposure_units: cost_units,
3688                        max_cost_per_invocation: max_per,
3689                        max_total_cost_units: max_total,
3690                        hold_id: Some(budget_hold_id.clone()),
3691                        event_id: Some(authorize_event_id),
3692                        authority: Some(authority.clone()),
3693                    })?)
3694                })?;
3695                match decision {
3696                    BudgetAuthorizeHoldDecision::Authorized(authorized) => {
3697                        let charge = BudgetChargeResult {
3698                            grant_index: matching.index,
3699                            cost_charged: cost_units,
3700                            currency,
3701                            budget_total,
3702                            new_committed_cost_units: authorized.committed_cost_units_after,
3703                            budget_hold_id: authorized
3704                                .hold_id
3705                                .unwrap_or_else(|| budget_hold_id.clone()),
3706                            authorize_metadata: authorized.metadata,
3707                        };
3708                        return Ok((matching.index, Some(charge)));
3709                    }
3710                    BudgetAuthorizeHoldDecision::Denied(_) => {
3711                        saw_exhausted_budget = true;
3712                    }
3713                }
3714            } else {
3715                // Non-monetary path: use try_increment as before.
3716                if self.with_budget_store(|store| {
3717                    Ok(store.try_increment(&cap.id, matching.index, grant.max_invocations)?)
3718                })? {
3719                    return Ok((matching.index, None));
3720                }
3721                saw_exhausted_budget = saw_exhausted_budget || grant.max_invocations.is_some();
3722            }
3723        }
3724
3725        if saw_exhausted_budget {
3726            Err(KernelError::BudgetExhausted(cap.id.clone()))
3727        } else {
3728            // No matching grant had any limit -- allow with the first grant's index.
3729            let first_index = matching_grants.first().map(|m| m.index).unwrap_or(0);
3730            Ok((first_index, None))
3731        }
3732    }
3733
3734    fn reverse_budget_charge(
3735        &self,
3736        capability_id: &str,
3737        charge: &BudgetChargeResult,
3738    ) -> Result<BudgetReverseHoldDecision, KernelError> {
3739        let authority = charge.authorize_metadata.authority.clone();
3740        self.with_budget_store(|store| {
3741            Ok(store.reverse_budget_hold(BudgetReverseHoldRequest {
3742                capability_id: capability_id.to_string(),
3743                grant_index: charge.grant_index,
3744                reversed_exposure_units: charge.cost_charged,
3745                hold_id: Some(charge.budget_hold_id.clone()),
3746                event_id: Some(charge.reverse_event_id()),
3747                authority,
3748            })?)
3749        })
3750    }
3751
3752    fn reconcile_budget_charge(
3753        &self,
3754        capability_id: &str,
3755        charge: &BudgetChargeResult,
3756        realized_cost_units: u64,
3757    ) -> Result<BudgetReconcileHoldDecision, KernelError> {
3758        let authority = charge.authorize_metadata.authority.clone();
3759        self.with_budget_store(|store| {
3760            Ok(store.reconcile_budget_hold(BudgetReconcileHoldRequest {
3761                capability_id: capability_id.to_string(),
3762                grant_index: charge.grant_index,
3763                exposed_cost_units: charge.cost_charged,
3764                realized_spend_units: realized_cost_units.min(charge.cost_charged),
3765                hold_id: Some(charge.budget_hold_id.clone()),
3766                event_id: Some(charge.reconcile_event_id()),
3767                authority,
3768            })?)
3769        })
3770    }
3771
3772    #[allow(dead_code)]
3773    fn reduce_budget_charge_to_actual(
3774        &self,
3775        capability_id: &str,
3776        charge: &BudgetChargeResult,
3777        actual_cost_units: u64,
3778    ) -> Result<u64, KernelError> {
3779        Ok(self
3780            .reconcile_budget_charge(
3781                capability_id,
3782                charge,
3783                actual_cost_units.min(charge.cost_charged),
3784            )?
3785            .committed_cost_units_after)
3786    }
3787
3788    #[allow(clippy::too_many_arguments)]
3789    fn finalize_budgeted_tool_output_with_cost_and_metadata(
3790        &self,
3791        request: &ToolCallRequest,
3792        output: ToolServerOutput,
3793        elapsed: Duration,
3794        timestamp: u64,
3795        matched_grant_index: usize,
3796        cost_context: FinalizeToolOutputCostContext<'_>,
3797        extra_metadata: Option<serde_json::Value>,
3798    ) -> Result<ToolCallResponse, KernelError> {
3799        let FinalizeToolOutputCostContext {
3800            charge_result,
3801            reported_cost,
3802            payment_authorization,
3803            cap,
3804        } = cost_context;
3805        let Some(charge) = charge_result else {
3806            return self.finalize_tool_output_with_metadata(
3807                request,
3808                output,
3809                elapsed,
3810                timestamp,
3811                matched_grant_index,
3812                extra_metadata,
3813            );
3814        };
3815
3816        let reported_cost_ref = reported_cost.as_ref();
3817        let mut oracle_evidence = None;
3818        let mut cross_currency_note = None;
3819        let (actual_cost, cross_currency_failed) = if let Some(cost) =
3820            reported_cost_ref.filter(|cost| cost.currency != charge.currency)
3821        {
3822            match self.resolve_cross_currency_cost(cost, &charge.currency, timestamp) {
3823                Ok((converted_units, evidence)) => {
3824                    oracle_evidence = Some(evidence);
3825                    cross_currency_note = Some(serde_json::json!({
3826                        "oracle_conversion": {
3827                            "status": "applied",
3828                            "reported_currency": cost.currency,
3829                            "grant_currency": charge.currency,
3830                            "reported_units": cost.units,
3831                            "converted_units": converted_units
3832                        }
3833                    }));
3834                    (converted_units, false)
3835                }
3836                Err(error) => {
3837                    warn!(
3838                        request_id = %request.request_id,
3839                        reported_currency = %cost.currency,
3840                        charged_currency = %charge.currency,
3841                        reason = %error,
3842                        "cross-currency reconciliation failed; closing hold at authorized exposure"
3843                    );
3844                    cross_currency_note = Some(serde_json::json!({
3845                        "oracle_conversion": {
3846                            "status": "failed",
3847                            "reported_currency": cost.currency,
3848                            "grant_currency": charge.currency,
3849                            "reported_units": cost.units,
3850                            "provisional_units": charge.cost_charged,
3851                            "reason": error.to_string()
3852                        }
3853                    }));
3854                    (charge.cost_charged, true)
3855                }
3856            }
3857        } else {
3858            (
3859                reported_cost_ref
3860                    .map(|cost| cost.units)
3861                    .unwrap_or(charge.cost_charged),
3862                false,
3863            )
3864        };
3865
3866        let payment_already_settled = payment_authorization
3867            .as_ref()
3868            .is_some_and(|authorization| authorization.settled);
3869        let cost_overrun =
3870            !cross_currency_failed && actual_cost > charge.cost_charged && charge.cost_charged > 0;
3871
3872        if cost_overrun {
3873            warn!(
3874                request_id = %request.request_id,
3875                reported = actual_cost,
3876                charged = charge.cost_charged,
3877                "tool server reported cost exceeds max_cost_per_invocation; settlement_status=failed"
3878            );
3879        }
3880
3881        let realized_budget_units =
3882            if cross_currency_failed || payment_already_settled || cost_overrun {
3883                charge.cost_charged
3884            } else {
3885                actual_cost.min(charge.cost_charged)
3886            };
3887        let reconcile = self.reconcile_budget_charge(&cap.id, &charge, realized_budget_units)?;
3888        let running_committed_cost_units = reconcile.committed_cost_units_after;
3889
3890        let payment_result = if let Some(authorization) = payment_authorization.as_ref() {
3891            if authorization.settled || cross_currency_failed || cost_overrun {
3892                None
3893            } else {
3894                let adapter = self.payment_adapter.as_ref().ok_or_else(|| {
3895                    KernelError::Internal(
3896                        "payment authorization present without configured adapter".to_string(),
3897                    )
3898                })?;
3899                Some(if actual_cost == 0 {
3900                    adapter.release(&authorization.authorization_id, &request.request_id)
3901                } else {
3902                    adapter.capture(
3903                        &authorization.authorization_id,
3904                        actual_cost,
3905                        &charge.currency,
3906                        &request.request_id,
3907                    )
3908                })
3909            }
3910        } else {
3911            None
3912        };
3913
3914        let settlement = if cross_currency_failed || cost_overrun {
3915            ReceiptSettlement {
3916                payment_reference: payment_authorization
3917                    .as_ref()
3918                    .map(|authorization| authorization.authorization_id.clone()),
3919                settlement_status: SettlementStatus::Failed,
3920            }
3921        } else if let Some(authorization) = payment_authorization.as_ref() {
3922            if authorization.settled {
3923                ReceiptSettlement::from_authorization(authorization)
3924            } else if let Some(payment_result) = payment_result.as_ref() {
3925                match payment_result {
3926                    Ok(result) => ReceiptSettlement::from_payment_result(result),
3927                    Err(error) => {
3928                        warn!(
3929                            request_id = %request.request_id,
3930                            reason = %error,
3931                            "post-execution payment settlement failed"
3932                        );
3933                        ReceiptSettlement {
3934                            payment_reference: Some(authorization.authorization_id.clone()),
3935                            settlement_status: SettlementStatus::Failed,
3936                        }
3937                    }
3938                }
3939            } else {
3940                warn!(
3941                    request_id = %request.request_id,
3942                    authorization_id = %authorization.authorization_id,
3943                    "unsettled authorization completed without a payment result"
3944                );
3945                ReceiptSettlement {
3946                    payment_reference: Some(authorization.authorization_id.clone()),
3947                    settlement_status: SettlementStatus::Failed,
3948                }
3949            }
3950        } else {
3951            ReceiptSettlement::settled()
3952        };
3953        let recorded_cost = if payment_already_settled && !cross_currency_failed && !cost_overrun {
3954            charge.cost_charged
3955        } else {
3956            actual_cost
3957        };
3958
3959        let budget_remaining = charge
3960            .budget_total
3961            .saturating_sub(running_committed_cost_units);
3962        let delegation_depth = cap.delegation_chain.len() as u32;
3963        let root_budget_holder = cap.issuer.to_hex();
3964        let (payment_reference, settlement_status) = settlement.into_receipt_parts();
3965        let payment_breakdown = payment_authorization.as_ref().map(|authorization| {
3966            serde_json::json!({
3967                "payment": {
3968                    "authorization_id": authorization.authorization_id,
3969                    "adapter_metadata": authorization.metadata,
3970                    "preauthorized_units": charge.cost_charged,
3971                    "recorded_units": recorded_cost
3972                }
3973            })
3974        });
3975
3976        let financial_meta = FinancialReceiptMetadata {
3977            grant_index: charge.grant_index as u32,
3978            cost_charged: recorded_cost,
3979            currency: charge.currency.clone(),
3980            budget_remaining,
3981            budget_total: charge.budget_total,
3982            delegation_depth,
3983            root_budget_holder,
3984            payment_reference,
3985            settlement_status,
3986            cost_breakdown: merge_metadata_objects(
3987                merge_metadata_objects(
3988                    reported_cost_ref.and_then(|cost| cost.breakdown.clone()),
3989                    payment_breakdown,
3990                ),
3991                cross_currency_note,
3992            ),
3993            oracle_evidence,
3994            attempted_cost: None,
3995        };
3996
3997        let limited_output = self.apply_stream_limits(output, elapsed)?;
3998        let tool_call_output = match &limited_output {
3999            ToolServerOutput::Value(value) => ToolCallOutput::Value(value.clone()),
4000            ToolServerOutput::Stream(ToolServerStreamResult::Complete(stream)) => {
4001                ToolCallOutput::Stream(stream.clone())
4002            }
4003            ToolServerOutput::Stream(ToolServerStreamResult::Incomplete { stream, .. }) => {
4004                ToolCallOutput::Stream(stream.clone())
4005            }
4006        };
4007
4008        let budget_metadata =
4009            self.budget_execution_receipt_metadata(&charge, Some(("reconciled", &reconcile)));
4010        let merged_extra_metadata =
4011            self.merge_budget_receipt_metadata(extra_metadata, budget_metadata);
4012        let financial_json = Some(serde_json::json!({ "financial": financial_meta }));
4013        let merged_extra_metadata = merge_metadata_objects(financial_json, merged_extra_metadata);
4014
4015        match limited_output {
4016            ToolServerOutput::Value(_)
4017            | ToolServerOutput::Stream(ToolServerStreamResult::Complete(_)) => self
4018                .build_allow_response_with_metadata(
4019                    request,
4020                    tool_call_output,
4021                    timestamp,
4022                    Some(charge.grant_index),
4023                    merged_extra_metadata.clone(),
4024                ),
4025            ToolServerOutput::Stream(ToolServerStreamResult::Incomplete { reason, .. }) => self
4026                .build_incomplete_response_with_output_and_metadata(
4027                    request,
4028                    Some(tool_call_output),
4029                    &reason,
4030                    timestamp,
4031                    Some(charge.grant_index),
4032                    merged_extra_metadata,
4033                ),
4034        }
4035    }
4036
4037    fn block_on_price_oracle<T>(
4038        &self,
4039        future: impl Future<Output = Result<T, PriceOracleError>>,
4040    ) -> Result<T, KernelError> {
4041        match tokio::runtime::Handle::try_current() {
4042            Ok(handle) => match handle.runtime_flavor() {
4043                tokio::runtime::RuntimeFlavor::MultiThread => tokio::task::block_in_place(|| {
4044                    handle
4045                        .block_on(future)
4046                        .map_err(|error| KernelError::CrossCurrencyOracle(error.to_string()))
4047                }),
4048                tokio::runtime::RuntimeFlavor::CurrentThread => {
4049                    Err(KernelError::CrossCurrencyOracle(
4050                        "current-thread tokio runtime cannot synchronously resolve price oracles"
4051                            .to_string(),
4052                    ))
4053                }
4054                flavor => Err(KernelError::CrossCurrencyOracle(format!(
4055                    "unsupported tokio runtime flavor for synchronous oracle resolution: {flavor:?}"
4056                ))),
4057            },
4058            Err(_) => tokio::runtime::Builder::new_current_thread()
4059                .enable_all()
4060                .build()
4061                .map_err(|error| {
4062                    KernelError::CrossCurrencyOracle(format!(
4063                        "failed to build synchronous oracle runtime: {error}"
4064                    ))
4065                })?
4066                .block_on(future)
4067                .map_err(|error| KernelError::CrossCurrencyOracle(error.to_string())),
4068        }
4069    }
4070
4071    fn resolve_cross_currency_cost(
4072        &self,
4073        reported_cost: &ToolInvocationCost,
4074        grant_currency: &str,
4075        timestamp: u64,
4076    ) -> Result<(u64, chio_core::web3::OracleConversionEvidence), KernelError> {
4077        let oracle =
4078            self.price_oracle
4079                .as_ref()
4080                .ok_or_else(|| KernelError::NoCrossCurrencyOracle {
4081                    base: reported_cost.currency.clone(),
4082                    quote: grant_currency.to_string(),
4083                })?;
4084        let rate =
4085            self.block_on_price_oracle(oracle.get_rate(&reported_cost.currency, grant_currency))?;
4086        let converted_units =
4087            convert_supported_units(reported_cost.units, &rate, rate.conversion_margin_bps)
4088                .map_err(|error| KernelError::CrossCurrencyOracle(error.to_string()))?;
4089        let evidence = rate
4090            .to_conversion_evidence(
4091                reported_cost.units,
4092                reported_cost.currency.clone(),
4093                grant_currency.to_string(),
4094                converted_units,
4095                timestamp,
4096            )
4097            .map_err(|error| KernelError::CrossCurrencyOracle(error.to_string()))?;
4098        Ok((converted_units, evidence))
4099    }
4100
4101    fn ensure_registered_tool_target(&self, request: &ToolCallRequest) -> Result<(), KernelError> {
4102        self.tool_servers.get(&request.server_id).ok_or_else(|| {
4103            KernelError::ToolNotRegistered(format!(
4104                "server \"{}\" / tool \"{}\"",
4105                request.server_id, request.tool_name
4106            ))
4107        })?;
4108        Ok(())
4109    }
4110
4111    fn authorize_payment_if_needed(
4112        &self,
4113        request: &ToolCallRequest,
4114        charge_result: Option<&BudgetChargeResult>,
4115    ) -> Result<Option<PaymentAuthorization>, PaymentError> {
4116        let Some(charge) = charge_result else {
4117            return Ok(None);
4118        };
4119        let Some(adapter) = self.payment_adapter.as_ref() else {
4120            return Ok(None);
4121        };
4122
4123        let governed = request
4124            .governed_intent
4125            .as_ref()
4126            .map(|intent| {
4127                intent
4128                    .binding_hash()
4129                    .map(|intent_hash| GovernedPaymentContext {
4130                        intent_id: intent.id.clone(),
4131                        intent_hash,
4132                        purpose: intent.purpose.clone(),
4133                        server_id: intent.server_id.clone(),
4134                        tool_name: intent.tool_name.clone(),
4135                        approval_token_id: request
4136                            .approval_token
4137                            .as_ref()
4138                            .map(|token| token.id.clone()),
4139                    })
4140                    .map_err(|error| {
4141                        PaymentError::RailError(format!(
4142                            "failed to hash governed intent for payment authorization: {error}"
4143                        ))
4144                    })
4145            })
4146            .transpose()?;
4147        let commerce = request.governed_intent.as_ref().and_then(|intent| {
4148            intent
4149                .commerce
4150                .as_ref()
4151                .map(|commerce| CommercePaymentContext {
4152                    seller: commerce.seller.clone(),
4153                    shared_payment_token_id: commerce.shared_payment_token_id.clone(),
4154                    max_amount: intent.max_amount.clone(),
4155                })
4156        });
4157
4158        adapter
4159            .authorize(&PaymentAuthorizeRequest {
4160                amount_units: charge.cost_charged,
4161                currency: charge.currency.clone(),
4162                payer: request.agent_id.clone(),
4163                payee: request.server_id.clone(),
4164                reference: request.request_id.clone(),
4165                governed,
4166                commerce,
4167            })
4168            .map(Some)
4169    }
4170
4171    fn governed_requirements(
4172        grant: &ToolGrant,
4173    ) -> (
4174        bool,
4175        Option<u64>,
4176        Option<String>,
4177        Option<RuntimeAssuranceTier>,
4178        Option<GovernedAutonomyTier>,
4179    ) {
4180        let mut intent_required = false;
4181        let mut approval_threshold_units = None;
4182        let mut seller = None;
4183        let mut minimum_runtime_assurance = None;
4184        let mut minimum_autonomy_tier = None;
4185
4186        for constraint in &grant.constraints {
4187            match constraint {
4188                Constraint::GovernedIntentRequired => {
4189                    intent_required = true;
4190                }
4191                Constraint::RequireApprovalAbove { threshold_units } => {
4192                    approval_threshold_units = Some(
4193                        approval_threshold_units.map_or(*threshold_units, |current: u64| {
4194                            current.max(*threshold_units)
4195                        }),
4196                    );
4197                }
4198                Constraint::SellerExact(expected_seller) => {
4199                    seller = Some(expected_seller.clone());
4200                }
4201                Constraint::MinimumRuntimeAssurance(required_tier) => {
4202                    minimum_runtime_assurance = Some(
4203                        minimum_runtime_assurance
4204                            .map_or(*required_tier, |current: RuntimeAssuranceTier| {
4205                                current.max(*required_tier)
4206                            }),
4207                    );
4208                }
4209                Constraint::MinimumAutonomyTier(required_tier) => {
4210                    minimum_autonomy_tier = Some(
4211                        minimum_autonomy_tier
4212                            .map_or(*required_tier, |current: GovernedAutonomyTier| {
4213                                current.max(*required_tier)
4214                            }),
4215                    );
4216                }
4217                // Phase 2.2 data-layer, communication, financial,
4218                // model-routing, and memory-governance constraints do
4219                // not contribute to governed-transaction requirements.
4220                // Their enforcement is wired into request_matching.rs
4221                // (argument-level checks) and downstream data/content
4222                // guards (SQL parsing, result shaping, HITL replay).
4223                Constraint::TableAllowlist(_)
4224                | Constraint::ColumnDenylist(_)
4225                | Constraint::MaxRowsReturned(_)
4226                | Constraint::OperationClass(_)
4227                | Constraint::AudienceAllowlist(_)
4228                | Constraint::MaxArgsSize(_)
4229                | Constraint::ContentReviewTier(_)
4230                | Constraint::MaxTransactionAmountUsd(_)
4231                | Constraint::RequireDualApproval(_)
4232                | Constraint::ModelConstraint { .. }
4233                | Constraint::MemoryStoreAllowlist(_)
4234                | Constraint::MemoryWriteDenyPatterns(_) => {}
4235                _ => {}
4236            }
4237        }
4238
4239        (
4240            intent_required,
4241            approval_threshold_units,
4242            seller,
4243            minimum_runtime_assurance,
4244            minimum_autonomy_tier,
4245        )
4246    }
4247
4248    fn verify_governed_approval_signature(
4249        &self,
4250        approval_token: &GovernedApprovalToken,
4251    ) -> Result<(), String> {
4252        // Multi-algorithm dispatch happens inside `approval_token.verify_signature()`:
4253        // the `approver` public key and the `signature` field are each algorithm-
4254        // tagged (Ed25519 by default; `p256:` / `p384:` under the FIPS crypto
4255        // path), so ECDSA approvals are validated through aws-lc-rs when the
4256        // `chio-core-types/fips` feature is enabled without any kernel-side
4257        // algorithm plumbing.
4258        let kernel_pk = self.config.keypair.public_key();
4259        let mut trusted = self.config.ca_public_keys.clone();
4260        for authority_pk in self.capability_authority.trusted_public_keys() {
4261            if !trusted.contains(&authority_pk) {
4262                trusted.push(authority_pk);
4263            }
4264        }
4265        if !trusted.contains(&kernel_pk) {
4266            trusted.push(kernel_pk);
4267        }
4268
4269        for pk in &trusted {
4270            if *pk == approval_token.approver {
4271                return match approval_token.verify_signature() {
4272                    Ok(true) => Ok(()),
4273                    Ok(false) => Err("signature did not verify".to_string()),
4274                    Err(error) => Err(error.to_string()),
4275                };
4276            }
4277        }
4278
4279        Err("approval signer public key not found among trusted authorities".to_string())
4280    }
4281
4282    fn verify_governed_runtime_attestation(
4283        &self,
4284        attestation: &chio_core::capability::RuntimeAttestationEvidence,
4285        now: u64,
4286    ) -> Result<VerifiedRuntimeAttestationRecord, KernelError> {
4287        verify_governed_runtime_attestation_record(
4288            attestation,
4289            self.attestation_trust_policy.as_ref(),
4290            now,
4291        )
4292    }
4293
4294    fn verify_governed_request_runtime_attestation(
4295        &self,
4296        request: &ToolCallRequest,
4297        now: u64,
4298    ) -> Result<Option<VerifiedRuntimeAttestationRecord>, KernelError> {
4299        request
4300            .governed_intent
4301            .as_ref()
4302            .and_then(|intent| intent.runtime_attestation.as_ref())
4303            .map(|attestation| self.verify_governed_runtime_attestation(attestation, now))
4304            .transpose()
4305    }
4306
4307    fn validate_runtime_assurance(
4308        verified_runtime_attestation: Option<&VerifiedRuntimeAttestationRecord>,
4309        required_tier: RuntimeAssuranceTier,
4310        requirement_source: &str,
4311    ) -> Result<(), KernelError> {
4312        let Some(verified_runtime_attestation) = verified_runtime_attestation else {
4313            return Err(KernelError::GovernedTransactionDenied(format!(
4314                "runtime attestation tier '{required_tier:?}' required by {requirement_source}"
4315            )));
4316        };
4317
4318        if !verified_runtime_attestation.is_locally_accepted() {
4319            let reason = verified_runtime_attestation
4320                .policy_outcome
4321                .reason
4322                .as_deref()
4323                .unwrap_or(
4324                    "runtime attestation evidence did not cross a local verified trust boundary",
4325                );
4326            return Err(KernelError::GovernedTransactionDenied(format!(
4327                "runtime attestation tier '{required_tier:?}' required by {requirement_source}; {reason}"
4328            )));
4329        }
4330
4331        let effective_tier = verified_runtime_attestation.effective_tier();
4332        if effective_tier < required_tier {
4333            return Err(KernelError::GovernedTransactionDenied(format!(
4334                "runtime attestation tier '{effective_tier:?}' is below required '{required_tier:?}' for {requirement_source}"
4335            )));
4336        }
4337
4338        Ok(())
4339    }
4340
4341    fn validate_governed_approval_token(
4342        &self,
4343        request: &ToolCallRequest,
4344        cap: &CapabilityToken,
4345        intent_hash: &str,
4346        approval_token: &GovernedApprovalToken,
4347        now: u64,
4348    ) -> Result<(), KernelError> {
4349        approval_token
4350            .validate_time(now)
4351            .map_err(|error| KernelError::GovernedTransactionDenied(error.to_string()))?;
4352
4353        if approval_token.request_id != request.request_id {
4354            return Err(KernelError::GovernedTransactionDenied(
4355                "approval token request binding does not match the tool call".to_string(),
4356            ));
4357        }
4358
4359        if approval_token.governed_intent_hash != intent_hash {
4360            return Err(KernelError::GovernedTransactionDenied(
4361                "approval token intent binding does not match the governed intent".to_string(),
4362            ));
4363        }
4364
4365        if approval_token.subject != cap.subject {
4366            return Err(KernelError::GovernedTransactionDenied(
4367                "approval token subject does not match the capability subject".to_string(),
4368            ));
4369        }
4370
4371        if approval_token.decision != GovernedApprovalDecision::Approved {
4372            return Err(KernelError::GovernedTransactionDenied(
4373                "approval token does not approve the governed transaction".to_string(),
4374            ));
4375        }
4376
4377        self.verify_governed_approval_signature(approval_token)
4378            .map_err(|reason| {
4379                KernelError::GovernedTransactionDenied(format!(
4380                    "approval token verification failed: {reason}"
4381                ))
4382            })?;
4383
4384        // Step 7: Cap approval token lifetime. Tokens with expires_at more
4385        // than MAX_APPROVAL_TTL_SECS beyond issued_at are rejected to prevent
4386        // long-lived tokens from outliving the replay store's eviction window.
4387        const MAX_APPROVAL_TTL_SECS: u64 = 3600; // 1 hour max
4388        let token_lifetime = approval_token
4389            .expires_at
4390            .saturating_sub(approval_token.issued_at);
4391        if token_lifetime > MAX_APPROVAL_TTL_SECS {
4392            return Err(KernelError::GovernedTransactionDenied(format!(
4393                "approval token lifetime ({token_lifetime}s) exceeds maximum ({MAX_APPROVAL_TTL_SECS}s)"
4394            )));
4395        }
4396
4397        // Step 8: Single-use replay check. An approval token must not be
4398        // consumed more than once. The replay store TTL is set to
4399        // MAX_APPROVAL_TTL_SECS, which is >= any valid token's lifetime
4400        // (enforced by step 7). This guarantees a token can never be replayed
4401        // after cache eviction because the token itself will have expired
4402        // before eviction occurs.
4403        if let Some(ref replay_store) = self.approval_replay_store {
4404            let is_fresh = replay_store
4405                .check_and_insert(&approval_token.request_id, intent_hash)
4406                .map_err(|_| {
4407                    KernelError::GovernedTransactionDenied(
4408                        "approval replay store unavailable; denying as fail-closed".to_string(),
4409                    )
4410                })?;
4411            if !is_fresh {
4412                return Err(KernelError::GovernedTransactionDenied(
4413                    "approval token has already been consumed (replay detected)".to_string(),
4414                ));
4415            }
4416        }
4417
4418        Ok(())
4419    }
4420
4421    fn validate_metered_billing_context(
4422        intent: &chio_core::capability::GovernedTransactionIntent,
4423        charge_result: Option<&BudgetChargeResult>,
4424        now: u64,
4425    ) -> Result<(), KernelError> {
4426        let Some(metered) = intent.metered_billing.as_ref() else {
4427            return Ok(());
4428        };
4429
4430        let quote = &metered.quote;
4431        if quote.quote_id.trim().is_empty() {
4432            return Err(KernelError::GovernedTransactionDenied(
4433                "metered billing quote_id must not be empty".to_string(),
4434            ));
4435        }
4436        if quote.provider.trim().is_empty() {
4437            return Err(KernelError::GovernedTransactionDenied(
4438                "metered billing provider must not be empty".to_string(),
4439            ));
4440        }
4441        if quote.billing_unit.trim().is_empty() {
4442            return Err(KernelError::GovernedTransactionDenied(
4443                "metered billing unit must not be empty".to_string(),
4444            ));
4445        }
4446        if quote.quoted_units == 0 {
4447            return Err(KernelError::GovernedTransactionDenied(
4448                "metered billing quoted_units must be greater than zero".to_string(),
4449            ));
4450        }
4451        if quote
4452            .expires_at
4453            .is_some_and(|expires_at| expires_at <= quote.issued_at)
4454        {
4455            return Err(KernelError::GovernedTransactionDenied(
4456                "metered billing quote expires_at must be after issued_at".to_string(),
4457            ));
4458        }
4459        if quote.expires_at.is_some() && !quote.is_valid_at(now) {
4460            return Err(KernelError::GovernedTransactionDenied(
4461                "metered billing quote is missing or expired".to_string(),
4462            ));
4463        }
4464        if metered.max_billed_units == Some(0) {
4465            return Err(KernelError::GovernedTransactionDenied(
4466                "metered billing max_billed_units must be greater than zero when present"
4467                    .to_string(),
4468            ));
4469        }
4470        if metered
4471            .max_billed_units
4472            .is_some_and(|max_billed_units| max_billed_units < quote.quoted_units)
4473        {
4474            return Err(KernelError::GovernedTransactionDenied(
4475                "metered billing max_billed_units cannot be lower than quote.quoted_units"
4476                    .to_string(),
4477            ));
4478        }
4479        if let Some(intent_amount) = intent.max_amount.as_ref() {
4480            if intent_amount.currency != quote.quoted_cost.currency {
4481                return Err(KernelError::GovernedTransactionDenied(
4482                    "metered billing quote currency does not match governed intent currency"
4483                        .to_string(),
4484                ));
4485            }
4486        }
4487        if let Some(charge) = charge_result {
4488            if charge.currency != quote.quoted_cost.currency {
4489                return Err(KernelError::GovernedTransactionDenied(
4490                    "metered billing quote currency does not match the grant currency".to_string(),
4491                ));
4492            }
4493        }
4494
4495        Ok(())
4496    }
4497
4498    fn validate_governed_call_chain_context(
4499        &self,
4500        request: &ToolCallRequest,
4501        cap: &CapabilityToken,
4502        intent: &chio_core::capability::GovernedTransactionIntent,
4503        parent_context: Option<&OperationContext>,
4504        now: u64,
4505    ) -> Result<Option<ValidatedGovernedCallChainProof>, KernelError> {
4506        let Some(call_chain) = intent.call_chain.as_ref() else {
4507            return Ok(None);
4508        };
4509
4510        if call_chain.chain_id.trim().is_empty() {
4511            return Err(KernelError::GovernedTransactionDenied(
4512                "governed call_chain.chain_id must not be empty".to_string(),
4513            ));
4514        }
4515        if call_chain.parent_request_id.trim().is_empty() {
4516            return Err(KernelError::GovernedTransactionDenied(
4517                "governed call_chain.parent_request_id must not be empty".to_string(),
4518            ));
4519        }
4520        if call_chain.parent_request_id == request.request_id {
4521            return Err(KernelError::GovernedTransactionDenied(
4522                "governed call_chain.parent_request_id must not equal the current request_id"
4523                    .to_string(),
4524            ));
4525        }
4526        if let Some(parent_context) = parent_context {
4527            let local_parent_request_id = parent_context.request_id.to_string();
4528            if call_chain.parent_request_id != local_parent_request_id {
4529                return Err(KernelError::GovernedTransactionDenied(
4530                    "governed call_chain.parent_request_id does not match the locally authenticated parent request".to_string(),
4531                ));
4532            }
4533            self.validate_parent_request_continuation(request, parent_context)?;
4534        }
4535        if call_chain.origin_subject.trim().is_empty() {
4536            return Err(KernelError::GovernedTransactionDenied(
4537                "governed call_chain.origin_subject must not be empty".to_string(),
4538            ));
4539        }
4540        if call_chain.delegator_subject.trim().is_empty() {
4541            return Err(KernelError::GovernedTransactionDenied(
4542                "governed call_chain.delegator_subject must not be empty".to_string(),
4543            ));
4544        }
4545        if call_chain
4546            .parent_receipt_id
4547            .as_deref()
4548            .is_some_and(|value| value.trim().is_empty())
4549        {
4550            return Err(KernelError::GovernedTransactionDenied(
4551                "governed call_chain.parent_receipt_id must not be empty when present".to_string(),
4552            ));
4553        }
4554        if let Some(capability_delegator_subject) = cap
4555            .delegation_chain
4556            .last()
4557            .map(|link| link.delegator.to_hex())
4558        {
4559            if call_chain.delegator_subject != capability_delegator_subject {
4560                return Err(KernelError::GovernedTransactionDenied(
4561                    "governed call_chain.delegator_subject does not match the validated capability delegation source".to_string(),
4562                ));
4563            }
4564        }
4565        if let Some(capability_origin_subject) = cap
4566            .delegation_chain
4567            .first()
4568            .map(|link| link.delegator.to_hex())
4569        {
4570            if call_chain.origin_subject != capability_origin_subject {
4571                return Err(KernelError::GovernedTransactionDenied(
4572                    "governed call_chain.origin_subject does not match the validated capability lineage origin".to_string(),
4573                ));
4574            }
4575        }
4576
4577        self.validate_governed_call_chain_upstream_proof(
4578            request,
4579            cap,
4580            intent,
4581            call_chain,
4582            parent_context,
4583            now,
4584        )
4585    }
4586
4587    fn validate_governed_call_chain_upstream_proof(
4588        &self,
4589        request: &ToolCallRequest,
4590        cap: &CapabilityToken,
4591        intent: &chio_core::capability::GovernedTransactionIntent,
4592        call_chain: &chio_core::capability::GovernedCallChainContext,
4593        parent_context: Option<&OperationContext>,
4594        now: u64,
4595    ) -> Result<Option<ValidatedGovernedCallChainProof>, KernelError> {
4596        if let Some(continuation_token) = intent.explicit_continuation_token().map_err(|error| {
4597            KernelError::GovernedTransactionDenied(format!(
4598                "governed call_chain continuation token is malformed: {error}"
4599            ))
4600        })? {
4601            let signature_valid = continuation_token.verify_signature().map_err(|error| {
4602                KernelError::GovernedTransactionDenied(format!(
4603                    "governed call_chain continuation token failed signature verification: {error}"
4604                ))
4605            })?;
4606            if !signature_valid {
4607                return Err(KernelError::GovernedTransactionDenied(
4608                    "governed call_chain continuation token failed signature verification"
4609                        .to_string(),
4610                ));
4611            }
4612            continuation_token.validate_time(now).map_err(|error| {
4613                KernelError::GovernedTransactionDenied(format!(
4614                    "governed call_chain continuation token rejected by time bounds: {error}"
4615                ))
4616            })?;
4617            if continuation_token.subject != cap.subject {
4618                return Err(KernelError::GovernedTransactionDenied(
4619                    "governed call_chain continuation token subject does not match the capability subject"
4620                        .to_string(),
4621                ));
4622            }
4623            if continuation_token.current_subject != cap.subject.to_hex() {
4624                return Err(KernelError::GovernedTransactionDenied(
4625                    "governed call_chain continuation token current_subject does not match the capability subject"
4626                        .to_string(),
4627                ));
4628            }
4629
4630            let signer_matches_capability_lineage = cap
4631                .delegation_chain
4632                .last()
4633                .is_some_and(|link| link.delegator == continuation_token.signer);
4634            if !self.is_trusted_governed_continuation_signer(&continuation_token.signer)
4635                && !signer_matches_capability_lineage
4636            {
4637                return Err(KernelError::GovernedTransactionDenied(
4638                    "governed call_chain continuation token signer is not trusted".to_string(),
4639                ));
4640            }
4641            if continuation_token.chain_id != call_chain.chain_id {
4642                return Err(KernelError::GovernedTransactionDenied(
4643                    "governed call_chain continuation token chain_id does not match the asserted call_chain".to_string(),
4644                ));
4645            }
4646            if continuation_token.parent_request_id != call_chain.parent_request_id {
4647                return Err(KernelError::GovernedTransactionDenied(
4648                    "governed call_chain continuation token parent_request_id does not match the asserted call_chain".to_string(),
4649                ));
4650            }
4651            if continuation_token.parent_receipt_id != call_chain.parent_receipt_id {
4652                return Err(KernelError::GovernedTransactionDenied(
4653                    "governed call_chain continuation token parent_receipt_id does not match the asserted call_chain".to_string(),
4654                ));
4655            }
4656            if continuation_token.origin_subject != call_chain.origin_subject {
4657                return Err(KernelError::GovernedTransactionDenied(
4658                    "governed call_chain continuation token origin_subject does not match the asserted call_chain".to_string(),
4659                ));
4660            }
4661            if continuation_token.delegator_subject != call_chain.delegator_subject {
4662                return Err(KernelError::GovernedTransactionDenied(
4663                    "governed call_chain continuation token delegator_subject does not match the asserted call_chain".to_string(),
4664                ));
4665            }
4666            if continuation_token.audience.is_some()
4667                && !continuation_token.matches_target(&request.server_id, &request.tool_name)
4668            {
4669                return Err(KernelError::GovernedTransactionDenied(
4670                    "governed call_chain continuation token target does not match the tool call"
4671                        .to_string(),
4672                ));
4673            }
4674            if let Some(expected_intent_hash) = continuation_token.governed_intent_hash.as_deref() {
4675                let intent_hash = intent.binding_hash().map_err(|error| {
4676                    KernelError::GovernedTransactionDenied(format!(
4677                        "failed to hash governed transaction intent for continuation validation: {error}"
4678                    ))
4679                })?;
4680                if expected_intent_hash != intent_hash {
4681                    return Err(KernelError::GovernedTransactionDenied(
4682                        "governed call_chain continuation token intent_hash does not match the governed intent".to_string(),
4683                    ));
4684                }
4685            }
4686            if let Some(parent_capability_id) = continuation_token.parent_capability_id.as_deref() {
4687                let Some(expected_parent_capability_id) = cap
4688                    .delegation_chain
4689                    .last()
4690                    .map(|link| link.capability_id.as_str())
4691                else {
4692                    return Err(KernelError::GovernedTransactionDenied(
4693                        "governed call_chain continuation token parent_capability_id requires a delegated capability lineage".to_string(),
4694                    ));
4695                };
4696                if parent_capability_id != expected_parent_capability_id {
4697                    return Err(KernelError::GovernedTransactionDenied(
4698                        "governed call_chain continuation token parent_capability_id does not match the capability lineage".to_string(),
4699                    ));
4700                }
4701            }
4702            if let Some(expected_link_hash) = continuation_token.delegation_link_hash.as_deref() {
4703                let Some(last_link) = cap.delegation_chain.last() else {
4704                    return Err(KernelError::GovernedTransactionDenied(
4705                        "governed call_chain continuation token delegation_link_hash requires a delegated capability lineage".to_string(),
4706                    ));
4707                };
4708                let actual_link_hash =
4709                    canonical_json_bytes(&last_link.body()).map_err(|error| {
4710                        KernelError::GovernedTransactionDenied(format!(
4711                            "failed to hash capability delegation lineage for continuation validation: {error}"
4712                        ))
4713                    })?;
4714                if sha256_hex(&actual_link_hash) != expected_link_hash {
4715                    return Err(KernelError::GovernedTransactionDenied(
4716                        "governed call_chain continuation token delegation_link_hash does not match the capability lineage".to_string(),
4717                    ));
4718                }
4719            }
4720
4721            let local_parent_receipt = if let Some(parent_receipt_id) =
4722                continuation_token.parent_receipt_id.as_deref()
4723            {
4724                match self.local_receipt_artifact(parent_receipt_id) {
4725                    Some(parent_receipt) => {
4726                        let signature_valid = parent_receipt.verify_signature()?;
4727                        if !signature_valid {
4728                            return Err(KernelError::GovernedTransactionDenied(
4729                                "governed call_chain parent receipt failed signature verification"
4730                                    .to_string(),
4731                            ));
4732                        }
4733                        Some(parent_receipt)
4734                    }
4735                    None => {
4736                        if continuation_token.parent_receipt_hash.is_some()
4737                            || continuation_token.parent_session_anchor.is_some()
4738                        {
4739                            return Err(KernelError::GovernedTransactionDenied(
4740                                "governed call_chain continuation token parent_receipt_id does not resolve to a locally persisted receipt".to_string(),
4741                            ));
4742                        }
4743                        None
4744                    }
4745                }
4746            } else {
4747                if continuation_token.parent_receipt_hash.is_some() {
4748                    return Err(KernelError::GovernedTransactionDenied(
4749                        "governed call_chain continuation token parent_receipt_hash requires parent_receipt_id".to_string(),
4750                    ));
4751                }
4752                None
4753            };
4754
4755            if let Some(expected_parent_receipt_hash) =
4756                continuation_token.parent_receipt_hash.as_deref()
4757            {
4758                let Some(parent_receipt) = local_parent_receipt.as_ref() else {
4759                    return Err(KernelError::GovernedTransactionDenied(
4760                        "governed call_chain continuation token parent_receipt_hash requires a locally persisted parent receipt".to_string(),
4761                    ));
4762                };
4763                if parent_receipt.artifact_hash()? != expected_parent_receipt_hash {
4764                    return Err(KernelError::GovernedTransactionDenied(
4765                        "governed call_chain continuation token parent_receipt_hash does not match the authoritative parent receipt".to_string(),
4766                    ));
4767                }
4768            }
4769
4770            let validated_session_anchor_id = if let Some(parent_session_anchor) =
4771                continuation_token.parent_session_anchor.as_ref()
4772            {
4773                let authoritative_parent_anchor = if let Some(parent_context) = parent_context {
4774                    Some(self.with_session(&parent_context.session_id, |session| {
4775                        session.validate_context(parent_context)?;
4776                        Ok(session.session_anchor().reference())
4777                    })?)
4778                } else {
4779                    local_parent_receipt
4780                        .as_ref()
4781                        .and_then(LocalReceiptArtifact::session_anchor_reference)
4782                };
4783                let Some(authoritative_parent_anchor) = authoritative_parent_anchor else {
4784                    return Err(KernelError::GovernedTransactionDenied(
4785                        "governed call_chain continuation token parent_session_anchor could not be verified against authoritative parent lineage".to_string(),
4786                    ));
4787                };
4788                if authoritative_parent_anchor != *parent_session_anchor {
4789                    return Err(KernelError::GovernedTransactionDenied(
4790                        "governed call_chain continuation token session anchor does not match the authoritative parent lineage".to_string(),
4791                    ));
4792                }
4793                Some(parent_session_anchor.session_anchor_id.clone())
4794            } else {
4795                None
4796            };
4797
4798            return Ok(Some(ValidatedGovernedCallChainProof {
4799                upstream_proof: None,
4800                continuation_token_id: Some(continuation_token.token_id.clone()),
4801                session_anchor_id: validated_session_anchor_id,
4802            }));
4803        }
4804
4805        let Some(upstream_proof) = intent.upstream_call_chain_proof().map_err(|error| {
4806            KernelError::GovernedTransactionDenied(format!(
4807                "governed call_chain upstream proof is malformed: {error}"
4808            ))
4809        })?
4810        else {
4811            return Ok(None);
4812        };
4813
4814        let signature_valid = upstream_proof.verify_signature().map_err(|error| {
4815            KernelError::GovernedTransactionDenied(format!(
4816                "governed call_chain upstream proof failed signature verification: {error}"
4817            ))
4818        })?;
4819        if !signature_valid {
4820            return Err(KernelError::GovernedTransactionDenied(
4821                "governed call_chain upstream proof failed signature verification".to_string(),
4822            ));
4823        }
4824        upstream_proof.validate_time(now).map_err(|error| {
4825            KernelError::GovernedTransactionDenied(format!(
4826                "governed call_chain upstream proof rejected by time bounds: {error}"
4827            ))
4828        })?;
4829        if upstream_proof.subject != cap.subject {
4830            return Err(KernelError::GovernedTransactionDenied(
4831                "governed call_chain upstream proof subject does not match the capability subject"
4832                    .to_string(),
4833            ));
4834        }
4835
4836        let Some(expected_signer) = cap.delegation_chain.last().map(|link| &link.delegator) else {
4837            return Err(KernelError::GovernedTransactionDenied(
4838                "governed call_chain upstream proof requires a delegated capability lineage"
4839                    .to_string(),
4840            ));
4841        };
4842        if upstream_proof.signer != *expected_signer {
4843            return Err(KernelError::GovernedTransactionDenied(
4844                "governed call_chain upstream proof signer does not match the validated capability delegation source".to_string(),
4845            ));
4846        }
4847        if upstream_proof.chain_id != call_chain.chain_id {
4848            return Err(KernelError::GovernedTransactionDenied(
4849                "governed call_chain upstream proof chain_id does not match the asserted call_chain".to_string(),
4850            ));
4851        }
4852        if upstream_proof.parent_request_id != call_chain.parent_request_id {
4853            return Err(KernelError::GovernedTransactionDenied(
4854                "governed call_chain upstream proof parent_request_id does not match the asserted call_chain".to_string(),
4855            ));
4856        }
4857        if upstream_proof.parent_receipt_id != call_chain.parent_receipt_id {
4858            return Err(KernelError::GovernedTransactionDenied(
4859                "governed call_chain upstream proof parent_receipt_id does not match the asserted call_chain".to_string(),
4860            ));
4861        }
4862        if upstream_proof.origin_subject != call_chain.origin_subject {
4863            return Err(KernelError::GovernedTransactionDenied(
4864                "governed call_chain upstream proof origin_subject does not match the asserted call_chain".to_string(),
4865            ));
4866        }
4867        if upstream_proof.delegator_subject != call_chain.delegator_subject {
4868            return Err(KernelError::GovernedTransactionDenied(
4869                "governed call_chain upstream proof delegator_subject does not match the asserted call_chain".to_string(),
4870            ));
4871        }
4872
4873        Ok(Some(ValidatedGovernedCallChainProof {
4874            upstream_proof: Some(upstream_proof),
4875            continuation_token_id: None,
4876            session_anchor_id: None,
4877        }))
4878    }
4879
4880    fn validate_governed_autonomy_bond(
4881        &self,
4882        request: &ToolCallRequest,
4883        cap: &CapabilityToken,
4884        bond_id: &str,
4885        now: u64,
4886    ) -> Result<(), KernelError> {
4887        let Some(bond_row) = self.with_receipt_store(|store| {
4888            store.resolve_credit_bond(bond_id).map_err(|error| {
4889                KernelError::GovernedTransactionDenied(format!(
4890                    "failed to resolve delegation bond `{bond_id}`: {error}"
4891                ))
4892            })
4893        })?
4894        else {
4895            return Err(KernelError::GovernedTransactionDenied(
4896                "delegation bond lookup unavailable because no receipt store is configured"
4897                    .to_string(),
4898            ));
4899        };
4900        let bond_row = bond_row.ok_or_else(|| {
4901            KernelError::GovernedTransactionDenied(format!(
4902                "delegation bond `{bond_id}` was not found"
4903            ))
4904        })?;
4905
4906        let signed_bond = &bond_row.bond;
4907        let signature_valid = signed_bond.verify_signature().map_err(|error| {
4908            KernelError::GovernedTransactionDenied(format!(
4909                "delegation bond `{bond_id}` failed signature verification: {error}"
4910            ))
4911        })?;
4912        if !signature_valid {
4913            return Err(KernelError::GovernedTransactionDenied(format!(
4914                "delegation bond `{bond_id}` failed signature verification"
4915            )));
4916        }
4917        if bond_row.lifecycle_state != CreditBondLifecycleState::Active {
4918            return Err(KernelError::GovernedTransactionDenied(format!(
4919                "delegation bond `{bond_id}` is not active"
4920            )));
4921        }
4922        if signed_bond.body.expires_at <= now {
4923            return Err(KernelError::GovernedTransactionDenied(format!(
4924                "delegation bond `{bond_id}` is expired"
4925            )));
4926        }
4927
4928        let report = &signed_bond.body.report;
4929        if !report.support_boundary.autonomy_gating_supported {
4930            return Err(KernelError::GovernedTransactionDenied(format!(
4931                "delegation bond `{bond_id}` does not advertise runtime autonomy gating support"
4932            )));
4933        }
4934        if !report.prerequisites.active_facility_met {
4935            return Err(KernelError::GovernedTransactionDenied(format!(
4936                "delegation bond `{bond_id}` is missing an active granted facility"
4937            )));
4938        }
4939        if !report.prerequisites.runtime_assurance_met {
4940            return Err(KernelError::GovernedTransactionDenied(format!(
4941                "delegation bond `{bond_id}` was issued without satisfied runtime assurance prerequisites"
4942            )));
4943        }
4944        if report.prerequisites.certification_required && !report.prerequisites.certification_met {
4945            return Err(KernelError::GovernedTransactionDenied(format!(
4946                "delegation bond `{bond_id}` requires an active certification record"
4947            )));
4948        }
4949        match report.disposition {
4950            CreditBondDisposition::Lock | CreditBondDisposition::Hold => {}
4951            CreditBondDisposition::Release => {
4952                return Err(KernelError::GovernedTransactionDenied(format!(
4953                    "delegation bond `{bond_id}` is released and does not back autonomous execution"
4954                )));
4955            }
4956            CreditBondDisposition::Impair => {
4957                return Err(KernelError::GovernedTransactionDenied(format!(
4958                    "delegation bond `{bond_id}` is impaired and does not back autonomous execution"
4959                )));
4960            }
4961        }
4962
4963        let subject_key = cap.subject.to_hex();
4964        let mut bound_to_subject_or_capability = false;
4965        if let Some(bound_subject) = report.filters.agent_subject.as_deref() {
4966            if bound_subject != subject_key {
4967                return Err(KernelError::GovernedTransactionDenied(format!(
4968                    "delegation bond `{bond_id}` subject binding does not match the capability subject"
4969                )));
4970            }
4971            bound_to_subject_or_capability = true;
4972        }
4973        if let Some(bound_capability_id) = report.filters.capability_id.as_deref() {
4974            if bound_capability_id != cap.id {
4975                return Err(KernelError::GovernedTransactionDenied(format!(
4976                    "delegation bond `{bond_id}` capability binding does not match the executing capability"
4977                )));
4978            }
4979            bound_to_subject_or_capability = true;
4980        }
4981        if !bound_to_subject_or_capability {
4982            return Err(KernelError::GovernedTransactionDenied(format!(
4983                "delegation bond `{bond_id}` must be bound to the current capability or subject"
4984            )));
4985        }
4986
4987        let Some(bound_server) = report.filters.tool_server.as_deref() else {
4988            return Err(KernelError::GovernedTransactionDenied(format!(
4989                "delegation bond `{bond_id}` must be scoped to the current tool server"
4990            )));
4991        };
4992        if bound_server != request.server_id {
4993            return Err(KernelError::GovernedTransactionDenied(format!(
4994                "delegation bond `{bond_id}` tool server scope does not match the governed request"
4995            )));
4996        }
4997        if let Some(bound_tool) = report.filters.tool_name.as_deref() {
4998            if bound_tool != request.tool_name {
4999                return Err(KernelError::GovernedTransactionDenied(format!(
5000                    "delegation bond `{bond_id}` tool scope does not match the governed request"
5001                )));
5002            }
5003        }
5004
5005        Ok(())
5006    }
5007
5008    fn validate_governed_autonomy(
5009        &self,
5010        request: &ToolCallRequest,
5011        cap: &CapabilityToken,
5012        intent: &chio_core::capability::GovernedTransactionIntent,
5013        minimum_autonomy_tier: Option<GovernedAutonomyTier>,
5014        verified_runtime_attestation: Option<&VerifiedRuntimeAttestationRecord>,
5015        now: u64,
5016    ) -> Result<(), KernelError> {
5017        let autonomy = match (intent.autonomy.as_ref(), minimum_autonomy_tier) {
5018            (None, None) => return Ok(()),
5019            (Some(autonomy), _) => autonomy,
5020            (None, Some(required_tier)) => {
5021                return Err(KernelError::GovernedTransactionDenied(format!(
5022                    "governed autonomy tier '{required_tier:?}' required by grant"
5023                )));
5024            }
5025        };
5026
5027        if let Some(required_tier) = minimum_autonomy_tier {
5028            if autonomy.tier < required_tier {
5029                return Err(KernelError::GovernedTransactionDenied(format!(
5030                    "governed autonomy tier '{:?}' is below required '{required_tier:?}'",
5031                    autonomy.tier
5032                )));
5033            }
5034        }
5035
5036        let bond_id = autonomy
5037            .delegation_bond_id
5038            .as_deref()
5039            .map(str::trim)
5040            .filter(|value| !value.is_empty());
5041
5042        if !autonomy.tier.requires_delegation_bond() {
5043            if bond_id.is_some() {
5044                return Err(KernelError::GovernedTransactionDenied(
5045                    "direct governed autonomy tier must not attach a delegation bond".to_string(),
5046                ));
5047            }
5048            return Ok(());
5049        }
5050
5051        if autonomy.tier.requires_call_chain() && intent.call_chain.is_none() {
5052            return Err(KernelError::GovernedTransactionDenied(format!(
5053                "governed autonomy tier '{:?}' requires delegated call-chain context",
5054                autonomy.tier
5055            )));
5056        }
5057
5058        let required_runtime_assurance = autonomy.tier.minimum_runtime_assurance();
5059        let requirement_source = format!("governed autonomy tier '{:?}'", autonomy.tier);
5060        Self::validate_runtime_assurance(
5061            verified_runtime_attestation,
5062            required_runtime_assurance,
5063            &requirement_source,
5064        )?;
5065
5066        let bond_id = bond_id.ok_or_else(|| {
5067            KernelError::GovernedTransactionDenied(format!(
5068                "governed autonomy tier '{:?}' requires a delegation bond attachment",
5069                autonomy.tier
5070            ))
5071        })?;
5072        self.validate_governed_autonomy_bond(request, cap, bond_id, now)
5073    }
5074
5075    fn validate_governed_transaction(
5076        &self,
5077        request: &ToolCallRequest,
5078        cap: &CapabilityToken,
5079        grant: &ToolGrant,
5080        charge_result: Option<&BudgetChargeResult>,
5081        parent_context: Option<&OperationContext>,
5082        now: u64,
5083    ) -> Result<Option<ValidatedGovernedAdmission>, KernelError> {
5084        let (
5085            intent_required,
5086            approval_threshold_units,
5087            required_seller,
5088            minimum_runtime_assurance,
5089            minimum_autonomy_tier,
5090        ) = Self::governed_requirements(grant);
5091        let governed_request_present =
5092            request.governed_intent.is_some() || request.approval_token.is_some();
5093
5094        if !intent_required
5095            && approval_threshold_units.is_none()
5096            && required_seller.is_none()
5097            && minimum_runtime_assurance.is_none()
5098            && minimum_autonomy_tier.is_none()
5099            && !governed_request_present
5100        {
5101            return Ok(None);
5102        }
5103
5104        let intent = request.governed_intent.as_ref().ok_or_else(|| {
5105            KernelError::GovernedTransactionDenied(
5106                "governed transaction intent required by grant or request".to_string(),
5107            )
5108        })?;
5109
5110        if intent.server_id != request.server_id || intent.tool_name != request.tool_name {
5111            return Err(KernelError::GovernedTransactionDenied(
5112                "governed transaction intent target does not match the tool call".to_string(),
5113            ));
5114        }
5115
5116        let verified_runtime_attestation =
5117            self.verify_governed_request_runtime_attestation(request, now)?;
5118
5119        let validated_upstream_call_chain_proof =
5120            self.validate_governed_call_chain_context(request, cap, intent, parent_context, now)?;
5121
5122        let intent_hash = intent.binding_hash().map_err(|error| {
5123            KernelError::GovernedTransactionDenied(format!(
5124                "failed to hash governed transaction intent: {error}"
5125            ))
5126        })?;
5127        let commerce = intent.commerce.as_ref();
5128
5129        if let Some(commerce) = commerce {
5130            if commerce.seller.trim().is_empty() {
5131                return Err(KernelError::GovernedTransactionDenied(
5132                    "governed commerce seller scope must not be empty".to_string(),
5133                ));
5134            }
5135            if commerce.shared_payment_token_id.trim().is_empty() {
5136                return Err(KernelError::GovernedTransactionDenied(
5137                    "governed commerce approval requires a shared payment token reference"
5138                        .to_string(),
5139                ));
5140            }
5141            if intent.max_amount.is_none() {
5142                return Err(KernelError::GovernedTransactionDenied(
5143                    "governed commerce approval requires an explicit max_amount bound".to_string(),
5144                ));
5145            }
5146        }
5147
5148        if let Some(required_seller) = required_seller.as_deref() {
5149            let commerce = commerce.ok_or_else(|| {
5150                KernelError::GovernedTransactionDenied(
5151                    "seller-scoped governed request requires commerce approval context".to_string(),
5152                )
5153            })?;
5154            if commerce.seller != required_seller {
5155                return Err(KernelError::GovernedTransactionDenied(
5156                    "governed commerce seller does not match the grant seller scope".to_string(),
5157                ));
5158            }
5159        }
5160
5161        if let Some(required_tier) = minimum_runtime_assurance {
5162            Self::validate_runtime_assurance(
5163                verified_runtime_attestation.as_ref(),
5164                required_tier,
5165                "grant",
5166            )?;
5167        }
5168        self.validate_governed_autonomy(
5169            request,
5170            cap,
5171            intent,
5172            minimum_autonomy_tier,
5173            verified_runtime_attestation.as_ref(),
5174            now,
5175        )?;
5176
5177        Self::validate_metered_billing_context(intent, charge_result, now)?;
5178
5179        if let (Some(intent_amount), Some(charge)) = (intent.max_amount.as_ref(), charge_result) {
5180            if intent_amount.currency != charge.currency {
5181                return Err(KernelError::GovernedTransactionDenied(
5182                    "governed intent currency does not match the grant currency".to_string(),
5183                ));
5184            }
5185            if intent_amount.units < charge.cost_charged {
5186                return Err(KernelError::GovernedTransactionDenied(
5187                    "governed intent amount is lower than the provisional invocation charge"
5188                        .to_string(),
5189                ));
5190            }
5191        }
5192
5193        let requested_units = charge_result
5194            .map(|charge| charge.cost_charged)
5195            .or_else(|| intent.max_amount.as_ref().map(|amount| amount.units))
5196            .unwrap_or(0);
5197        let approval_required = approval_threshold_units
5198            .map(|threshold_units| requested_units >= threshold_units)
5199            .unwrap_or(false);
5200
5201        if let Some(approval_token) = request.approval_token.as_ref() {
5202            self.validate_governed_approval_token(request, cap, &intent_hash, approval_token, now)?;
5203        } else if approval_required {
5204            return Err(KernelError::GovernedTransactionDenied(format!(
5205                "approval token required for governed transaction intent {}",
5206                intent.id
5207            )));
5208        }
5209
5210        Ok(Some(ValidatedGovernedAdmission {
5211            call_chain_proof: validated_upstream_call_chain_proof,
5212            verified_runtime_attestation,
5213        }))
5214    }
5215
5216    fn governed_call_chain_receipt_evidence(
5217        &self,
5218        request: &ToolCallRequest,
5219        cap: &CapabilityToken,
5220        parent_context: Option<&OperationContext>,
5221        validated_proof: Option<ValidatedGovernedCallChainProof>,
5222    ) -> Option<GovernedCallChainReceiptEvidence> {
5223        let call_chain = request.governed_intent.as_ref()?.call_chain.as_ref()?;
5224        let continuation_token_id = validated_proof
5225            .as_ref()
5226            .and_then(|proof| proof.continuation_token_id.clone());
5227        let session_anchor_id = validated_proof
5228            .as_ref()
5229            .and_then(|proof| proof.session_anchor_id.clone());
5230        let upstream_proof = validated_proof.and_then(|proof| proof.upstream_proof);
5231        let local_parent_request_id = parent_context
5232            .map(|context| context.request_id.to_string())
5233            .filter(|_| {
5234                parent_context.is_some_and(|context| {
5235                    self.validate_parent_request_continuation(request, context)
5236                        .is_ok()
5237                })
5238            });
5239        let local_parent_receipt_id = call_chain
5240            .parent_receipt_id
5241            .as_ref()
5242            .filter(|receipt_id| self.has_local_receipt_id(receipt_id))
5243            .cloned();
5244        let capability_delegator_subject = cap
5245            .delegation_chain
5246            .last()
5247            .map(|link| link.delegator.to_hex());
5248        let capability_origin_subject = cap
5249            .delegation_chain
5250            .first()
5251            .map(|link| link.delegator.to_hex());
5252
5253        if local_parent_request_id.is_none()
5254            && local_parent_receipt_id.is_none()
5255            && capability_delegator_subject.is_none()
5256            && capability_origin_subject.is_none()
5257            && continuation_token_id.is_none()
5258            && session_anchor_id.is_none()
5259            && upstream_proof.is_none()
5260        {
5261            return None;
5262        }
5263
5264        Some(GovernedCallChainReceiptEvidence {
5265            local_parent_request_id,
5266            local_parent_receipt_id,
5267            capability_delegator_subject,
5268            capability_origin_subject,
5269            upstream_proof,
5270            continuation_token_id,
5271            session_anchor_id,
5272        })
5273    }
5274
5275    fn validate_parent_request_continuation(
5276        &self,
5277        request: &ToolCallRequest,
5278        parent_context: &OperationContext,
5279    ) -> Result<(), KernelError> {
5280        let child_request_id = RequestId::new(request.request_id.clone());
5281        self.with_session(&parent_context.session_id, |session| {
5282            session.validate_context(parent_context)?;
5283            session
5284                .validate_parent_request_lineage(&child_request_id, &parent_context.request_id)?;
5285            Ok(())
5286        })
5287    }
5288
5289    fn has_local_receipt_id(&self, receipt_id: &str) -> bool {
5290        let chio_receipt_match = self.receipt_log.lock().ok().is_some_and(|log| {
5291            log.receipts()
5292                .iter()
5293                .any(|receipt| receipt.id == receipt_id)
5294        });
5295        if chio_receipt_match {
5296            return true;
5297        }
5298
5299        self.child_receipt_log.lock().ok().is_some_and(|log| {
5300            log.receipts()
5301                .iter()
5302                .any(|receipt| receipt.id == receipt_id)
5303        })
5304    }
5305
5306    fn is_trusted_governed_continuation_signer(&self, signer: &chio_core::PublicKey) -> bool {
5307        if *signer == self.config.keypair.public_key() {
5308            return true;
5309        }
5310        if self
5311            .config
5312            .ca_public_keys
5313            .iter()
5314            .any(|candidate| candidate == signer)
5315        {
5316            return true;
5317        }
5318        self.capability_authority
5319            .trusted_public_keys()
5320            .into_iter()
5321            .any(|candidate| candidate == *signer)
5322    }
5323
5324    fn local_receipt_artifact(&self, receipt_id: &str) -> Option<LocalReceiptArtifact> {
5325        let tool_match = self.receipt_log.lock().ok().and_then(|log| {
5326            log.receipts()
5327                .iter()
5328                .find(|receipt| receipt.id == receipt_id)
5329                .cloned()
5330                .map(LocalReceiptArtifact::Tool)
5331        });
5332        if tool_match.is_some() {
5333            return tool_match;
5334        }
5335
5336        self.child_receipt_log.lock().ok().and_then(|log| {
5337            log.receipts()
5338                .iter()
5339                .find(|receipt| receipt.id == receipt_id)
5340                .cloned()
5341                .map(LocalReceiptArtifact::Child)
5342        })
5343    }
5344
5345    fn unwind_aborted_monetary_invocation(
5346        &self,
5347        request: &ToolCallRequest,
5348        cap: &CapabilityToken,
5349        charge_result: Option<&BudgetChargeResult>,
5350        payment_authorization: Option<&PaymentAuthorization>,
5351    ) -> Result<Option<BudgetReverseHoldDecision>, KernelError> {
5352        let Some(charge) = charge_result else {
5353            return Ok(None);
5354        };
5355
5356        if let Some(authorization) = payment_authorization {
5357            let adapter = self.payment_adapter.as_ref().ok_or_else(|| {
5358                KernelError::Internal(
5359                    "payment authorization present without configured adapter".to_string(),
5360                )
5361            })?;
5362            let unwind_result = if authorization.settled {
5363                adapter.refund(
5364                    &authorization.authorization_id,
5365                    charge.cost_charged,
5366                    &charge.currency,
5367                    &request.request_id,
5368                )
5369            } else {
5370                adapter.release(&authorization.authorization_id, &request.request_id)
5371            };
5372            if let Err(error) = unwind_result {
5373                return Err(KernelError::Internal(format!(
5374                    "failed to unwind payment after aborted tool invocation: {error}"
5375                )));
5376            }
5377        }
5378
5379        Ok(Some(self.reverse_budget_charge(&cap.id, charge)?))
5380    }
5381
5382    fn record_observed_capability_snapshot(
5383        &self,
5384        capability: &CapabilityToken,
5385    ) -> Result<(), KernelError> {
5386        let parent_capability_id = capability
5387            .delegation_chain
5388            .last()
5389            .map(|link| link.capability_id.as_str());
5390        let _ = self.with_receipt_store(|store| {
5391            Ok(store.record_capability_snapshot(capability, parent_capability_id)?)
5392        })?;
5393        Ok(())
5394    }
5395
5396    /// Verify a DPoP proof carried on the request against the capability.
5397    ///
5398    /// Fails closed: if no proof is present, or if the nonce store / config is
5399    /// absent (misconfigured kernel), or if verification fails, the call is denied.
5400    fn verify_dpop_for_request(
5401        &self,
5402        request: &ToolCallRequest,
5403        cap: &CapabilityToken,
5404    ) -> Result<(), KernelError> {
5405        let proof = request.dpop_proof.as_ref().ok_or_else(|| {
5406            KernelError::DpopVerificationFailed(
5407                "grant requires DPoP proof but none was provided".to_string(),
5408            )
5409        })?;
5410
5411        let nonce_store = self.dpop_nonce_store.as_ref().ok_or_else(|| {
5412            KernelError::DpopVerificationFailed(
5413                "kernel DPoP nonce store not configured".to_string(),
5414            )
5415        })?;
5416
5417        let config = self.dpop_config.as_ref().ok_or_else(|| {
5418            KernelError::DpopVerificationFailed("kernel DPoP config not configured".to_string())
5419        })?;
5420
5421        // Compute action hash from the serialized arguments.
5422        let args_bytes = canonical_json_bytes(&request.arguments).map_err(|e| {
5423            KernelError::DpopVerificationFailed(format!(
5424                "failed to serialize arguments for action hash: {e}"
5425            ))
5426        })?;
5427        let action_hash = sha256_hex(&args_bytes);
5428
5429        dpop::verify_dpop_proof(
5430            proof,
5431            cap,
5432            &request.server_id,
5433            &request.tool_name,
5434            &action_hash,
5435            nonce_store,
5436            config,
5437        )
5438    }
5439
5440    /// Run all registered guards. Fail-closed: any error from a guard is
5441    /// treated as a deny.
5442    fn run_guards(
5443        &self,
5444        request: &ToolCallRequest,
5445        scope: &ChioScope,
5446        session_filesystem_roots: Option<&[String]>,
5447        matched_grant_index: Option<usize>,
5448    ) -> Result<(), KernelError> {
5449        let ctx = GuardContext {
5450            request,
5451            scope,
5452            agent_id: &request.agent_id,
5453            server_id: &request.server_id,
5454            session_filesystem_roots,
5455            matched_grant_index,
5456        };
5457
5458        for guard in &self.guards {
5459            match guard.evaluate(&ctx) {
5460                Ok(Verdict::Allow) => {
5461                    debug!(guard = guard.name(), "guard passed");
5462                }
5463                Ok(Verdict::Deny) => {
5464                    return Err(KernelError::GuardDenied(format!(
5465                        "guard \"{}\" denied the request",
5466                        guard.name()
5467                    )));
5468                }
5469                Ok(Verdict::PendingApproval) => {
5470                    // Phase 3.4: a legacy `Guard` should not return the
5471                    // HITL marker. The fully integrated approval flow
5472                    // runs via `ApprovalGuard::evaluate` rather than
5473                    // the `Guard` trait so this branch is unreachable
5474                    // in practice. Fail-closed just in case.
5475                    return Err(KernelError::GuardDenied(format!(
5476                        "guard \"{}\" requested approval via legacy path",
5477                        guard.name()
5478                    )));
5479                }
5480                Err(e) => {
5481                    // Fail closed: guard errors are treated as denials.
5482                    return Err(KernelError::GuardDenied(format!(
5483                        "guard \"{}\" error (fail-closed): {e}",
5484                        guard.name()
5485                    )));
5486                }
5487            }
5488        }
5489
5490        Ok(())
5491    }
5492
5493    /// Forward the validated request and optionally report actual invocation cost.
5494    ///
5495    /// When `has_monetary_grant` is true, calls `invoke_with_cost` so the server
5496    /// can report the actual cost incurred. For non-monetary grants the standard
5497    /// dispatch path is used and cost is always None.
5498    fn dispatch_tool_call_with_cost(
5499        &self,
5500        request: &ToolCallRequest,
5501        has_monetary_grant: bool,
5502    ) -> Result<(ToolServerOutput, Option<ToolInvocationCost>), KernelError> {
5503        let server = self.tool_servers.get(&request.server_id).ok_or_else(|| {
5504            KernelError::ToolNotRegistered(format!(
5505                "server \"{}\" / tool \"{}\"",
5506                request.server_id, request.tool_name
5507            ))
5508        })?;
5509
5510        // Try streaming first regardless of monetary mode.
5511        if let Some(stream) =
5512            server.invoke_stream(&request.tool_name, request.arguments.clone(), None)?
5513        {
5514            return Ok((ToolServerOutput::Stream(stream), None));
5515        }
5516
5517        if has_monetary_grant {
5518            let (value, cost) =
5519                server.invoke_with_cost(&request.tool_name, request.arguments.clone(), None)?;
5520            Ok((ToolServerOutput::Value(value), cost))
5521        } else {
5522            let value = server.invoke(&request.tool_name, request.arguments.clone(), None)?;
5523            Ok((ToolServerOutput::Value(value), None))
5524        }
5525    }
5526
5527    /// Build a denial response, including FinancialReceiptMetadata when the
5528    fn record_child_receipts(&self, receipts: Vec<ChildRequestReceipt>) -> Result<(), KernelError> {
5529        for receipt in receipts {
5530            let _ = self.with_receipt_store(|store| Ok(store.append_child_receipt(&receipt)?))?;
5531            self.child_receipt_log
5532                .lock()
5533                .map_err(|_| KernelError::Internal("child receipt log lock poisoned".to_string()))?
5534                .append(receipt);
5535        }
5536        Ok(())
5537    }
5538}
5539
5540/// Extract a guard name from a `GuardDenied` error message shaped like
5541/// `guard "<name>" denied the request` or `guard "<name>" error ...`.
5542///
5543/// Plan evaluation surfaces the offending guard in the per-step verdict
5544/// so callers can target a specific guard when replanning. Parsing the
5545/// name out of the canonical string is sufficient here; the structured
5546/// denial payload defined by Phase 0.5 is a tool-call response type and
5547/// is not shared with plan evaluation.
5548fn extract_guard_name(message: &str) -> Option<String> {
5549    let start_marker = "guard \"";
5550    let start = message.find(start_marker)? + start_marker.len();
5551    let rest = &message[start..];
5552    let end = rest.find('"')?;
5553    Some(rest[..end].to_string())
5554}
5555
5556fn scope_from_capability_snapshot(
5557    snapshot: &crate::capability_lineage::CapabilitySnapshot,
5558) -> Result<ChioScope, KernelError> {
5559    serde_json::from_str(&snapshot.grants_json).map_err(|error| {
5560        KernelError::Internal(format!(
5561            "invalid capability snapshot scope for {}: {error}",
5562            snapshot.capability_id
5563        ))
5564    })
5565}
5566
5567fn validate_delegation_scope_step(
5568    parent_capability_id: &str,
5569    child_capability_id: &str,
5570    parent_scope: &ChioScope,
5571    child_scope: &ChioScope,
5572    child_expires_at: u64,
5573    link: &chio_core::capability::DelegationLink,
5574) -> Result<(), KernelError> {
5575    validate_delegatable_subset(
5576        parent_capability_id,
5577        child_capability_id,
5578        parent_scope,
5579        child_scope,
5580    )?;
5581    validate_declared_attenuations(child_capability_id, child_scope, child_expires_at, link)?;
5582    Ok(())
5583}
5584
5585fn validate_delegatable_subset(
5586    parent_capability_id: &str,
5587    child_capability_id: &str,
5588    parent_scope: &ChioScope,
5589    child_scope: &ChioScope,
5590) -> Result<(), KernelError> {
5591    for child_grant in &child_scope.grants {
5592        let allowed = parent_scope.grants.iter().any(|parent_grant| {
5593            parent_grant.operations.contains(&Operation::Delegate)
5594                && child_grant.is_subset_of(parent_grant)
5595        });
5596        if !allowed {
5597            return Err(KernelError::DelegationInvalid(format!(
5598                "parent capability {} does not authorize delegated tool grant {}/{} on child capability {}",
5599                parent_capability_id,
5600                child_grant.server_id,
5601                child_grant.tool_name,
5602                child_capability_id
5603            )));
5604        }
5605    }
5606
5607    for child_grant in &child_scope.resource_grants {
5608        let allowed = parent_scope.resource_grants.iter().any(|parent_grant| {
5609            parent_grant.operations.contains(&Operation::Delegate)
5610                && child_grant.is_subset_of(parent_grant)
5611        });
5612        if !allowed {
5613            return Err(KernelError::DelegationInvalid(format!(
5614                "parent capability {} does not authorize delegated resource grant {} on child capability {}",
5615                parent_capability_id, child_grant.uri_pattern, child_capability_id
5616            )));
5617        }
5618    }
5619
5620    for child_grant in &child_scope.prompt_grants {
5621        let allowed = parent_scope.prompt_grants.iter().any(|parent_grant| {
5622            parent_grant.operations.contains(&Operation::Delegate)
5623                && child_grant.is_subset_of(parent_grant)
5624        });
5625        if !allowed {
5626            return Err(KernelError::DelegationInvalid(format!(
5627                "parent capability {} does not authorize delegated prompt grant {} on child capability {}",
5628                parent_capability_id, child_grant.prompt_name, child_capability_id
5629            )));
5630        }
5631    }
5632
5633    Ok(())
5634}
5635
5636fn validate_declared_attenuations(
5637    child_capability_id: &str,
5638    child_scope: &ChioScope,
5639    child_expires_at: u64,
5640    link: &chio_core::capability::DelegationLink,
5641) -> Result<(), KernelError> {
5642    for attenuation in &link.attenuations {
5643        match attenuation {
5644            chio_core::capability::Attenuation::RemoveTool {
5645                server_id,
5646                tool_name,
5647            } => {
5648                if child_scope
5649                    .grants
5650                    .iter()
5651                    .any(|grant| tool_grant_covers_target(grant, server_id, tool_name))
5652                {
5653                    return Err(KernelError::DelegationInvalid(format!(
5654                        "child capability {} still grants removed tool {}/{}",
5655                        child_capability_id, server_id, tool_name
5656                    )));
5657                }
5658            }
5659            chio_core::capability::Attenuation::RemoveOperation {
5660                server_id,
5661                tool_name,
5662                operation,
5663            } => {
5664                if child_scope.grants.iter().any(|grant| {
5665                    tool_grant_covers_target(grant, server_id, tool_name)
5666                        && grant.operations.contains(operation)
5667                }) {
5668                    return Err(KernelError::DelegationInvalid(format!(
5669                        "child capability {} still grants removed operation {:?} on {}/{}",
5670                        child_capability_id, operation, server_id, tool_name
5671                    )));
5672                }
5673            }
5674            chio_core::capability::Attenuation::AddConstraint {
5675                server_id,
5676                tool_name,
5677                constraint,
5678            } => {
5679                if child_scope.grants.iter().any(|grant| {
5680                    tool_grant_covers_target(grant, server_id, tool_name)
5681                        && !grant.constraints.contains(constraint)
5682                }) {
5683                    return Err(KernelError::DelegationInvalid(format!(
5684                        "child capability {} is missing declared constraint on {}/{}",
5685                        child_capability_id, server_id, tool_name
5686                    )));
5687                }
5688            }
5689            chio_core::capability::Attenuation::ReduceBudget {
5690                server_id,
5691                tool_name,
5692                max_invocations,
5693            } => {
5694                if child_scope.grants.iter().any(|grant| {
5695                    tool_grant_covers_target(grant, server_id, tool_name)
5696                        && grant
5697                            .max_invocations
5698                            .is_none_or(|value| value > *max_invocations)
5699                }) {
5700                    return Err(KernelError::DelegationInvalid(format!(
5701                        "child capability {} exceeds declared invocation budget on {}/{}",
5702                        child_capability_id, server_id, tool_name
5703                    )));
5704                }
5705            }
5706            chio_core::capability::Attenuation::ShortenExpiry { new_expires_at } => {
5707                if child_expires_at > *new_expires_at {
5708                    return Err(KernelError::DelegationInvalid(format!(
5709                        "child capability {} expires after declared shortened expiry {}",
5710                        child_capability_id, new_expires_at
5711                    )));
5712                }
5713            }
5714            chio_core::capability::Attenuation::ReduceCostPerInvocation {
5715                server_id,
5716                tool_name,
5717                max_cost_per_invocation,
5718            } => {
5719                if child_scope.grants.iter().any(|grant| {
5720                    tool_grant_covers_target(grant, server_id, tool_name)
5721                        && grant.max_cost_per_invocation.as_ref().is_none_or(|value| {
5722                            value.currency != max_cost_per_invocation.currency
5723                                || value.units > max_cost_per_invocation.units
5724                        })
5725                }) {
5726                    return Err(KernelError::DelegationInvalid(format!(
5727                        "child capability {} exceeds declared per-invocation cost ceiling on {}/{}",
5728                        child_capability_id, server_id, tool_name
5729                    )));
5730                }
5731            }
5732            chio_core::capability::Attenuation::ReduceTotalCost {
5733                server_id,
5734                tool_name,
5735                max_total_cost,
5736            } => {
5737                if child_scope.grants.iter().any(|grant| {
5738                    tool_grant_covers_target(grant, server_id, tool_name)
5739                        && grant.max_total_cost.as_ref().is_none_or(|value| {
5740                            value.currency != max_total_cost.currency
5741                                || value.units > max_total_cost.units
5742                        })
5743                }) {
5744                    return Err(KernelError::DelegationInvalid(format!(
5745                        "child capability {} exceeds declared total-cost ceiling on {}/{}",
5746                        child_capability_id, server_id, tool_name
5747                    )));
5748                }
5749            }
5750        }
5751    }
5752
5753    Ok(())
5754}
5755
5756fn tool_grant_covers_target(grant: &ToolGrant, server_id: &str, tool_name: &str) -> bool {
5757    (grant.server_id == "*" || grant.server_id == server_id)
5758        && (grant.tool_name == "*" || grant.tool_name == tool_name)
5759}
5760
5761/// Parameters for building a receipt.
5762pub(crate) struct ReceiptParams<'a> {
5763    capability_id: &'a str,
5764    tool_name: &'a str,
5765    server_id: &'a str,
5766    decision: Decision,
5767    action: ToolCallAction,
5768    content_hash: String,
5769    metadata: Option<serde_json::Value>,
5770    timestamp: u64,
5771    /// Strength of kernel mediation for this evaluation. Defaults to
5772    /// `Mediated` (the safest baseline) when integration adapters do not
5773    /// override it.
5774    trust_level: chio_core::TrustLevel,
5775    /// Phase 1.5 multi-tenant receipt isolation: explicit tenant tag for
5776    /// this receipt. `None` in virtually every call site -- the evaluate
5777    /// path plumbs the resolved tenant through
5778    /// [`scope_receipt_tenant_id`] so `build_and_sign_receipt` can pick it
5779    /// up without adding a parameter to every builder signature.
5780    ///
5781    /// MUST be derived from session / auth context, not caller-provided
5782    /// request fields (see `STRUCTURAL-SECURITY-FIXES.md` section 6).
5783    tenant_id: Option<String>,
5784}
5785
5786pub(crate) fn current_unix_timestamp() -> u64 {
5787    SystemTime::now()
5788        .duration_since(UNIX_EPOCH)
5789        .map(|d| d.as_secs())
5790        .unwrap_or(0)
5791}
5792
5793#[allow(dead_code)]
5794#[path = "responses.rs"]
5795mod responses;
5796#[path = "session_ops.rs"]
5797mod session_ops;
5798#[cfg(test)]
5799#[path = "tests.rs"]
5800mod tests;