Skip to main content

chio_kernel/kernel/
session_ops.rs

1use std::sync::Arc;
2
3use base64::engine::general_purpose::URL_SAFE_NO_PAD;
4use base64::Engine as _;
5use dashmap::mapref::entry::Entry;
6use rand::rngs::OsRng;
7use rand::RngCore;
8
9use crate::session::{SessionAnchorSnapshot, SessionRequestStart};
10
11use super::*;
12
13/// Number of CSPRNG bytes used to derive a fresh session id. 16 bytes (128 bits)
14/// is well above the birthday-bound budget for any realistic session population
15/// and matches the "URL-safe random handle" recipe used elsewhere in the
16/// workspace.
17const SESSION_ID_ENTROPY_BYTES: usize = 16;
18
19/// Mint a fresh URL-safe session identifier from the operating system's
20/// CSPRNG. Random handles prevent external enumeration of active tenants and
21/// close the session-fixation surface that sequential ids carry.
22fn generate_random_session_id() -> SessionId {
23    let mut bytes = [0u8; SESSION_ID_ENTROPY_BYTES];
24    OsRng.fill_bytes(&mut bytes);
25    // base64url without padding produces 22 chars for 16 bytes; the
26    // `sess-` prefix preserves human readability for log scanning.
27    SessionId::new(format!("sess-{}", URL_SAFE_NO_PAD.encode(bytes)))
28}
29
30fn map_session_persist_error(error: SessionPersistError<KernelError>) -> KernelError {
31    match error {
32        SessionPersistError::Session(error) => KernelError::Session(error),
33        SessionPersistError::Persist(error) => error,
34    }
35}
36
37fn parse_tool_call_operation_execution_nonce(
38    operation: &ToolCallOperation,
39) -> Result<Option<crate::execution_nonce::SignedExecutionNonce>, KernelError> {
40    match operation.execution_nonce.as_ref() {
41        Some(value) => Some(serde_json::from_value(value.clone()).map_err(|error| {
42            KernelError::InvalidConstraint(format!(
43                "session tool call execution_nonce is malformed: {error}"
44            ))
45        }))
46        .transpose(),
47        None => Ok(None),
48    }
49}
50
51impl ChioKernel {
52    pub fn open_session(
53        &self,
54        agent_id: AgentId,
55        issued_capabilities: Vec<CapabilityToken>,
56    ) -> Result<SessionId, KernelError> {
57        let session_id = generate_random_session_id();
58
59        self.open_session_with_id(session_id, agent_id, issued_capabilities)
60    }
61
62    pub fn open_session_with_id(
63        &self,
64        session_id: SessionId,
65        agent_id: AgentId,
66        issued_capabilities: Vec<CapabilityToken>,
67    ) -> Result<SessionId, KernelError> {
68        info!(session_id = %session_id, agent_id = %agent_id, "opening session");
69        let session = self.with_sessions_write(|sessions| {
70            let session = Arc::new(Session::new(
71                session_id.clone(),
72                agent_id,
73                issued_capabilities,
74            ));
75            match sessions.entry(session_id.clone()) {
76                Entry::Occupied(_) => Err(KernelError::SessionAlreadyExists(session_id.clone())),
77                Entry::Vacant(entry) => {
78                    entry.insert(Arc::clone(&session));
79                    Ok(session)
80                }
81            }
82        })?;
83        let session_snapshot = session.session_anchor_snapshot();
84        if let Err(error) = self.persist_session_anchor_snapshot(&session_snapshot, None) {
85            self.with_sessions_write(|sessions| {
86                sessions.remove(&session_id);
87                Ok(())
88            })?;
89            return Err(error);
90        }
91
92        Ok(session_id)
93    }
94
95    /// Transition a session into the `ready` state once setup is complete.
96    pub fn activate_session(&self, session_id: &SessionId) -> Result<(), KernelError> {
97        self.validate_web3_evidence_prerequisites()?;
98        self.with_session_mut(session_id, |session| {
99            session.activate()?;
100            Ok(())
101        })
102    }
103
104    /// Persist transport/session authentication context for a session.
105    pub fn set_session_auth_context(
106        &self,
107        session_id: &SessionId,
108        auth_context: SessionAuthContext,
109    ) -> Result<(), KernelError> {
110        self.with_session_mut(session_id, |session| {
111            session
112                .set_auth_context_persisted(auth_context, |session_snapshot, supersedes| {
113                    self.persist_session_anchor_snapshot(session_snapshot, supersedes)
114                })
115                .map_err(map_session_persist_error)
116        })
117    }
118
119    /// Persist peer capabilities negotiated at the edge for a session.
120    pub fn set_session_peer_capabilities(
121        &self,
122        session_id: &SessionId,
123        peer_capabilities: PeerCapabilities,
124    ) -> Result<(), KernelError> {
125        self.with_session_mut(session_id, |session| {
126            session.set_peer_capabilities(peer_capabilities);
127            Ok(())
128        })
129    }
130
131    /// Replace the session's current root snapshot.
132    pub fn replace_session_roots(
133        &self,
134        session_id: &SessionId,
135        roots: Vec<RootDefinition>,
136    ) -> Result<(), KernelError> {
137        self.with_session_mut(session_id, |session| {
138            session.replace_roots(roots);
139            Ok(())
140        })
141    }
142
143    /// Return the runtime's normalized root view for a session.
144    pub fn normalized_session_roots(
145        &self,
146        session_id: &SessionId,
147    ) -> Result<Vec<NormalizedRoot>, KernelError> {
148        self.with_session(session_id, |session| Ok(session.normalized_roots()))
149    }
150
151    /// Return only the enforceable filesystem root paths for a session.
152    pub fn enforceable_filesystem_root_paths(
153        &self,
154        session_id: &SessionId,
155    ) -> Result<Vec<String>, KernelError> {
156        self.with_session(session_id, |session| {
157            Ok(session
158                .enforceable_filesystem_roots()
159                .into_iter()
160                .filter_map(|root| root.normalized_filesystem_path().map(str::to_string))
161                .collect())
162        })
163    }
164
165    pub(crate) fn session_enforceable_filesystem_root_paths_owned(
166        &self,
167        session_id: &SessionId,
168    ) -> Result<Vec<String>, KernelError> {
169        self.with_session(session_id, |session| {
170            Ok(session
171                .enforceable_filesystem_roots()
172                .into_iter()
173                .filter_map(|root| root.normalized_filesystem_path().map(str::to_string))
174                .collect())
175        })
176    }
177
178    pub(crate) fn resource_path_within_root(candidate: &str, root: &str) -> bool {
179        if candidate == root {
180            return true;
181        }
182
183        if root == "/" {
184            return candidate.starts_with('/');
185        }
186
187        candidate
188            .strip_prefix(root)
189            .map(|suffix| suffix.starts_with('/'))
190            .unwrap_or(false)
191    }
192
193    pub(crate) fn resource_path_matches_session_roots(
194        path: &str,
195        session_roots: &[String],
196    ) -> bool {
197        if session_roots.is_empty() {
198            return false;
199        }
200
201        session_roots
202            .iter()
203            .any(|root| Self::resource_path_within_root(path, root))
204    }
205
206    pub(crate) fn enforce_resource_roots(
207        &self,
208        context: &OperationContext,
209        operation: &ReadResourceOperation,
210    ) -> Result<(), KernelError> {
211        match operation.classify_uri_for_runtime() {
212            ResourceUriClassification::NonFileSystem { .. } => Ok(()),
213            ResourceUriClassification::EnforceableFileSystem {
214                normalized_path, ..
215            } => {
216                let session_roots =
217                    self.session_enforceable_filesystem_root_paths_owned(&context.session_id)?;
218
219                if Self::resource_path_matches_session_roots(&normalized_path, &session_roots) {
220                    Ok(())
221                } else {
222                    let reason = if session_roots.is_empty() {
223                        "no enforceable filesystem roots are available for this session".to_string()
224                    } else {
225                        format!(
226                            "filesystem-backed resource path {normalized_path} is outside the negotiated roots"
227                        )
228                    };
229
230                    Err(KernelError::ResourceRootDenied {
231                        uri: operation.uri.clone(),
232                        reason,
233                    })
234                }
235            }
236            ResourceUriClassification::UnenforceableFileSystem { reason, .. } => {
237                Err(KernelError::ResourceRootDenied {
238                    uri: operation.uri.clone(),
239                    reason: format!(
240                        "filesystem-backed resource URI could not be enforced: {reason}"
241                    ),
242                })
243            }
244        }
245    }
246
247    pub(crate) fn build_resource_read_deny_receipt(
248        &self,
249        operation: &ReadResourceOperation,
250        reason: &str,
251    ) -> Result<ChioReceipt, KernelError> {
252        let receipt_content = receipt_content_for_output(None, None)?;
253        let action = ToolCallAction::from_parameters(serde_json::json!({
254            "uri": &operation.uri,
255        }))
256        .map_err(|error| {
257            KernelError::ReceiptSigningFailed(format!(
258                "failed to hash resource read parameters: {error}"
259            ))
260        })?;
261
262        let receipt = self.build_and_sign_receipt(ReceiptParams {
263            request_id: None,
264            capability_id: &operation.capability.id,
265            tool_name: "resources/read",
266            server_id: "session",
267            decision: Decision::Deny {
268                reason: reason.to_string(),
269                guard: "session_roots".to_string(),
270            },
271            action,
272            content_hash: receipt_content.content_hash,
273            canonical_content: receipt_content.canonical_content,
274            metadata: merge_metadata_objects(
275                Some(serde_json::json!({
276                    "resource": {
277                        "uri": &operation.uri,
278                    }
279                })),
280                receipt_attribution_metadata(&operation.capability, None),
281            ),
282            timestamp: current_unix_timestamp(),
283            trust_level: chio_core::receipt::kinds::TrustLevel::default(),
284            tenant_id: None,
285        })?;
286
287        self.record_chio_receipt(&receipt)?;
288        Ok(receipt)
289    }
290
291    /// Subscribe the session to update notifications for a concrete resource URI.
292    pub fn subscribe_session_resource(
293        &self,
294        session_id: &SessionId,
295        capability: &CapabilityToken,
296        agent_id: &str,
297        uri: &str,
298    ) -> Result<(), KernelError> {
299        self.validate_non_tool_capability(capability, agent_id)?;
300
301        if !capability_matches_resource_subscription(capability, uri)? {
302            return Err(KernelError::OutOfScopeResource {
303                uri: uri.to_string(),
304            });
305        }
306
307        if !self.resource_exists(uri)? {
308            return Err(KernelError::ResourceNotRegistered(uri.to_string()));
309        }
310
311        self.with_session_mut(session_id, |session| {
312            session.subscribe_resource(uri.to_string());
313            Ok(())
314        })
315    }
316
317    /// Remove a session-scoped resource subscription. Missing subscriptions are ignored.
318    pub fn unsubscribe_session_resource(
319        &self,
320        session_id: &SessionId,
321        uri: &str,
322    ) -> Result<(), KernelError> {
323        self.with_session_mut(session_id, |session| {
324            session.unsubscribe_resource(uri);
325            Ok(())
326        })
327    }
328
329    /// Check whether a session currently holds a resource subscription.
330    pub fn session_has_resource_subscription(
331        &self,
332        session_id: &SessionId,
333        uri: &str,
334    ) -> Result<bool, KernelError> {
335        self.with_session(
336            session_id,
337            |session| Ok(session.is_resource_subscribed(uri)),
338        )
339    }
340
341    /// Mark a session as draining. New tool calls are rejected after this point.
342    pub fn begin_draining_session(&self, session_id: &SessionId) -> Result<(), KernelError> {
343        self.with_session_mut(session_id, |session| {
344            session.begin_draining()?;
345            Ok(())
346        })
347    }
348
349    /// Close a session and clear transient session-scoped state.
350    pub fn close_session(&self, session_id: &SessionId) -> Result<(), KernelError> {
351        self.with_session_mut(session_id, |session| {
352            session
353                .close_persisted(|session_snapshot, supersedes| {
354                    self.persist_session_anchor_snapshot(session_snapshot, supersedes)
355                })
356                .map_err(map_session_persist_error)
357        })
358    }
359
360    /// Inspect an existing session.
361    pub fn session(&self, session_id: &SessionId) -> Option<Session> {
362        self.with_sessions_read(|sessions| {
363            Ok(sessions
364                .get(session_id)
365                .map(|session| session.value().as_ref().clone()))
366        })
367        .ok()
368        .flatten()
369    }
370
371    pub fn session_count(&self) -> usize {
372        self.with_sessions_read(|sessions| Ok(sessions.len()))
373            .unwrap_or(0)
374    }
375
376    pub fn resource_provider_count(&self) -> usize {
377        self.resource_providers.len()
378    }
379
380    pub fn prompt_provider_count(&self) -> usize {
381        self.prompt_providers.len()
382    }
383
384    /// Validate a session-scoped operation and register it as in flight.
385    pub fn begin_session_request(
386        &self,
387        context: &OperationContext,
388        operation_kind: OperationKind,
389        cancellable: bool,
390    ) -> Result<(), KernelError> {
391        let start = self.with_sessions_write(|sessions| {
392            begin_session_request_in_sessions(sessions, context, operation_kind, cancellable)
393        })?;
394        if let Err(error) = self.persist_request_lineage_snapshot(&start) {
395            let _ = self.with_sessions_write(|sessions| {
396                if let Ok(session) = session_from_map(sessions, &start.session.session_id) {
397                    session.discard_unpersisted_request_start(&start.lineage.request_id);
398                }
399                Ok(())
400            });
401            return Err(error);
402        }
403        Ok(())
404    }
405
406    fn begin_or_resume_execution_nonce_request(
407        &self,
408        context: &OperationContext,
409        operation_kind: OperationKind,
410        execution_nonce: Option<&crate::execution_nonce::SignedExecutionNonce>,
411    ) -> Result<(), KernelError> {
412        if let Some(nonce) = execution_nonce
413            .filter(|nonce| nonce.nonce.bound_to.request_id == context.request_id.as_str())
414        {
415            let resumed = self.with_sessions_read(|sessions| {
416                let session = session_from_map(sessions, &context.session_id)?;
417                if session.inflight().get(&context.request_id).is_some() {
418                    session.validate_execution_nonce_retry(
419                        context,
420                        operation_kind,
421                        nonce.nonce_id(),
422                    )?;
423                    return Ok(true);
424                }
425                if session.terminal().get(&context.request_id).is_some() {
426                    return Err(crate::session::SessionError::ExecutionNonceRetryMismatch {
427                        request_id: context.request_id.clone(),
428                    }
429                    .into());
430                }
431                Ok(false)
432            })?;
433            if resumed {
434                return Ok(());
435            }
436        }
437        self.begin_session_request(context, operation_kind, true)
438    }
439
440    fn finish_execution_nonce_request(
441        &self,
442        context: &OperationContext,
443        response: Option<&ToolCallResponse>,
444        terminal_state: OperationTerminalState,
445    ) -> Result<(), KernelError> {
446        if let Some(nonce) = response
447            .filter(|response| response.output.is_none())
448            .and_then(|response| response.execution_nonce.as_deref())
449        {
450            return self.with_sessions_write(|sessions| {
451                session_from_map(sessions, &context.session_id)?
452                    .mark_execution_nonce_pending(&context.request_id, nonce.nonce_id())?;
453                Ok(())
454            });
455        }
456        self.complete_session_request_with_terminal_state(
457            &context.session_id,
458            &context.request_id,
459            terminal_state,
460        )
461    }
462
463    /// Construct and register a child request under an existing parent request.
464    pub fn begin_child_request(
465        &self,
466        parent_context: &OperationContext,
467        request_id: RequestId,
468        operation_kind: OperationKind,
469        progress_token: Option<ProgressToken>,
470        cancellable: bool,
471    ) -> Result<OperationContext, KernelError> {
472        let (child_context, start) = self.with_sessions_write(|sessions| {
473            begin_child_request_in_sessions(
474                sessions,
475                parent_context,
476                request_id,
477                operation_kind,
478                progress_token,
479                cancellable,
480            )
481        })?;
482        if let Err(error) = self.persist_request_lineage_snapshot(&start) {
483            let _ = self.with_sessions_write(|sessions| {
484                if let Ok(session) = session_from_map(sessions, &start.session.session_id) {
485                    session.discard_unpersisted_request_start(&start.lineage.request_id);
486                }
487                Ok(())
488            });
489            return Err(error);
490        }
491        Ok(child_context)
492    }
493
494    /// Complete an in-flight session request.
495    pub fn complete_session_request(
496        &self,
497        session_id: &SessionId,
498        request_id: &RequestId,
499    ) -> Result<(), KernelError> {
500        self.complete_session_request_with_terminal_state(
501            session_id,
502            request_id,
503            OperationTerminalState::Completed,
504        )
505    }
506
507    /// Complete an in-flight session request with an explicit terminal state.
508    pub fn complete_session_request_with_terminal_state(
509        &self,
510        session_id: &SessionId,
511        request_id: &RequestId,
512        terminal_state: OperationTerminalState,
513    ) -> Result<(), KernelError> {
514        self.with_sessions_write(|sessions| {
515            complete_session_request_with_terminal_state_in_sessions(
516                sessions,
517                session_id,
518                request_id,
519                terminal_state,
520            )
521        })
522    }
523
524    fn signed_session_anchor_for_snapshot(
525        &self,
526        snapshot: &SessionAnchorSnapshot,
527    ) -> Result<chio_core::session::SessionAnchor, KernelError> {
528        let body = chio_core::session::SessionAnchorBody::new(
529            snapshot.session_anchor.id().to_string(),
530            chio_core::session::SessionAnchorContext::new(
531                snapshot.session_id.clone(),
532                snapshot.agent_id.clone(),
533                snapshot.auth_context.clone(),
534                chio_core::session::SessionProofBinding::from_auth_context(&snapshot.auth_context),
535            ),
536            snapshot.session_anchor.auth_epoch(),
537            snapshot.session_anchor.issued_at(),
538            self.config.keypair.public_key(),
539        )
540        .map_err(|error| {
541            KernelError::Internal(format!("failed to build session anchor body: {error}"))
542        })?;
543
544        chio_core::session::SessionAnchor::sign(body, &self.config.keypair).map_err(|error| {
545            KernelError::Internal(format!("failed to sign session anchor: {error}"))
546        })
547    }
548
549    fn persist_session_anchor_snapshot(
550        &self,
551        session: &SessionAnchorSnapshot,
552        supersedes_anchor_id: Option<&str>,
553    ) -> Result<(), KernelError> {
554        let anchor = self.signed_session_anchor_for_snapshot(session)?;
555        let anchor_json = serde_json::to_value(&anchor).map_err(|error| {
556            KernelError::Internal(format!("failed to serialize session anchor: {error}"))
557        })?;
558        self.with_receipt_store(|store| {
559            Ok(store.record_session_anchor(
560                session.session_id.as_str(),
561                &anchor.id,
562                &anchor.auth_context_hash,
563                anchor.issued_at,
564                supersedes_anchor_id,
565                &anchor_json,
566            )?)
567        })?;
568        Ok(())
569    }
570
571    fn persist_request_lineage_snapshot(
572        &self,
573        start: &SessionRequestStart,
574    ) -> Result<(), KernelError> {
575        let local_lineage = &start.lineage;
576        let anchor = self.signed_session_anchor_for_snapshot(&start.session)?;
577        let anchor_reference = anchor.reference().map_err(|error| {
578            KernelError::Internal(format!(
579                "failed to derive session anchor reference: {error}"
580            ))
581        })?;
582        let lineage_mode = if local_lineage.parent_request_id.is_some() {
583            chio_core::session::RequestLineageMode::LocalChild
584        } else {
585            chio_core::session::RequestLineageMode::Root
586        };
587        let mut lineage_record = chio_core::session::RequestLineageRecord::new(
588            local_lineage.request_id.clone(),
589            anchor_reference,
590            local_lineage.operation_kind,
591            lineage_mode,
592            local_lineage.started_at,
593        );
594        if let Some(parent_request_id) = local_lineage.parent_request_id.clone() {
595            lineage_record = lineage_record.with_parent_request_id(parent_request_id);
596        }
597        let lineage_json = serde_json::to_value(&lineage_record).map_err(|error| {
598            KernelError::Internal(format!("failed to serialize request lineage: {error}"))
599        })?;
600        self.with_receipt_store(|store| {
601            Ok(store.record_request_lineage(
602                start.session.session_id.as_str(),
603                local_lineage.request_id.as_str(),
604                local_lineage
605                    .parent_request_id
606                    .as_ref()
607                    .map(|value| value.as_str()),
608                Some(anchor.id.as_str()),
609                local_lineage.started_at,
610                None,
611                &lineage_json,
612            )?)
613        })?;
614        Ok(())
615    }
616
617    /// Mark an in-flight session request as cancelled.
618    pub fn request_session_cancellation(
619        &self,
620        session_id: &SessionId,
621        request_id: &RequestId,
622    ) -> Result<(), KernelError> {
623        self.with_session_mut(session_id, |session| {
624            session
625                .request_cancellation(request_id)
626                .map_err(KernelError::from)
627        })
628    }
629
630    /// Validate whether a sampling child request is allowed for this session.
631    pub fn validate_sampling_request(
632        &self,
633        context: &OperationContext,
634        operation: &CreateMessageOperation,
635    ) -> Result<(), KernelError> {
636        self.with_sessions_read(|sessions| {
637            validate_sampling_request_in_sessions(
638                sessions,
639                self.config.allow_sampling,
640                self.config.allow_sampling_tool_use,
641                context,
642                operation,
643            )
644        })
645    }
646
647    /// Validate whether an elicitation child request is allowed for this session.
648    pub fn validate_elicitation_request(
649        &self,
650        context: &OperationContext,
651        operation: &CreateElicitationOperation,
652    ) -> Result<(), KernelError> {
653        self.with_sessions_read(|sessions| {
654            validate_elicitation_request_in_sessions(
655                sessions,
656                self.config.allow_elicitation,
657                context,
658                operation,
659            )
660        })
661    }
662
663    /// Evaluate a session-scoped tool call while allowing the target tool server to proxy
664    /// negotiated nested flows back through a client transport owned by the edge.
665    pub fn evaluate_tool_call_operation_with_nested_flow_client<C: NestedFlowClient>(
666        &self,
667        context: &OperationContext,
668        operation: &ToolCallOperation,
669        client: &mut C,
670    ) -> Result<ToolCallResponse, KernelError> {
671        self.validate_web3_evidence_prerequisites()?;
672        let execution_nonce = parse_tool_call_operation_execution_nonce(operation)?;
673        self.begin_or_resume_execution_nonce_request(
674            context,
675            OperationKind::ToolCall,
676            execution_nonce.as_ref(),
677        )?;
678
679        let request = ToolCallRequest {
680            request_id: context.request_id.to_string(),
681            capability: operation.capability.clone(),
682            tool_name: operation.tool_name.clone(),
683            server_id: operation.server_id.clone(),
684            agent_id: context.agent_id.clone(),
685            arguments: operation.arguments.clone(),
686            dpop_proof: None,
687            execution_nonce,
688            governed_intent: operation.governed_intent.clone(),
689            approval_token: operation.approval_token.clone(),
690            approval_tokens: operation.approval_tokens.clone(),
691            threshold_approval_proposal: operation.threshold_approval_proposal.clone(),
692            supplemental_authorization: operation.supplemental_authorization.clone(),
693            model_metadata: operation.model_metadata.clone(),
694            federated_origin_kernel_id: None,
695        };
696
697        let result = self.evaluate_tool_call_with_nested_flow_client(
698            context,
699            &request,
700            client,
701            operation.extra_metadata.clone(),
702        );
703        let terminal_state = match &result {
704            Ok(response) => response.terminal_state.clone(),
705            Err(KernelError::RequestCancelled { request_id, reason })
706                if request_id == &context.request_id =>
707            {
708                self.with_session_mut(&context.session_id, |session| {
709                    session.request_cancellation(&context.request_id)?;
710                    Ok(())
711                })?;
712                OperationTerminalState::Cancelled {
713                    reason: reason.clone(),
714                }
715            }
716            _ => OperationTerminalState::Completed,
717        };
718        self.finish_execution_nonce_request(context, result.as_ref().ok(), terminal_state)?;
719        result
720    }
721
722    /// Async-native variant for hosts that already run inside a Tokio runtime.
723    ///
724    /// This path avoids the synchronous dispatch bridge, so current-thread
725    /// runtimes do not convert nested-flow tool calls into bridge errors. The
726    /// synchronous entrypoint remains for blocking edges and still fails before
727    /// side effects when a current-thread runtime is entered.
728    pub async fn evaluate_tool_call_operation_with_nested_flow_client_async<C: NestedFlowClient>(
729        &self,
730        context: &OperationContext,
731        operation: &ToolCallOperation,
732        client: &mut C,
733    ) -> Result<ToolCallResponse, KernelError> {
734        self.validate_web3_evidence_prerequisites()?;
735        let execution_nonce = parse_tool_call_operation_execution_nonce(operation)?;
736        self.begin_or_resume_execution_nonce_request(
737            context,
738            OperationKind::ToolCall,
739            execution_nonce.as_ref(),
740        )?;
741
742        let request = ToolCallRequest {
743            request_id: context.request_id.to_string(),
744            capability: operation.capability.clone(),
745            tool_name: operation.tool_name.clone(),
746            server_id: operation.server_id.clone(),
747            agent_id: context.agent_id.clone(),
748            arguments: operation.arguments.clone(),
749            dpop_proof: None,
750            execution_nonce,
751            governed_intent: operation.governed_intent.clone(),
752            approval_token: operation.approval_token.clone(),
753            approval_tokens: operation.approval_tokens.clone(),
754            threshold_approval_proposal: operation.threshold_approval_proposal.clone(),
755            supplemental_authorization: operation.supplemental_authorization.clone(),
756            model_metadata: operation.model_metadata.clone(),
757            federated_origin_kernel_id: None,
758        };
759
760        let result = self
761            .evaluate_tool_call_with_nested_flow_client_async(
762                context,
763                &request,
764                client,
765                operation.extra_metadata.clone(),
766            )
767            .await;
768        let terminal_state = match &result {
769            Ok(response) => response.terminal_state.clone(),
770            Err(KernelError::RequestCancelled { request_id, reason })
771                if request_id == &context.request_id =>
772            {
773                self.with_session_mut(&context.session_id, |session| {
774                    session.request_cancellation(&context.request_id)?;
775                    Ok(())
776                })?;
777                OperationTerminalState::Cancelled {
778                    reason: reason.clone(),
779                }
780            }
781            _ => OperationTerminalState::Completed,
782        };
783        self.finish_execution_nonce_request(context, result.as_ref().ok(), terminal_state)?;
784        result
785    }
786
787    /// Evaluate a normalized operation against a specific session.
788    ///
789    /// This is the higher-level entry point that future JSON-RPC or MCP edges
790    /// should target. The current stdio loop normalizes raw frames into these
791    /// operations before invoking the kernel.
792    pub fn evaluate_session_operation(
793        &self,
794        context: &OperationContext,
795        operation: &SessionOperation,
796    ) -> Result<SessionOperationResponse, KernelError> {
797        // Install tenant_id scope for the duration of this session-scoped
798        // evaluation so every receipt signed here (tool call, resource read
799        // deny, etc.) is tagged with the session's tenant. The ToolCall
800        // branch also installs a scope via its sync_with_session_context
801        // path; the nested scope is a no-op because the value matches, but
802        // it keeps non-tool-call branches (e.g. evaluate_resource_read)
803        // covered.
804        let tenant_id = self.resolve_tenant_id_for_session(Some(&context.session_id));
805        let _tenant_request_scope = self
806            .scope_receipt_tenant_id_for_request(context.request_id.as_str(), tenant_id.clone());
807        let _tenant_scope = scope_receipt_tenant_id(tenant_id);
808
809        self.validate_web3_evidence_prerequisites()?;
810        let operation_kind = operation.kind();
811        let should_track_inflight = matches!(
812            operation,
813            SessionOperation::ToolCall(_)
814                | SessionOperation::ReadResource(_)
815                | SessionOperation::GetPrompt(_)
816                | SessionOperation::Complete(_)
817        );
818        let parsed_tool_call_execution_nonce = match operation {
819            SessionOperation::ToolCall(tool_call) => {
820                parse_tool_call_operation_execution_nonce(tool_call)?
821            }
822            _ => None,
823        };
824
825        if should_track_inflight {
826            if matches!(operation, SessionOperation::ToolCall(_)) {
827                self.begin_or_resume_execution_nonce_request(
828                    context,
829                    operation_kind,
830                    parsed_tool_call_execution_nonce.as_ref(),
831                )?;
832            } else {
833                self.begin_session_request(context, operation_kind, true)?;
834            }
835        } else {
836            self.with_session_mut(&context.session_id, |session| {
837                session.validate_context(context)?;
838                session.ensure_operation_allowed(operation_kind)?;
839                Ok(())
840            })?;
841        }
842
843        let evaluation = match operation {
844            SessionOperation::ToolCall(tool_call) => {
845                let request = ToolCallRequest {
846                    request_id: context.request_id.to_string(),
847                    capability: tool_call.capability.clone(),
848                    tool_name: tool_call.tool_name.clone(),
849                    server_id: tool_call.server_id.clone(),
850                    agent_id: context.agent_id.clone(),
851                    arguments: tool_call.arguments.clone(),
852                    dpop_proof: None,
853                    execution_nonce: parsed_tool_call_execution_nonce,
854                    governed_intent: tool_call.governed_intent.clone(),
855                    approval_token: tool_call.approval_token.clone(),
856                    approval_tokens: tool_call.approval_tokens.clone(),
857                    threshold_approval_proposal: tool_call.threshold_approval_proposal.clone(),
858                    supplemental_authorization: tool_call.supplemental_authorization.clone(),
859                    model_metadata: tool_call.model_metadata.clone(),
860                    federated_origin_kernel_id: None,
861                };
862                let session_roots =
863                    self.session_enforceable_filesystem_root_paths_owned(&context.session_id)?;
864
865                // Pass the session_id so the evaluate path can resolve
866                // tenant_id from session.auth_context for every receipt
867                // signed during this tool call.
868                self.evaluate_tool_call_sync_with_session_context(
869                    &request,
870                    Some(session_roots.as_slice()),
871                    tool_call.extra_metadata.clone(),
872                    Some(&context.session_id),
873                )
874                .map(SessionOperationResponse::ToolCall)
875            }
876            SessionOperation::CreateMessage(_) => Err(KernelError::Internal(
877                "sampling/createMessage must be evaluated by an MCP edge with a client transport"
878                    .to_string(),
879            )),
880            SessionOperation::CreateElicitation(_) => Err(KernelError::Internal(
881                "elicitation/create must be evaluated by an MCP edge with a client transport"
882                    .to_string(),
883            )),
884            SessionOperation::ListRoots => {
885                let roots = self
886                    .session(&context.session_id)
887                    .ok_or_else(|| KernelError::UnknownSession(context.session_id.clone()))?
888                    .roots();
889                Ok(SessionOperationResponse::RootList { roots })
890            }
891            SessionOperation::ListResources => {
892                let resources = self
893                    .list_resources_for_session(&context.session_id)?
894                    .into_iter()
895                    .collect();
896                Ok(SessionOperationResponse::ResourceList { resources })
897            }
898            SessionOperation::ReadResource(resource_read) => {
899                self.evaluate_resource_read(context, resource_read)
900            }
901            SessionOperation::ListResourceTemplates => {
902                let templates = self.list_resource_templates_for_session(&context.session_id)?;
903                Ok(SessionOperationResponse::ResourceTemplateList { templates })
904            }
905            SessionOperation::ListPrompts => {
906                let prompts = self.list_prompts_for_session(&context.session_id)?;
907                Ok(SessionOperationResponse::PromptList { prompts })
908            }
909            SessionOperation::GetPrompt(prompt_get) => self
910                .evaluate_prompt_get(context, prompt_get)
911                .map(|prompt| SessionOperationResponse::PromptGet { prompt }),
912            SessionOperation::Complete(complete) => self
913                .evaluate_completion(context, complete)
914                .map(|completion| SessionOperationResponse::Completion { completion }),
915            SessionOperation::ListCapabilities => {
916                let capabilities = self
917                    .session(&context.session_id)
918                    .ok_or_else(|| KernelError::UnknownSession(context.session_id.clone()))?
919                    .capabilities()
920                    .to_vec();
921
922                Ok(SessionOperationResponse::CapabilityList { capabilities })
923            }
924            SessionOperation::Heartbeat => Ok(SessionOperationResponse::Heartbeat),
925        };
926
927        if should_track_inflight {
928            let terminal_state = match &evaluation {
929                Ok(SessionOperationResponse::ToolCall(response)) => response.terminal_state.clone(),
930                _ => OperationTerminalState::Completed,
931            };
932            let response = match &evaluation {
933                Ok(SessionOperationResponse::ToolCall(response)) => Some(response),
934                _ => None,
935            };
936            self.finish_execution_nonce_request(context, response, terminal_state)?;
937        }
938
939        evaluation
940    }
941
942    pub(crate) fn list_resources_for_session(
943        &self,
944        session_id: &SessionId,
945    ) -> Result<Vec<ResourceDefinition>, KernelError> {
946        let session = self
947            .session(session_id)
948            .ok_or_else(|| KernelError::UnknownSession(session_id.clone()))?;
949
950        let mut resources = Vec::new();
951        for provider in &self.resource_providers {
952            resources.extend(provider.list_resources().into_iter().filter(|resource| {
953                session.capabilities().iter().any(|capability| {
954                    capability_matches_resource_request(capability, &resource.uri).unwrap_or(false)
955                })
956            }));
957        }
958
959        Ok(resources)
960    }
961
962    pub(crate) fn resource_exists(&self, uri: &str) -> Result<bool, KernelError> {
963        for provider in &self.resource_providers {
964            if provider
965                .list_resources()
966                .iter()
967                .any(|resource| resource.uri == uri)
968            {
969                return Ok(true);
970            }
971
972            if provider.read_resource(uri)?.is_some() {
973                return Ok(true);
974            }
975        }
976
977        Ok(false)
978    }
979
980    pub(crate) fn list_resource_templates_for_session(
981        &self,
982        session_id: &SessionId,
983    ) -> Result<Vec<ResourceTemplateDefinition>, KernelError> {
984        let session = self
985            .session(session_id)
986            .ok_or_else(|| KernelError::UnknownSession(session_id.clone()))?;
987
988        let mut templates = Vec::new();
989        for provider in &self.resource_providers {
990            templates.extend(
991                provider
992                    .list_resource_templates()
993                    .into_iter()
994                    .filter(|template| {
995                        session.capabilities().iter().any(|capability| {
996                            capability_matches_resource_pattern(capability, &template.uri_template)
997                                .unwrap_or(false)
998                        })
999                    }),
1000            );
1001        }
1002
1003        Ok(templates)
1004    }
1005
1006    pub(crate) fn evaluate_resource_read(
1007        &self,
1008        context: &OperationContext,
1009        operation: &ReadResourceOperation,
1010    ) -> Result<SessionOperationResponse, KernelError> {
1011        self.validate_non_tool_capability(&operation.capability, &context.agent_id)?;
1012
1013        if !capability_matches_resource_request(&operation.capability, &operation.uri)? {
1014            return Err(KernelError::OutOfScopeResource {
1015                uri: operation.uri.clone(),
1016            });
1017        }
1018
1019        match self.enforce_resource_roots(context, operation) {
1020            Ok(()) => {}
1021            Err(KernelError::ResourceRootDenied { reason, .. }) => {
1022                let receipt = self.build_resource_read_deny_receipt(operation, &reason)?;
1023                return Ok(SessionOperationResponse::ResourceReadDenied { receipt });
1024            }
1025            Err(error) => return Err(error),
1026        }
1027
1028        for provider in &self.resource_providers {
1029            if let Some(contents) = provider.read_resource(&operation.uri)? {
1030                return Ok(SessionOperationResponse::ResourceRead { contents });
1031            }
1032        }
1033
1034        Err(KernelError::ResourceNotRegistered(operation.uri.clone()))
1035    }
1036
1037    pub(crate) fn list_prompts_for_session(
1038        &self,
1039        session_id: &SessionId,
1040    ) -> Result<Vec<PromptDefinition>, KernelError> {
1041        let session = self
1042            .session(session_id)
1043            .ok_or_else(|| KernelError::UnknownSession(session_id.clone()))?;
1044
1045        let mut prompts = Vec::new();
1046        for provider in &self.prompt_providers {
1047            prompts.extend(provider.list_prompts().into_iter().filter(|prompt| {
1048                session.capabilities().iter().any(|capability| {
1049                    capability_matches_prompt_request(capability, &prompt.name).unwrap_or(false)
1050                })
1051            }));
1052        }
1053
1054        Ok(prompts)
1055    }
1056
1057    pub(crate) fn evaluate_prompt_get(
1058        &self,
1059        context: &OperationContext,
1060        operation: &GetPromptOperation,
1061    ) -> Result<PromptResult, KernelError> {
1062        self.validate_non_tool_capability(&operation.capability, &context.agent_id)?;
1063
1064        if !capability_matches_prompt_request(&operation.capability, &operation.prompt_name)? {
1065            return Err(KernelError::OutOfScopePrompt {
1066                prompt: operation.prompt_name.clone(),
1067            });
1068        }
1069
1070        for provider in &self.prompt_providers {
1071            if let Some(prompt) =
1072                provider.get_prompt(&operation.prompt_name, operation.arguments.clone())?
1073            {
1074                return Ok(prompt);
1075            }
1076        }
1077
1078        Err(KernelError::PromptNotRegistered(
1079            operation.prompt_name.clone(),
1080        ))
1081    }
1082
1083    pub(crate) fn evaluate_completion(
1084        &self,
1085        context: &OperationContext,
1086        operation: &CompleteOperation,
1087    ) -> Result<CompletionResult, KernelError> {
1088        self.validate_non_tool_capability(&operation.capability, &context.agent_id)?;
1089
1090        match &operation.reference {
1091            CompletionReference::Prompt { name } => {
1092                if !capability_matches_prompt_request(&operation.capability, name)? {
1093                    return Err(KernelError::OutOfScopePrompt {
1094                        prompt: name.clone(),
1095                    });
1096                }
1097
1098                for provider in &self.prompt_providers {
1099                    if let Some(completion) = provider.complete_prompt_argument(
1100                        name,
1101                        &operation.argument.name,
1102                        &operation.argument.value,
1103                        &operation.context_arguments,
1104                    )? {
1105                        return Ok(completion);
1106                    }
1107                }
1108
1109                Err(KernelError::PromptNotRegistered(name.clone()))
1110            }
1111            CompletionReference::Resource { uri } => {
1112                if !capability_matches_resource_pattern(&operation.capability, uri)? {
1113                    return Err(KernelError::OutOfScopeResource { uri: uri.clone() });
1114                }
1115
1116                for provider in &self.resource_providers {
1117                    if let Some(completion) = provider.complete_resource_argument(
1118                        uri,
1119                        &operation.argument.name,
1120                        &operation.argument.value,
1121                        &operation.context_arguments,
1122                    )? {
1123                        return Ok(completion);
1124                    }
1125                }
1126
1127                Err(KernelError::ResourceNotRegistered(uri.clone()))
1128            }
1129        }
1130    }
1131}