use chio_core::canonical::canonical_json_bytes;
use chio_core::session::OperationTerminalState;
use chio_tool_call_fabric::{DenyReason, ProviderId, ReceiptId, ToolInvocation, VerdictResult};
use crate::runtime::{ToolCallRequest, ToolCallResponse, Verdict};
use crate::{AgentId, ServerId};
use chio_core::capability::token::CapabilityToken;
#[derive(Debug, thiserror::Error)]
pub enum ProviderVerdictError {
#[error("fabric arguments payload is not valid JSON: {0}")]
InvalidArguments(#[source] serde_json::Error),
}
pub fn build_tool_call_request(
invocation: &ToolInvocation,
capability: CapabilityToken,
agent_id: AgentId,
server_id: ServerId,
) -> Result<ToolCallRequest, ProviderVerdictError> {
let arguments = if invocation.arguments.is_empty() {
serde_json::Value::Null
} else {
serde_json::from_slice(&invocation.arguments)
.map_err(ProviderVerdictError::InvalidArguments)?
};
Ok(ToolCallRequest {
request_id: invocation.provenance.request_id.clone(),
capability,
tool_name: invocation.tool_name.clone(),
server_id,
agent_id,
arguments,
dpop_proof: None,
execution_nonce: None,
governed_intent: None,
approval_token: None,
approval_tokens: Vec::new(),
threshold_approval_proposal: None,
supplemental_authorization: None,
model_metadata: None,
federated_origin_kernel_id: None,
})
}
#[must_use]
pub fn verdict_result_from_response(
invocation: &ToolInvocation,
response: &ToolCallResponse,
) -> VerdictResult {
let receipt_id = ReceiptId(response.receipt.id.clone());
match response.verdict {
Verdict::Allow if matches!(response.terminal_state, OperationTerminalState::Completed) => {
VerdictResult::Allow {
redactions: Vec::new(),
receipt_id,
}
}
Verdict::Allow => VerdictResult::Deny {
reason: DenyReason::PolicyDeny {
rule_id: "kernel.execution_nonce_preflight".to_string(),
},
receipt_id,
},
Verdict::Deny => VerdictResult::Deny {
reason: classify_deny_reason(invocation, response),
receipt_id,
},
Verdict::PendingApproval => VerdictResult::Deny {
reason: DenyReason::PolicyDeny {
rule_id: "kernel.pending_approval".to_string(),
},
receipt_id,
},
}
}
fn classify_deny_reason(_invocation: &ToolInvocation, response: &ToolCallResponse) -> DenyReason {
let detail = response
.reason
.clone()
.unwrap_or_else(|| "kernel.deny".to_string());
DenyReason::PolicyDeny { rule_id: detail }
}
pub fn canonical_invocation_bytes(
invocation: &ToolInvocation,
) -> chio_core::error::Result<Vec<u8>> {
canonical_json_bytes(invocation)
}
pub const FABRIC_SHIM_PROVIDER_LANES: &[ProviderId] = &[
ProviderId::OpenAi,
ProviderId::Anthropic,
ProviderId::Bedrock,
];
impl crate::ChioKernel {
pub fn verdict_for_provider_invocation(
&self,
invocation: &ToolInvocation,
capability: CapabilityToken,
agent_id: AgentId,
server_id: ServerId,
) -> Result<VerdictResult, crate::KernelError> {
let request = build_tool_call_request(invocation, capability, agent_id, server_id)
.map_err(|e| {
crate::KernelError::Internal(format!(
"fabric arguments could not be lowered into kernel JSON: {e}"
))
})?;
let response = self.evaluate_tool_call_blocking(&request)?;
Ok(verdict_result_from_response(invocation, &response))
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use chio_core::receipt::{body::ChioReceipt, decision::Decision, decision::ToolCallAction};
use chio_core::session::OperationTerminalState;
use chio_tool_call_fabric::{Principal, ProvenanceStamp};
use std::time::{Duration, SystemTime};
fn sample_invocation() -> ToolInvocation {
ToolInvocation {
provider: ProviderId::OpenAi,
tool_name: "search_web".to_string(),
arguments: br#"{"query":"chio"}"#.to_vec(),
provenance: ProvenanceStamp {
provider: ProviderId::OpenAi,
request_id: "call_abc123".to_string(),
api_version: "responses.2026-04-25".to_string(),
principal: Principal::OpenAiOrg {
org_id: "org_123".to_string(),
},
received_at: SystemTime::UNIX_EPOCH + Duration::from_secs(1_700_000_000),
},
}
}
fn synthetic_receipt(id: &str, decision: Decision) -> ChioReceipt {
let kp = chio_core::crypto::Keypair::generate();
let body = chio_core::receipt::body::ChioReceiptBody {
id: id.to_string(),
timestamp: 1_700_000_000,
capability_id: "cap-test".to_string(),
tool_server: "srv-test".to_string(),
tool_name: "search_web".to_string(),
action: ToolCallAction {
parameters: serde_json::json!({"query": "chio"}),
parameter_hash: "0".repeat(64),
},
decision: Some(decision),
receipt_kind: chio_core::receipt::kinds::ReceiptKind::MediatedDecision,
boundary_class: chio_core::receipt::kinds::BoundaryClass::Prevent,
observation_outcome: None,
tool_origin: chio_core::receipt::kinds::ToolOrigin::CallerExecuted,
redaction_mode: chio_core::receipt::kinds::RedactionMode::None,
actor_chain: Vec::new(),
content_hash: "0".repeat(64),
policy_hash: "0".repeat(64),
evidence: Vec::new(),
metadata: None,
trust_level: Default::default(),
tenant_id: None,
kernel_key: kp.public_key(),
bbs_projection_version: None,
};
ChioReceipt::sign(body, &kp).unwrap()
}
fn allow_response() -> ToolCallResponse {
ToolCallResponse {
request_id: "call_abc123".to_string(),
verdict: Verdict::Allow,
output: None,
reason: None,
terminal_state: OperationTerminalState::Completed,
receipt: synthetic_receipt("rcpt_allow", Decision::Allow),
execution_nonce: None,
}
}
fn deny_response(reason: &str) -> ToolCallResponse {
let dec = Decision::Deny {
reason: reason.to_string(),
guard: "policy.deny".to_string(),
};
ToolCallResponse {
request_id: "call_abc123".to_string(),
verdict: Verdict::Deny,
output: None,
reason: Some(reason.to_string()),
terminal_state: OperationTerminalState::Completed,
receipt: synthetic_receipt("rcpt_deny", dec),
execution_nonce: None,
}
}
fn pending_response() -> ToolCallResponse {
ToolCallResponse {
request_id: "call_abc123".to_string(),
verdict: Verdict::PendingApproval,
output: None,
reason: Some("approval pending".to_string()),
terminal_state: OperationTerminalState::Incomplete {
reason: "approval pending".to_string(),
},
receipt: synthetic_receipt("rcpt_pending", Decision::Allow),
execution_nonce: None,
}
}
fn nonce_preflight_response() -> ToolCallResponse {
ToolCallResponse {
request_id: "call_abc123".to_string(),
verdict: Verdict::Allow,
output: None,
reason: None,
terminal_state: OperationTerminalState::Incomplete {
reason: "execution nonce preflight requires retry with presented nonce".to_string(),
},
receipt: synthetic_receipt("rcpt_preflight", Decision::Allow),
execution_nonce: None,
}
}
#[test]
fn provider_verdict_allow_maps_to_fabric_allow() {
let inv = sample_invocation();
let resp = allow_response();
let v = verdict_result_from_response(&inv, &resp);
match v {
VerdictResult::Allow {
redactions,
receipt_id,
} => {
assert!(redactions.is_empty());
assert_eq!(receipt_id, ReceiptId(resp.receipt.id.clone()));
}
other => panic!("expected allow, got {other:?}"),
}
}
#[test]
fn provider_verdict_deny_carries_kernel_reason_as_rule_id() {
let inv = sample_invocation();
let resp = deny_response("budget exhausted");
let v = verdict_result_from_response(&inv, &resp);
match v {
VerdictResult::Deny { reason, receipt_id } => {
assert_eq!(receipt_id, ReceiptId(resp.receipt.id.clone()));
match reason {
DenyReason::PolicyDeny { rule_id } => {
assert_eq!(rule_id, "budget exhausted");
}
other => panic!("expected policy_deny, got {other:?}"),
}
}
other => panic!("expected deny, got {other:?}"),
}
}
#[test]
fn provider_verdict_deny_falls_back_to_default_rule_id_when_reason_missing() {
let inv = sample_invocation();
let mut resp = deny_response("unused");
resp.reason = None;
let v = verdict_result_from_response(&inv, &resp);
let VerdictResult::Deny { reason, .. } = v else {
panic!("expected deny");
};
let DenyReason::PolicyDeny { rule_id } = reason else {
panic!("expected policy_deny");
};
assert_eq!(rule_id, "kernel.deny");
}
#[test]
fn provider_verdict_pending_approval_fails_closed() {
let inv = sample_invocation();
let resp = pending_response();
let v = verdict_result_from_response(&inv, &resp);
match v {
VerdictResult::Deny { reason, .. } => match reason {
DenyReason::PolicyDeny { rule_id } => {
assert_eq!(rule_id, "kernel.pending_approval");
}
other => panic!("expected policy_deny for pending, got {other:?}"),
},
other => panic!("expected deny for pending approval, got {other:?}"),
}
}
#[test]
fn provider_verdict_nonce_preflight_fails_closed() {
let inv = sample_invocation();
let resp = nonce_preflight_response();
let v = verdict_result_from_response(&inv, &resp);
match v {
VerdictResult::Deny { reason, receipt_id } => {
assert_eq!(receipt_id, ReceiptId(resp.receipt.id.clone()));
match reason {
DenyReason::PolicyDeny { rule_id } => {
assert_eq!(rule_id, "kernel.execution_nonce_preflight");
}
other => panic!("expected policy_deny for nonce preflight, got {other:?}"),
}
}
other => panic!("expected deny for nonce preflight, got {other:?}"),
}
}
#[test]
fn provider_verdict_receipt_id_round_trips() {
let inv = sample_invocation();
let resp = allow_response();
let v = verdict_result_from_response(&inv, &resp);
let json = serde_json::to_string(&v).unwrap();
let back: VerdictResult = serde_json::from_str(&json).unwrap();
assert_eq!(v, back);
}
#[test]
fn provider_verdict_canonical_invocation_bytes_are_stable() {
let inv = sample_invocation();
let a = canonical_invocation_bytes(&inv).unwrap();
let b = canonical_invocation_bytes(&inv).unwrap();
assert_eq!(a, b);
}
#[test]
fn provider_verdict_known_provider_lanes_are_three() {
assert_eq!(FABRIC_SHIM_PROVIDER_LANES.len(), 3);
}
}