Skip to main content

chio_kernel_core/
evaluate.rs

1//! Pure-compute verdict evaluation.
2//!
3//! [`evaluate`] walks a `(capability, request, guards)` tuple through the
4//! sync checks that do not require I/O or mutable kernel state:
5//!
6//! 1. Issuer trust + signature + time-bound verification via
7//!    [`crate::capability_verify::verify_capability`].
8//! 2. Subject-binding check (agent_id == capability.subject hex).
9//! 3. Portable scope match via [`crate::scope::resolve_matching_grants`].
10//! 4. Guard pipeline: every registered guard is invoked in order;
11//!    fail-closed on error or `Deny`.
12//!
13//! What it does NOT do (fenced into `chio-kernel` proper):
14//!
15//! - Revocation membership lookup (stateful `RevocationStore`).
16//! - Budget mutation (stateful `BudgetStore`).
17//! - Delegation-chain ancestor inspection against the receipt store.
18//! - DPoP proof verification with nonce replay (LRU-backed).
19//! - Governed-transaction policy evaluation (pulls in chio-governance).
20//! - Payment authorisation (async adapter trait).
21//! - Tool dispatch to wrapped servers (async transport).
22//! - Receipt persistence / Merkle checkpointing (SQL / IO).
23//!
24//! The caller -- today `chio-kernel::ChioKernel::evaluate_tool_call_sync` and
25//! tomorrow `chio-kernel-wasm::BrowserKernel::evaluate` -- wraps this pure
26//! core in the I/O checks it needs.
27//!
28//! Verified-core boundary note:
29//! `formal/proof-manifest.toml` names this module as covered Rust surface for
30//! the current bounded verified core. The covered semantics stop at pure
31//! capability verification, subject binding, portable scope matching, and the
32//! synchronous guard pipeline; revocation lookups, budget mutation, DPoP, and
33//! tool dispatch stay outside this module and outside the present proof claim.
34
35use alloc::string::{String, ToString};
36use alloc::vec::Vec;
37
38use chio_core_types::capability::CapabilityToken;
39use chio_core_types::crypto::PublicKey;
40
41use crate::capability_verify::{verify_capability, CapabilityError, VerifiedCapability};
42use crate::clock::Clock;
43use crate::guard::{Guard, GuardContext, PortableToolCallRequest};
44use crate::normalized::{NormalizationError, NormalizedEvaluationVerdict};
45use crate::scope::{resolve_matching_grants, MatchedGrant};
46use crate::Verdict;
47
48/// Inputs to [`evaluate`]. Grouped into a struct so the call site stays
49/// tidy and future fields (e.g. a policy-digest override) can be added
50/// without breaking the public signature.
51pub struct EvaluateInput<'a> {
52    /// Tool call request being evaluated.
53    pub request: &'a PortableToolCallRequest,
54    /// The capability token authorising this call.
55    pub capability: &'a CapabilityToken,
56    /// Trusted issuer public keys (typically CA + kernel + authority).
57    pub trusted_issuers: &'a [PublicKey],
58    /// Clock used for time-bound enforcement.
59    pub clock: &'a dyn Clock,
60    /// Guard pipeline. Evaluated in order, fail-closed on deny or error.
61    pub guards: &'a [&'a dyn Guard],
62    /// Optional filesystem roots from the owning session, passed through to
63    /// guards that enforce root-based resource protection.
64    pub session_filesystem_roots: Option<&'a [String]>,
65}
66
67/// Verdict + context produced by [`evaluate`].
68///
69/// On `Verdict::Allow` the caller is handed the `VerifiedCapability` and
70/// the matched grant index; the full kernel uses those to drive budget
71/// accounting, receipt construction, and tool dispatch.
72#[derive(Debug, Clone)]
73pub struct EvaluationVerdict {
74    /// The three-valued verdict. `PendingApproval` is never produced by
75    /// the core; only Allow / Deny flow out of this module.
76    pub verdict: Verdict,
77    /// Human-readable deny reason when `verdict == Deny`.
78    pub reason: Option<String>,
79    /// Grant index that admitted the request. Populated on Allow.
80    pub matched_grant_index: Option<usize>,
81    /// Verified capability snapshot. Populated when signature + time
82    /// checks succeeded, even if a later guard denied.
83    pub verified: Option<VerifiedCapability>,
84}
85
86impl EvaluationVerdict {
87    /// Is this an allow verdict?
88    #[must_use]
89    pub fn is_allow(&self) -> bool {
90        self.verdict == Verdict::Allow
91    }
92
93    /// Is this a deny verdict?
94    #[must_use]
95    pub fn is_deny(&self) -> bool {
96        self.verdict == Verdict::Deny
97    }
98
99    /// Project this evaluation result into the proof-facing normalized AST.
100    pub fn normalized(
101        &self,
102        request: &PortableToolCallRequest,
103    ) -> Result<NormalizedEvaluationVerdict, NormalizationError> {
104        NormalizedEvaluationVerdict::try_from_evaluation(request, self)
105    }
106}
107
108/// Errors the portable core can raise.
109///
110/// These are portable-kernel equivalents of the legacy
111/// `chio_kernel::KernelError` variants that can be produced without any
112/// I/O. The caller in `chio-kernel` maps them back onto its richer
113/// `KernelError` surface for backward compatibility.
114#[derive(Debug, Clone, PartialEq, Eq)]
115pub enum KernelCoreError {
116    /// Capability signature or issuer trust failed.
117    InvalidCapability(CapabilityError),
118    /// Subject mismatch: `request.agent_id` != `capability.subject`.
119    SubjectMismatch { expected: String, actual: String },
120    /// No grant in scope covers the requested tool/server.
121    OutOfScope { tool: String, server: String },
122    /// Portable scope matching failed closed on an unsupported constraint.
123    ConstraintError { reason: String },
124    /// A guard returned a fail-closed error.
125    GuardError { guard: String, reason: String },
126    /// A guard denied the request outright.
127    GuardDenied { guard: String },
128}
129
130impl KernelCoreError {
131    /// Human-readable reason for the deny verdict.
132    #[must_use]
133    pub fn deny_reason(&self) -> String {
134        match self {
135            KernelCoreError::InvalidCapability(error) => match error {
136                CapabilityError::UntrustedIssuer => {
137                    "capability issuer is not a trusted CA".to_string()
138                }
139                CapabilityError::InvalidSignature => "capability signature is invalid".to_string(),
140                CapabilityError::NotYetValid => "capability not yet valid".to_string(),
141                CapabilityError::Expired => "capability has expired".to_string(),
142                CapabilityError::Internal(msg) => {
143                    let mut out = String::from("capability verification failed: ");
144                    out.push_str(msg);
145                    out
146                }
147            },
148            KernelCoreError::SubjectMismatch { expected, actual } => {
149                let mut out = String::from("request agent ");
150                out.push_str(actual);
151                out.push_str(" does not match capability subject ");
152                out.push_str(expected);
153                out
154            }
155            KernelCoreError::OutOfScope { tool, server } => {
156                let mut out = String::from("requested tool ");
157                out.push_str(tool);
158                out.push_str(" on server ");
159                out.push_str(server);
160                out.push_str(" is not in capability scope");
161                out
162            }
163            KernelCoreError::ConstraintError { reason } => {
164                let mut out = String::from("constraint evaluation failed: ");
165                out.push_str(reason);
166                out
167            }
168            KernelCoreError::GuardError { guard, reason } => {
169                let mut out = String::from("guard \"");
170                out.push_str(guard);
171                out.push_str("\" error (fail-closed): ");
172                out.push_str(reason);
173                out
174            }
175            KernelCoreError::GuardDenied { guard } => {
176                let mut out = String::from("guard \"");
177                out.push_str(guard);
178                out.push_str("\" denied the request");
179                out
180            }
181        }
182    }
183}
184
185/// Primary entry point for the portable kernel core.
186///
187/// Performs in order:
188///
189/// 1. Capability signature / issuer / time-bound verification.
190/// 2. Subject binding (agent_id match).
191/// 3. Portable scope match.
192/// 4. Guard pipeline (fail-closed).
193///
194/// Returns `Ok(EvaluationVerdict)` for Allow or Deny. An `Err` is only
195/// returned when the underlying `verify_canonical` machinery reports an
196/// internal failure that is not a clean verify-false; semantically this
197/// is still a deny at the caller's level and chio-kernel maps it onto
198/// `KernelError::Internal`.
199pub fn evaluate(input: EvaluateInput<'_>) -> EvaluationVerdict {
200    // Step 1: capability verification.
201    let verified = match verify_capability(input.capability, input.trusted_issuers, input.clock) {
202        Ok(verified) => verified,
203        Err(error) => {
204            let core_err = KernelCoreError::InvalidCapability(error);
205            return deny(core_err, None, None);
206        }
207    };
208
209    // Step 2: subject binding.
210    if verified.subject_hex != input.request.agent_id {
211        let core_err = KernelCoreError::SubjectMismatch {
212            expected: verified.subject_hex.clone(),
213            actual: input.request.agent_id.clone(),
214        };
215        return deny(core_err, None, Some(verified));
216    }
217
218    // Step 3: scope match.
219    let matches: Vec<MatchedGrant<'_>> = match resolve_matching_grants(
220        &verified.scope,
221        &input.request.tool_name,
222        &input.request.server_id,
223        &input.request.arguments,
224    ) {
225        Ok(matches) if matches.is_empty() => {
226            let core_err = KernelCoreError::OutOfScope {
227                tool: input.request.tool_name.clone(),
228                server: input.request.server_id.clone(),
229            };
230            return deny(core_err, None, Some(verified));
231        }
232        Ok(matches) => matches,
233        Err(crate::ScopeMatchError::OutOfScope) => {
234            let core_err = KernelCoreError::OutOfScope {
235                tool: input.request.tool_name.clone(),
236                server: input.request.server_id.clone(),
237            };
238            return deny(core_err, None, Some(verified));
239        }
240        Err(crate::ScopeMatchError::ConstraintError(reason)) => {
241            return deny(
242                KernelCoreError::ConstraintError { reason },
243                None,
244                Some(verified),
245            );
246        }
247    };
248    // Safe: guarded above.
249    let matched_grant_index = matches[0].index;
250
251    // Step 4: guard pipeline.
252    let ctx = GuardContext {
253        request: input.request,
254        scope: &verified.scope,
255        agent_id: &input.request.agent_id,
256        server_id: &input.request.server_id,
257        session_filesystem_roots: input.session_filesystem_roots,
258        matched_grant_index: Some(matched_grant_index),
259    };
260
261    for guard in input.guards {
262        match guard.evaluate(&ctx) {
263            Ok(Verdict::Allow) => {}
264            Ok(Verdict::Deny) | Ok(Verdict::PendingApproval) => {
265                // PendingApproval is reserved for the full kernel orchestration
266                // layer (chio-kernel::approval::ApprovalGuard); if a legacy sync
267                // guard surfaces it here we fail closed.
268                let core_err = KernelCoreError::GuardDenied {
269                    guard: guard.name().to_string(),
270                };
271                return deny(core_err, Some(matched_grant_index), Some(verified));
272            }
273            Err(error) => {
274                let core_err = KernelCoreError::GuardError {
275                    guard: guard.name().to_string(),
276                    reason: error.deny_reason(),
277                };
278                return deny(core_err, Some(matched_grant_index), Some(verified));
279            }
280        }
281    }
282
283    EvaluationVerdict {
284        verdict: Verdict::Allow,
285        reason: None,
286        matched_grant_index: Some(matched_grant_index),
287        verified: Some(verified),
288    }
289}
290
291fn deny(
292    error: KernelCoreError,
293    matched_grant_index: Option<usize>,
294    verified: Option<VerifiedCapability>,
295) -> EvaluationVerdict {
296    EvaluationVerdict {
297        verdict: Verdict::Deny,
298        reason: Some(error.deny_reason()),
299        matched_grant_index,
300        verified,
301    }
302}