Skip to main content

chio_kernel/
runtime.rs

1use chio_core::capability::{
2    CapabilityToken, GovernedApprovalToken, GovernedTransactionIntent, ModelMetadata,
3};
4use chio_core::receipt::ChioReceipt;
5use chio_core::session::{
6    CreateElicitationOperation, CreateElicitationResult, CreateMessageOperation,
7    CreateMessageResult, OperationContext, OperationTerminalState, RequestId, RootDefinition,
8};
9
10use crate::dpop;
11use crate::execution_nonce::SignedExecutionNonce;
12use crate::{AgentId, KernelError, ServerId};
13
14/// Verdict of a guard or capability evaluation.
15///
16/// This is the kernel's own verdict type, distinct from `chio_core::Decision`.
17/// The kernel uses this internally; it maps to `chio_core::Decision` when
18/// building receipts.
19///
20/// Phase 3.4 introduced the `PendingApproval` variant. The variant is a
21/// marker: the payload (`ApprovalRequest`) is returned separately via
22/// [`crate::approval::HitlVerdict`] so existing call sites that pattern-
23/// match on `Verdict` and rely on its `Copy` semantics keep compiling
24/// without change. The public contract therefore remains: `Allow`,
25/// `Deny`, and `PendingApproval` are the three possible outcomes of
26/// guard evaluation, and callers receive the full approval request via
27/// the richer HITL API surface when they need it.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum Verdict {
30    /// The action is allowed.
31    Allow,
32    /// The action is denied.
33    Deny,
34    /// The action is suspended pending a human decision. Look up the
35    /// associated `ApprovalRequest` via the HITL API.
36    PendingApproval,
37}
38
39/// A tool call request as seen by the kernel.
40#[derive(Debug)]
41pub struct ToolCallRequest {
42    /// Unique request identifier.
43    pub request_id: String,
44    /// The signed capability token authorizing this call.
45    pub capability: CapabilityToken,
46    /// The tool to invoke.
47    pub tool_name: String,
48    /// The server hosting the tool.
49    pub server_id: ServerId,
50    /// The calling agent's identifier (hex-encoded public key).
51    pub agent_id: AgentId,
52    /// Tool arguments.
53    pub arguments: serde_json::Value,
54    /// Optional DPoP proof. Required when the matched grant has `dpop_required == Some(true)`.
55    pub dpop_proof: Option<dpop::DpopProof>,
56    /// Optional governed transaction intent bound to this invocation.
57    pub governed_intent: Option<GovernedTransactionIntent>,
58    /// Optional approval token authorizing this governed invocation.
59    pub approval_token: Option<GovernedApprovalToken>,
60    /// Optional metadata describing the model executing the calling
61    /// agent. Consumed by `Constraint::ModelConstraint` enforcement.
62    ///
63    /// Absent in legacy callers; when the matched grant carries a
64    /// `ModelConstraint` with any requirement, the call is denied.
65    pub model_metadata: Option<ModelMetadata>,
66    /// Phase 20.3: identifier of the origin kernel when this request
67    /// crosses a federation boundary (agent in Org A invoking a tool in
68    /// Org B). When set, the local (tool-host) kernel dispatches the
69    /// signed receipt to the origin kernel for bilateral co-signing
70    /// before the receipt is persisted. Absent for intra-org calls.
71    ///
72    /// The field is skipped from wire serialization when `None` so the
73    /// legacy wire format stays byte-identical.
74    pub federated_origin_kernel_id: Option<String>,
75}
76
77/// The kernel's response to a tool call request.
78///
79/// Phase 1.1 added `execution_nonce` as a sibling field so the `Verdict`
80/// enum can keep its `Copy` semantics. The nonce is only populated for
81/// `Verdict::Allow` and only when the kernel has an `ExecutionNonceConfig`
82/// installed; non-allow responses and nonce-disabled deployments continue
83/// to carry `None` here.
84#[derive(Debug)]
85pub struct ToolCallResponse {
86    /// Correlation identifier (matches the request).
87    pub request_id: String,
88    /// The kernel's verdict.
89    pub verdict: Verdict,
90    /// The tool's output payload, which may be a direct value or a stream.
91    pub output: Option<ToolCallOutput>,
92    /// Denial reason (populated when verdict is Deny).
93    pub reason: Option<String>,
94    /// Explicit terminal lifecycle state for this request.
95    pub terminal_state: OperationTerminalState,
96    /// Signed receipt attesting to this decision.
97    pub receipt: ChioReceipt,
98    /// Phase 1.1: short-lived, single-use execution nonce bound to this
99    /// allow verdict. Populated only on `Verdict::Allow` when an
100    /// `ExecutionNonceConfig` is installed on the kernel. Legacy
101    /// deployments without a config leave this `None` and keep working.
102    ///
103    /// Boxed so the deny/cancel/incomplete hot paths (which all carry
104    /// `None`) don't widen the `SessionOperationResponse::ToolCall`
105    /// variant and trip clippy's `large_enum_variant`.
106    pub execution_nonce: Option<Box<SignedExecutionNonce>>,
107}
108
109/// Streamed tool output emitted before the final tool response frame.
110#[derive(Debug, Clone, PartialEq)]
111pub struct ToolCallChunk {
112    pub data: serde_json::Value,
113}
114
115/// Complete streamed output captured by the kernel.
116#[derive(Debug, Clone, PartialEq)]
117pub struct ToolCallStream {
118    pub chunks: Vec<ToolCallChunk>,
119}
120
121impl ToolCallStream {
122    pub fn chunk_count(&self) -> u64 {
123        self.chunks.len() as u64
124    }
125}
126
127/// Output produced by a tool invocation.
128#[derive(Debug, Clone, PartialEq)]
129pub enum ToolCallOutput {
130    Value(serde_json::Value),
131    Stream(ToolCallStream),
132}
133
134/// Stream-capable tool-server result.
135#[derive(Debug, Clone, PartialEq)]
136pub enum ToolServerStreamResult {
137    Complete(ToolCallStream),
138    Incomplete {
139        stream: ToolCallStream,
140        reason: String,
141    },
142}
143
144/// Tool-server output produced after validation and guard checks.
145#[derive(Debug, Clone, PartialEq)]
146pub enum ToolServerOutput {
147    Value(serde_json::Value),
148    Stream(ToolServerStreamResult),
149}
150
151/// Bridge exposed to tool-server implementations while a parent request is in flight.
152///
153/// Wrapped servers can use this to trigger negotiated server-to-client requests such as
154/// `roots/list` and `sampling/createMessage`, or to surface wrapped MCP notifications,
155/// without escaping kernel mediation.
156pub trait NestedFlowBridge {
157    fn parent_request_id(&self) -> &RequestId;
158
159    fn poll_parent_cancellation(&mut self) -> Result<(), KernelError> {
160        Ok(())
161    }
162
163    fn list_roots(&mut self) -> Result<Vec<RootDefinition>, KernelError>;
164
165    fn create_message(
166        &mut self,
167        operation: CreateMessageOperation,
168    ) -> Result<CreateMessageResult, KernelError>;
169
170    fn create_elicitation(
171        &mut self,
172        operation: CreateElicitationOperation,
173    ) -> Result<CreateElicitationResult, KernelError>;
174
175    fn notify_elicitation_completed(&mut self, elicitation_id: &str) -> Result<(), KernelError>;
176
177    fn notify_resource_updated(&mut self, uri: &str) -> Result<(), KernelError>;
178
179    fn notify_resources_list_changed(&mut self) -> Result<(), KernelError>;
180}
181
182/// Raw client transport used by the kernel to service nested flows on behalf of a parent request.
183///
184/// The kernel owns lineage, policy, and in-flight bookkeeping. Implementors only move the nested
185/// request or notification across the client transport and return the decoded response.
186pub trait NestedFlowClient {
187    fn poll_parent_cancellation(
188        &mut self,
189        _parent_context: &OperationContext,
190    ) -> Result<(), KernelError> {
191        Ok(())
192    }
193
194    fn list_roots(
195        &mut self,
196        parent_context: &OperationContext,
197        child_context: &OperationContext,
198    ) -> Result<Vec<RootDefinition>, KernelError>;
199
200    fn create_message(
201        &mut self,
202        parent_context: &OperationContext,
203        child_context: &OperationContext,
204        operation: &CreateMessageOperation,
205    ) -> Result<CreateMessageResult, KernelError>;
206
207    fn create_elicitation(
208        &mut self,
209        parent_context: &OperationContext,
210        child_context: &OperationContext,
211        operation: &CreateElicitationOperation,
212    ) -> Result<CreateElicitationResult, KernelError>;
213
214    fn notify_elicitation_completed(
215        &mut self,
216        parent_context: &OperationContext,
217        elicitation_id: &str,
218    ) -> Result<(), KernelError>;
219
220    fn notify_resource_updated(
221        &mut self,
222        parent_context: &OperationContext,
223        uri: &str,
224    ) -> Result<(), KernelError>;
225
226    fn notify_resources_list_changed(
227        &mut self,
228        parent_context: &OperationContext,
229    ) -> Result<(), KernelError>;
230}
231
232/// Cost reported by a tool server after invocation.
233///
234/// Tool servers that track monetary costs override `invoke_with_cost` and
235/// return this struct. Servers that do not override return `None` via the
236/// default implementation, and the kernel charges `max_cost_per_invocation`
237/// as a worst-case debit.
238#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
239pub struct ToolInvocationCost {
240    /// Cost in the currency's smallest unit (e.g. cents for USD).
241    pub units: u64,
242    /// ISO 4217 currency code.
243    pub currency: String,
244    /// Optional cost breakdown for audit.
245    #[serde(default, skip_serializing_if = "Option::is_none")]
246    pub breakdown: Option<serde_json::Value>,
247}
248
249/// Trait representing a connection to a tool server.
250///
251/// The kernel holds one `ToolServerConnection` per registered server. In
252/// production this is an mTLS connection over UDS or TCP. For testing,
253/// an in-process implementation can be used.
254pub trait ToolServerConnection: Send + Sync {
255    /// The server's unique identifier.
256    fn server_id(&self) -> &str;
257
258    /// List the tool names available on this server.
259    fn tool_names(&self) -> Vec<String>;
260
261    /// Invoke a tool on this server. The kernel has already validated the
262    /// capability and run guards before calling this.
263    fn invoke(
264        &self,
265        tool_name: &str,
266        arguments: serde_json::Value,
267        nested_flow_bridge: Option<&mut dyn NestedFlowBridge>,
268    ) -> Result<serde_json::Value, KernelError>;
269
270    /// Invoke a tool and optionally report the actual cost of the invocation.
271    ///
272    /// Tool servers that track monetary costs should override this method.
273    /// The default implementation delegates to `invoke` and returns `None`
274    /// cost, meaning the kernel will charge `max_cost_per_invocation` as
275    /// the worst-case debit.
276    fn invoke_with_cost(
277        &self,
278        tool_name: &str,
279        arguments: serde_json::Value,
280        nested_flow_bridge: Option<&mut dyn NestedFlowBridge>,
281    ) -> Result<(serde_json::Value, Option<ToolInvocationCost>), KernelError> {
282        let value = self.invoke(tool_name, arguments, nested_flow_bridge)?;
283        Ok((value, None))
284    }
285
286    /// Invoke a tool that can emit multiple streamed chunks before its final terminal state.
287    ///
288    /// Servers that do not support streaming can ignore this and rely on `invoke`.
289    fn invoke_stream(
290        &self,
291        tool_name: &str,
292        arguments: serde_json::Value,
293        nested_flow_bridge: Option<&mut dyn NestedFlowBridge>,
294    ) -> Result<Option<ToolServerStreamResult>, KernelError> {
295        let _ = (tool_name, arguments, nested_flow_bridge);
296        Ok(None)
297    }
298
299    /// Drain asynchronous events emitted after a tool invocation has already returned.
300    ///
301    /// Native tool servers can use this to surface late URL-elicitation completions and
302    /// catalog/resource notifications without depending on a still-live request-local bridge.
303    fn drain_events(&self) -> Result<Vec<ToolServerEvent>, KernelError> {
304        Ok(vec![])
305    }
306}
307
308#[derive(Debug, Clone, PartialEq, Eq)]
309pub enum ToolServerEvent {
310    ElicitationCompleted { elicitation_id: String },
311    ResourceUpdated { uri: String },
312    ResourcesListChanged,
313    ToolsListChanged,
314    PromptsListChanged,
315}