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}