use alloc::string::{String, ToString};
use chio_core_types::capability::{
crypto_floor::CapabilityCryptoFloor, features::CapabilityNegotiation, scope::ChioScope,
token::CapabilityToken,
};
use chio_core_types::crypto::PublicKey;
use crate::budget_split::{BudgetRegistry, NoopBudgetRegistry};
use crate::capability_verify::{
admit_delegated_budget, verify_capability_with_floor, CapabilityError, TrustRootResolver,
VerifiedCapability,
};
use crate::clock::Clock;
use crate::guard::{Guard, GuardContext, PortableToolCallRequest};
use crate::normalized::{NormalizationError, NormalizedEvaluationVerdict};
use crate::scope::resolve_matching_grants;
use crate::Verdict;
pub struct EvaluateInput<'a> {
pub request: &'a PortableToolCallRequest,
pub capability: &'a CapabilityToken,
pub trusted_issuers: &'a [PublicKey],
pub clock: &'a dyn Clock,
pub guards: &'a [&'a dyn Guard],
pub session_filesystem_roots: Option<&'a [String]>,
}
#[derive(Debug, Clone)]
pub struct EvaluationVerdict {
pub verdict: Verdict,
pub reason: Option<String>,
pub matched_grant_index: Option<usize>,
pub verified: Option<VerifiedCapability>,
}
impl EvaluationVerdict {
#[must_use]
pub fn is_allow(&self) -> bool {
self.verdict == Verdict::Allow
}
#[must_use]
pub fn is_deny(&self) -> bool {
self.verdict == Verdict::Deny
}
pub fn normalized(
&self,
request: &PortableToolCallRequest,
) -> Result<NormalizedEvaluationVerdict, NormalizationError> {
NormalizedEvaluationVerdict::try_from_evaluation(request, self)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum KernelCoreError {
InvalidCapability(CapabilityError),
SubjectMismatch { expected: String, actual: String },
OutOfScope { tool: String, server: String },
ConstraintError { reason: String },
UnsupportedCapabilityFeature { feature: String },
GuardError { guard: String, reason: String },
GuardDenied { guard: String },
}
impl KernelCoreError {
#[must_use]
pub fn deny_reason(&self) -> String {
match self {
KernelCoreError::InvalidCapability(error) => match error {
CapabilityError::UntrustedIssuer => {
"capability issuer is not a trusted CA".to_string()
}
CapabilityError::InvalidSignature => "capability signature is invalid".to_string(),
CapabilityError::CryptoFloorRejected(msg) => {
let mut out = String::from("capability rejected by crypto floor: ");
out.push_str(msg);
out
}
CapabilityError::NotYetValid => "capability not yet valid".to_string(),
CapabilityError::Expired => "capability has expired".to_string(),
CapabilityError::AttenuationViolation(msg) => {
let mut out = String::from("capability rejected by chain binding: ");
out.push_str(msg);
out
}
CapabilityError::BudgetSplitRejected(err) => {
let mut out = String::from("capability budget split rejected: ");
let formatted = alloc::format!("{err}");
out.push_str(&formatted);
out
}
CapabilityError::Internal(msg) => {
let mut out = String::from("capability verification failed: ");
out.push_str(msg);
out
}
},
KernelCoreError::SubjectMismatch { expected, actual } => {
let mut out = String::from("request agent ");
out.push_str(actual);
out.push_str(" does not match capability subject ");
out.push_str(expected);
out
}
KernelCoreError::OutOfScope { tool, server } => {
let mut out = String::from("requested tool ");
out.push_str(tool);
out.push_str(" on server ");
out.push_str(server);
out.push_str(" is not in capability scope");
out
}
KernelCoreError::ConstraintError { reason } => {
let mut out = String::from("constraint evaluation failed: ");
out.push_str(reason);
out
}
KernelCoreError::UnsupportedCapabilityFeature { feature } => {
let mut out = String::from("capability feature unsupported on this runtime: ");
out.push_str(feature);
out
}
KernelCoreError::GuardError { guard, reason } => {
let mut out = String::from("guard \"");
out.push_str(guard);
out.push_str("\" error (fail-closed): ");
out.push_str(reason);
out
}
KernelCoreError::GuardDenied { guard } => {
let mut out = String::from("guard \"");
out.push_str(guard);
out.push_str("\" denied the request");
out
}
}
}
}
fn out_of_scope_error(request: &PortableToolCallRequest) -> KernelCoreError {
KernelCoreError::OutOfScope {
tool: request.tool_name.clone(),
server: request.server_id.clone(),
}
}
fn resolve_matched_grant_index(
scope: &ChioScope,
request: &PortableToolCallRequest,
) -> Result<usize, KernelCoreError> {
let matches = match resolve_matching_grants(
scope,
&request.tool_name,
&request.server_id,
&request.arguments,
) {
Ok(matches) => matches,
Err(crate::ScopeMatchError::OutOfScope) => return Err(out_of_scope_error(request)),
Err(crate::ScopeMatchError::ConstraintError(reason)) => {
return Err(KernelCoreError::ConstraintError { reason });
}
};
matches
.first()
.map(|matched| matched.index)
.ok_or_else(|| out_of_scope_error(request))
}
pub fn evaluate(input: EvaluateInput<'_>) -> EvaluationVerdict {
evaluate_with_crypto_floor(input, CapabilityCryptoFloor::AllowClassical)
}
pub fn evaluate_with_crypto_floor(
input: EvaluateInput<'_>,
crypto_floor: CapabilityCryptoFloor,
) -> EvaluationVerdict {
let mut budgets = NoopBudgetRegistry;
evaluate_with_crypto_floor_and_budgets(input, crypto_floor, &mut budgets)
}
pub fn evaluate_with_crypto_floor_and_budgets(
input: EvaluateInput<'_>,
crypto_floor: CapabilityCryptoFloor,
budgets: &mut dyn BudgetRegistry,
) -> EvaluationVerdict {
if input.capability.attenuation_proof.is_some() {
let core_err = KernelCoreError::InvalidCapability(CapabilityError::AttenuationViolation(
"chain-binding requires a trust-root resolver on the evaluate path".to_string(),
));
return deny(core_err, None, None);
}
let mut verify_only_budgets = NoopBudgetRegistry;
let verified = match verify_capability_with_floor(
input.capability,
input.trusted_issuers,
input.clock,
crypto_floor,
&mut verify_only_budgets,
) {
Ok(verified) => verified,
Err(error) => {
let core_err = KernelCoreError::InvalidCapability(error);
return deny(core_err, None, None);
}
};
finish_verified_evaluation(input, verified, budgets)
}
pub fn evaluate_with_full_floor(
input: EvaluateInput<'_>,
crypto_floor: CapabilityCryptoFloor,
peer: &CapabilityNegotiation,
trust_root: &dyn TrustRootResolver,
budgets: &mut dyn BudgetRegistry,
) -> EvaluationVerdict {
evaluate_with_full_floor_and_root(input, crypto_floor, peer, None, trust_root, budgets)
}
pub fn evaluate_with_full_floor_and_root(
input: EvaluateInput<'_>,
crypto_floor: CapabilityCryptoFloor,
peer: &CapabilityNegotiation,
direct_root: Option<&CapabilityToken>,
trust_root: &dyn TrustRootResolver,
budgets: &mut dyn BudgetRegistry,
) -> EvaluationVerdict {
let mut verify_only_budgets = NoopBudgetRegistry;
let verified = match crate::capability_verify::verify_capability_full_with_root(
input.capability,
input.trusted_issuers,
input.clock,
crypto_floor,
crate::capability_verify::CapabilityFeatureContext { peer, direct_root },
trust_root,
&mut verify_only_budgets,
) {
Ok(verified) => verified,
Err(error) => {
let core_err = KernelCoreError::InvalidCapability(error);
return deny(core_err, None, None);
}
};
if input.capability.aggregate_invocation_budget.is_some() {
return deny(
KernelCoreError::UnsupportedCapabilityFeature {
feature: "aggregate invocation enforcement".to_string(),
},
None,
Some(verified),
);
}
if input.capability.scope.has_cumulative_approval() {
return deny(
KernelCoreError::UnsupportedCapabilityFeature {
feature: "cumulative approval enforcement".to_string(),
},
None,
Some(verified),
);
}
finish_verified_evaluation(input, verified, budgets)
}
fn finish_verified_evaluation(
input: EvaluateInput<'_>,
verified: VerifiedCapability,
budgets: &mut dyn BudgetRegistry,
) -> EvaluationVerdict {
if verified.subject_hex != input.request.agent_id {
let core_err = KernelCoreError::SubjectMismatch {
expected: verified.subject_hex.clone(),
actual: input.request.agent_id.clone(),
};
return deny(core_err, None, Some(verified));
}
let matched_grant_index = match resolve_matched_grant_index(&verified.scope, input.request) {
Ok(index) => index,
Err(error) => return deny(error, None, Some(verified)),
};
let ctx = GuardContext {
request: input.request,
scope: &verified.scope,
agent_id: &input.request.agent_id,
server_id: &input.request.server_id,
session_filesystem_roots: input.session_filesystem_roots,
matched_grant_index: Some(matched_grant_index),
};
for guard in input.guards {
match guard.evaluate(&ctx) {
Ok(Verdict::Allow) => {}
Ok(Verdict::Deny) | Ok(Verdict::PendingApproval) => {
let core_err = KernelCoreError::GuardDenied {
guard: guard.name().to_string(),
};
return deny(core_err, Some(matched_grant_index), Some(verified));
}
Err(error) => {
let core_err = KernelCoreError::GuardError {
guard: guard.name().to_string(),
reason: error.deny_reason(),
};
return deny(core_err, Some(matched_grant_index), Some(verified));
}
}
}
if let Err(error) = admit_delegated_budget(input.capability, budgets) {
let core_err = KernelCoreError::InvalidCapability(error);
return deny(core_err, Some(matched_grant_index), Some(verified));
}
EvaluationVerdict {
verdict: Verdict::Allow,
reason: None,
matched_grant_index: Some(matched_grant_index),
verified: Some(verified),
}
}
fn deny(
error: KernelCoreError,
matched_grant_index: Option<usize>,
verified: Option<VerifiedCapability>,
) -> EvaluationVerdict {
EvaluationVerdict {
verdict: Verdict::Deny,
reason: Some(error.deny_reason()),
matched_grant_index,
verified,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{BudgetRegistry, InMemoryBudgetRegistry, MAX_BUDGET_SHARE_BPS};
use alloc::vec;
use chio_core_types::capability::{
aggregate_invocation::{AggregateInvocationBudget, AggregateInvocationScope},
attenuation::{DelegationLink, DelegationLinkBody},
features::AGGREGATE_INVOCATION_BUDGET,
scope::{ChioScope, Operation, ToolGrant},
token::{CapabilityToken, CapabilityTokenBody},
};
use chio_core_types::crypto::Keypair;
fn grant(server_id: &str, tool_name: &str) -> ToolGrant {
ToolGrant {
server_id: server_id.to_string(),
tool_name: tool_name.to_string(),
operations: vec![Operation::Invoke],
constraints: vec![],
max_invocations: None,
max_cost_per_invocation: None,
max_total_cost: None,
dpop_required: None,
}
}
fn request() -> PortableToolCallRequest {
PortableToolCallRequest {
request_id: "req-1".to_string(),
tool_name: "echo".to_string(),
server_id: "srv-a".to_string(),
agent_id: "agent-1".to_string(),
arguments: serde_json::json!({"msg":"hello"}),
}
}
fn delegated_capability(
issuer: &Keypair,
subject: &Keypair,
parent_capability_id: &str,
) -> CapabilityToken {
let parent_link = match DelegationLink::sign(
DelegationLinkBody {
capability_id: parent_capability_id.to_string(),
delegator: issuer.public_key(),
delegatee: issuer.public_key(),
attenuations: vec![],
timestamp: 100,
scope_hash: None,
aggregate_budget: None,
cumulative_approval: None,
},
issuer,
) {
Ok(link) => link,
Err(error) => panic!("failed to sign parent delegation link: {error:?}"),
};
match CapabilityToken::sign(
CapabilityTokenBody {
id: "child-capability".to_string(),
issuer: issuer.public_key(),
subject: subject.public_key(),
scope: ChioScope {
grants: vec![grant("srv-a", "echo")],
resource_grants: vec![],
prompt_grants: vec![],
},
issued_at: 100,
expires_at: 200,
delegation_chain: vec![parent_link],
aggregate_invocation_budget: None,
},
issuer,
) {
Ok(token) => token,
Err(error) => panic!("failed to sign delegated capability: {error:?}"),
}
}
#[test]
fn resolve_matched_grant_index_uses_scope_specificity_order() {
let scope = ChioScope {
grants: vec![grant("*", "*"), grant("srv-a", "echo")],
resource_grants: vec![],
prompt_grants: vec![],
};
let matched_index = resolve_matched_grant_index(&scope, &request());
assert_eq!(matched_index, Ok(1));
}
#[test]
fn resolve_matched_grant_index_maps_missing_grant_to_request_identity() {
let scope = ChioScope {
grants: vec![grant("srv-b", "echo")],
resource_grants: vec![],
prompt_grants: vec![],
};
let error = resolve_matched_grant_index(&scope, &request());
assert_eq!(
error,
Err(KernelCoreError::OutOfScope {
tool: "echo".to_string(),
server: "srv-a".to_string(),
})
);
}
#[test]
fn budget_admission_waits_until_subject_scope_and_guards_allow() {
let issuer = Keypair::generate();
let subject = Keypair::generate();
let wrong_agent = Keypair::generate();
let parent_capability_id = "parent-capability";
let capability = delegated_capability(&issuer, &subject, parent_capability_id);
let mut request = request();
request.agent_id = wrong_agent.public_key().to_hex();
let trusted = [issuer.public_key()];
let clock = crate::FixedClock::new(150);
let guards: [&dyn Guard; 0] = [];
let mut budgets = InMemoryBudgetRegistry::new();
if let Err(error) =
budgets.register_parent(parent_capability_id.to_string(), MAX_BUDGET_SHARE_BPS)
{
panic!("failed to register parent budget split: {error:?}");
}
let verdict = evaluate_with_crypto_floor_and_budgets(
EvaluateInput {
request: &request,
capability: &capability,
trusted_issuers: &trusted,
clock: &clock,
guards: &guards,
session_filesystem_roots: None,
},
CapabilityCryptoFloor::AllowClassical,
&mut budgets,
);
assert!(verdict.is_deny());
let split = match budgets.split(parent_capability_id) {
Some(split) => split,
None => panic!("registered parent budget split was missing"),
};
assert_eq!(split.current_total_child_bps(), 0);
assert!(split.children.is_empty());
}
#[test]
fn full_floor_budget_admission_waits_until_subject_scope_and_guards_allow() {
let issuer = Keypair::generate();
let subject = Keypair::generate();
let wrong_agent = Keypair::generate();
let parent_capability_id = "parent-capability-full";
let capability = delegated_capability(&issuer, &subject, parent_capability_id);
let mut request = request();
request.agent_id = wrong_agent.public_key().to_hex();
let trusted = [issuer.public_key()];
let clock = crate::FixedClock::new(150);
let guards: [&dyn Guard; 0] = [];
let peer = CapabilityNegotiation::t1_default();
let trust_roots = |_issuer: &chio_core_types::crypto::PublicKey| None;
let mut budgets = InMemoryBudgetRegistry::new();
if let Err(error) =
budgets.register_parent(parent_capability_id.to_string(), MAX_BUDGET_SHARE_BPS)
{
panic!("failed to register parent budget split: {error:?}");
}
let verdict = evaluate_with_full_floor(
EvaluateInput {
request: &request,
capability: &capability,
trusted_issuers: &trusted,
clock: &clock,
guards: &guards,
session_filesystem_roots: None,
},
CapabilityCryptoFloor::AllowClassical,
&peer,
&trust_roots,
&mut budgets,
);
assert!(verdict.is_deny());
let split = match budgets.split(parent_capability_id) {
Some(split) => split,
None => panic!("registered parent budget split was missing"),
};
assert_eq!(split.current_total_child_bps(), 0);
assert!(split.children.is_empty());
}
#[test]
fn full_floor_denies_aggregate_budget_until_enforcement_is_available() {
let issuer = Keypair::generate();
let subject = Keypair::generate();
let capability = match CapabilityToken::sign(
CapabilityTokenBody {
id: "aggregate-not-enforced".to_string(),
issuer: issuer.public_key(),
subject: subject.public_key(),
scope: ChioScope {
grants: vec![grant("srv-a", "echo")],
..ChioScope::default()
},
issued_at: 100,
expires_at: 200,
delegation_chain: vec![],
aggregate_invocation_budget: Some(AggregateInvocationBudget {
scope: AggregateInvocationScope::Capability,
max_invocations: 1,
root_binding: None,
}),
},
&issuer,
) {
Ok(capability) => capability,
Err(error) => panic!("failed to sign aggregate capability: {error}"),
};
let mut request = request();
request.agent_id = subject.public_key().to_hex();
let trusted = [issuer.public_key()];
let clock = crate::FixedClock::new(150);
let guards: [&dyn Guard; 0] = [];
let mut peer = CapabilityNegotiation::v1_default();
peer.features
.insert(AGGREGATE_INVOCATION_BUDGET.to_string(), true);
let trust_roots = |_issuer: &chio_core_types::crypto::PublicKey| None;
let mut budgets = InMemoryBudgetRegistry::new();
let verdict = evaluate_with_full_floor(
EvaluateInput {
request: &request,
capability: &capability,
trusted_issuers: &trusted,
clock: &clock,
guards: &guards,
session_filesystem_roots: None,
},
CapabilityCryptoFloor::AllowClassical,
&peer,
&trust_roots,
&mut budgets,
);
assert!(verdict.is_deny());
assert!(verdict.reason.as_deref().is_some_and(|reason| reason
.contains("unsupported on this runtime: aggregate invocation enforcement")));
}
}