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
16pub type CapabilityId = String;
18
19pub type ServerId = String;
21
22pub const EMERGENCY_STOP_DENY_REASON: &str = "kernel emergency stop active";
26
27thread_local! {
47 static RECEIPT_TENANT_ID_SCOPE: std::cell::RefCell<Option<String>> =
48 const { std::cell::RefCell::new(None) };
49}
50
51pub(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
66pub(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
75pub(crate) fn current_scoped_receipt_tenant_id() -> Option<String> {
80 RECEIPT_TENANT_ID_SCOPE.with(|slot| slot.borrow().clone())
81}
82
83pub(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#[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 #[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
665pub trait Guard: Send + Sync {
671 fn name(&self) -> &str;
673
674 fn evaluate(&self, ctx: &GuardContext) -> Result<Verdict, KernelError>;
679}
680
681pub struct GuardContext<'a> {
683 pub request: &'a ToolCallRequest,
685 pub scope: &'a ChioScope,
687 pub agent_id: &'a AgentId,
689 pub server_id: &'a ServerId,
691 pub session_filesystem_roots: Option<&'a [String]>,
694 pub matched_grant_index: Option<usize>,
697}
698
699pub trait ResourceProvider: Send + Sync {
701 fn list_resources(&self) -> Vec<ResourceDefinition>;
703
704 fn list_resource_templates(&self) -> Vec<ResourceTemplateDefinition> {
706 vec![]
707 }
708
709 fn read_resource(&self, uri: &str) -> Result<Option<Vec<ResourceContent>>, KernelError>;
711
712 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
724pub trait PromptProvider: Send + Sync {
726 fn list_prompts(&self) -> Vec<PromptDefinition>;
728
729 fn get_prompt(
731 &self,
732 name: &str,
733 arguments: serde_json::Value,
734 ) -> Result<Option<PromptResult>, KernelError>;
735
736 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#[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#[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
815pub struct KernelConfig {
817 pub keypair: Keypair,
819
820 pub ca_public_keys: Vec<chio_core::PublicKey>,
822
823 pub max_delegation_depth: u32,
825
826 pub policy_hash: String,
828
829 pub allow_sampling: bool,
831
832 pub allow_sampling_tool_use: bool,
834
835 pub allow_elicitation: bool,
837
838 pub max_stream_duration_secs: u64,
840
841 pub max_stream_total_bytes: u64,
843
844 pub require_web3_evidence: bool,
847
848 pub checkpoint_batch_size: u64,
853
854 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
868pub 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 checkpoint_batch_size: u64,
894 checkpoint_seq_counter: AtomicU64,
896 last_checkpoint_seq: AtomicU64,
898 dpop_nonce_store: Option<dpop::DpopNonceStore>,
900 dpop_config: Option<dpop::DpopConfig>,
902 execution_nonce_config: Option<crate::execution_nonce::ExecutionNonceConfig>,
906 execution_nonce_store: Option<Box<dyn crate::execution_nonce::ExecutionNonceStore>>,
910 approval_replay_store: Option<dpop::DpopNonceStore>,
914 emergency_stopped: AtomicBool,
923 emergency_stopped_since: AtomicU64,
928 emergency_stop_reason: Mutex<Option<String>>,
933 memory_provenance: Option<Arc<dyn crate::memory_provenance::MemoryProvenanceStore>>,
941 federation_peers: RwLock<HashMap<String, chio_federation::FederationPeer>>,
947 federation_cosigner: Option<Arc<dyn chio_federation::BilateralCoSigningProtocol>>,
951 federation_dual_receipts: Mutex<HashMap<String, chio_federation::DualSignedReceipt>>,
956 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
970pub(crate) struct BudgetChargeResult {
974 grant_index: usize,
975 cost_charged: u64,
976 currency: String,
977 budget_total: u64,
978 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 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 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 #[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 #[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 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 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 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 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 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 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 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 pub fn emergency_stop(&self, reason: &str) -> Result<(), KernelError> {
1590 let now = current_unix_timestamp();
1591 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 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 #[must_use]
1630 pub fn is_emergency_stopped(&self) -> bool {
1631 self.emergency_stopped.load(Ordering::SeqCst)
1632 }
1633
1634 #[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 #[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 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 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 #[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 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 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 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 pub fn add_guard(&mut self, guard: Box<dyn Guard>) {
1853 self.guards.push(guard);
1854 }
1855
1856 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 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 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 fn validate_non_tool_capability(
1878 &self,
1879 capability: &CapabilityToken,
1880 agent_id: &str,
1881 ) -> Result<(), KernelError> {
1882 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 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 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 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 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 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 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 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 if let Err(error) =
2186 self.run_guards(&synthesised, &cap.scope, None, Some(matched_grant_index))
2187 {
2188 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 const MAX_APPROVAL_TTL_SECS: u64 = 3600; 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 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 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 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 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 return Err(KernelError::GuardDenied(format!(
5476 "guard \"{}\" requested approval via legacy path",
5477 guard.name()
5478 )));
5479 }
5480 Err(e) => {
5481 return Err(KernelError::GuardDenied(format!(
5483 "guard \"{}\" error (fail-closed): {e}",
5484 guard.name()
5485 )));
5486 }
5487 }
5488 }
5489
5490 Ok(())
5491 }
5492
5493 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 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 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
5540fn 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
5761pub(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 trust_level: chio_core::TrustLevel,
5775 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;