use std::collections::{BTreeMap, HashMap};
use std::sync::Arc;
use chio_core_types::capability::{
CapabilityToken, ChioScope, ModelMetadata, Operation, ToolGrant,
};
use chio_core_types::crypto::{Keypair, PublicKey};
use chio_core_types::receipt::GuardEvidence;
use chio_cross_protocol::{
plan_authoritative_route, route_selection_metadata, DiscoveryProtocol, TargetProtocolRegistry,
};
use chio_kernel::{
ApprovalStore, ChioKernel, Guard, GuardContext, InMemoryApprovalStore, KernelConfig,
KernelError, ToolCallRequest, ToolServerConnection, Verdict as KernelVerdict,
DEFAULT_CHECKPOINT_BATCH_SIZE, DEFAULT_MAX_STREAM_DURATION_SECS,
DEFAULT_MAX_STREAM_TOTAL_BYTES,
};
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use thiserror::Error;
use crate::{
http_status_metadata_decision, http_status_metadata_final, CallerIdentity, ChioHttpRequest,
HttpMethod, HttpReceipt, HttpReceiptBody, Verdict, CHIO_KERNEL_RECEIPT_ID_KEY,
};
const HTTP_AUTHORITY_SERVER_ID: &str = "chio_http_authority";
const HTTP_AUTHORITY_TOOL_NAME: &str = "authorize_http_request";
const HTTP_AUTHORITY_TTL_SECS: u64 = 60;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum HttpAuthorityPolicy {
SessionAllow,
DenyByDefault,
}
#[derive(Clone)]
pub struct HttpAuthority {
keypair: Arc<Keypair>,
policy_hash: String,
kernel: Arc<ChioKernel>,
kernel_subject: PublicKey,
kernel_agent_id: String,
approval_store: Arc<dyn ApprovalStore>,
trusted_capability_issuers: Vec<PublicKey>,
}
impl std::fmt::Debug for HttpAuthority {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("HttpAuthority")
.field("policy_hash", &self.policy_hash)
.field("kernel_agent_id", &self.kernel_agent_id)
.finish_non_exhaustive()
}
}
pub struct HttpAuthorityInput<'a> {
pub request_id: String,
pub method: HttpMethod,
pub route_pattern: String,
pub path: &'a str,
pub query: &'a HashMap<String, String>,
pub caller: CallerIdentity,
pub body_hash: Option<String>,
pub body_length: u64,
pub session_id: Option<String>,
pub capability_id_hint: Option<&'a str>,
pub presented_capability: Option<&'a str>,
pub requested_tool_server: Option<&'a str>,
pub requested_tool_name: Option<&'a str>,
pub requested_arguments: Option<&'a Value>,
pub model_metadata: Option<&'a ModelMetadata>,
pub policy: HttpAuthorityPolicy,
}
#[derive(Debug, Clone)]
pub struct PreparedHttpEvaluation {
pub verdict: Verdict,
pub evidence: Vec<GuardEvidence>,
pub request_id: String,
pub route_pattern: String,
pub http_method: HttpMethod,
pub caller_identity_hash: String,
pub content_hash: String,
pub session_id: Option<String>,
pub capability_id: Option<String>,
pub kernel_receipt_id: String,
pub route_selection_metadata: Option<Value>,
}
#[derive(Debug, Clone)]
pub struct HttpAuthorityEvaluation {
pub verdict: Verdict,
pub receipt: HttpReceipt,
pub evidence: Vec<GuardEvidence>,
}
#[derive(Debug, Error)]
pub enum HttpAuthorityError {
#[error("failed to hash caller identity: {0}")]
CallerIdentity(String),
#[error("failed to compute content hash: {0}")]
ContentHash(String),
#[error("kernel-backed authorization failed: {0}")]
Kernel(String),
#[error("kernel-backed authorization requires approval")]
PendingApproval {
approval_id: Option<String>,
kernel_receipt_id: String,
},
#[error("failed to sign receipt: {0}")]
ReceiptSign(String),
}
#[derive(Debug, Clone)]
struct PresentedCapabilityState {
capability_id: Option<String>,
invalid_reason: Option<String>,
}
#[derive(Clone, Copy)]
struct RequestedToolInvocation<'a> {
server_id: &'a str,
tool_name: &'a str,
arguments: &'a Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct HttpKernelAuthorizationRequest {
request_id: String,
method: HttpMethod,
route_pattern: String,
path: String,
content_hash: String,
caller_identity_hash: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
session_id: Option<String>,
policy: HttpAuthorityPolicy,
capability: HttpKernelCapabilityState,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct HttpKernelCapabilityState {
#[serde(default, skip_serializing_if = "Option::is_none")]
id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
invalid_reason: Option<String>,
}
struct HttpAuthorizationServer;
impl ToolServerConnection for HttpAuthorizationServer {
fn server_id(&self) -> &str {
HTTP_AUTHORITY_SERVER_ID
}
fn tool_names(&self) -> Vec<String> {
vec![HTTP_AUTHORITY_TOOL_NAME.to_string()]
}
fn invoke(
&self,
tool_name: &str,
_arguments: serde_json::Value,
_nested_flow_bridge: Option<&mut dyn chio_kernel::NestedFlowBridge>,
) -> Result<serde_json::Value, KernelError> {
if tool_name != HTTP_AUTHORITY_TOOL_NAME {
return Err(KernelError::Internal(format!(
"unsupported HTTP authority tool: {tool_name}"
)));
}
Ok(serde_json::json!({ "authorized": true }))
}
}
struct HttpProjectionGuard;
impl Guard for HttpProjectionGuard {
fn name(&self) -> &str {
"http_projection_policy"
}
fn evaluate(&self, ctx: &GuardContext<'_>) -> Result<KernelVerdict, KernelError> {
let projected: HttpKernelAuthorizationRequest =
serde_json::from_value(ctx.request.arguments.clone()).map_err(|error| {
KernelError::Internal(format!(
"failed to decode projected HTTP authorization request: {error}"
))
})?;
if let Some(reason) = projected.capability.invalid_reason {
return Err(KernelError::GuardDenied(reason));
}
match projected.policy {
HttpAuthorityPolicy::SessionAllow => Ok(KernelVerdict::Allow),
HttpAuthorityPolicy::DenyByDefault => {
if projected.capability.id.is_some() {
Ok(KernelVerdict::Allow)
} else {
Err(KernelError::GuardDenied(
"side-effect route requires a capability token".to_string(),
))
}
}
}
}
}
impl HttpAuthority {
#[must_use]
pub fn new(keypair: Keypair, policy_hash: String) -> Self {
Self::new_with_approval_store_and_trusted_issuers(
keypair,
policy_hash,
Arc::new(InMemoryApprovalStore::new()),
Vec::new(),
)
}
#[must_use]
pub fn new_with_approval_store(
keypair: Keypair,
policy_hash: String,
approval_store: Arc<dyn ApprovalStore>,
) -> Self {
Self::new_with_approval_store_and_trusted_issuers(
keypair,
policy_hash,
approval_store,
Vec::new(),
)
}
#[must_use]
pub fn new_with_approval_store_and_trusted_issuers(
keypair: Keypair,
policy_hash: String,
approval_store: Arc<dyn ApprovalStore>,
mut trusted_capability_issuers: Vec<PublicKey>,
) -> Self {
let keypair = Arc::new(keypair);
let signer_public_key = keypair.public_key();
if !trusted_capability_issuers.contains(&signer_public_key) {
trusted_capability_issuers.push(signer_public_key.clone());
}
let kernel_subject = Keypair::generate().public_key();
let kernel_agent_id = kernel_subject.to_hex();
let mut kernel = ChioKernel::new(KernelConfig {
keypair: keypair.as_ref().clone(),
ca_public_keys: trusted_capability_issuers.clone(),
max_delegation_depth: 8,
policy_hash: policy_hash.clone(),
allow_sampling: false,
allow_sampling_tool_use: false,
allow_elicitation: false,
max_stream_duration_secs: DEFAULT_MAX_STREAM_DURATION_SECS,
max_stream_total_bytes: DEFAULT_MAX_STREAM_TOTAL_BYTES,
require_web3_evidence: false,
checkpoint_batch_size: DEFAULT_CHECKPOINT_BATCH_SIZE,
retention_config: None,
});
kernel.register_tool_server(Box::new(HttpAuthorizationServer));
kernel.add_guard(Box::new(HttpProjectionGuard));
Self {
keypair,
policy_hash,
kernel: Arc::new(kernel),
kernel_subject,
kernel_agent_id,
approval_store,
trusted_capability_issuers,
}
}
#[must_use]
pub fn approval_store(&self) -> Arc<dyn ApprovalStore> {
Arc::clone(&self.approval_store)
}
fn trusted_capability_issuers(&self) -> &[PublicKey] {
&self.trusted_capability_issuers
}
pub fn evaluate(
&self,
input: HttpAuthorityInput<'_>,
) -> Result<HttpAuthorityEvaluation, HttpAuthorityError> {
let prepared = self.prepare(input)?;
let receipt = self.sign_decision_receipt(&prepared)?;
Ok(HttpAuthorityEvaluation {
verdict: prepared.verdict.clone(),
receipt,
evidence: prepared.evidence.clone(),
})
}
pub fn prepare(
&self,
input: HttpAuthorityInput<'_>,
) -> Result<PreparedHttpEvaluation, HttpAuthorityError> {
let presented_capability = validate_presented_capability(
input.capability_id_hint,
input.presented_capability,
self.trusted_capability_issuers(),
input.requested_tool_server,
input.requested_tool_name,
input.requested_arguments,
input.model_metadata,
);
let caller_identity_hash = input
.caller
.identity_hash()
.map_err(|e| HttpAuthorityError::CallerIdentity(e.to_string()))?;
let chio_request = ChioHttpRequest {
request_id: input.request_id.clone(),
method: input.method,
route_pattern: input.route_pattern.clone(),
path: input.path.to_string(),
query: input.query.clone(),
headers: HashMap::new(),
caller: input.caller,
body_hash: input.body_hash,
body_length: input.body_length,
session_id: input.session_id.clone(),
capability_id: presented_capability.capability_id.clone(),
tool_server: input.requested_tool_server.map(str::to_owned),
tool_name: input.requested_tool_name.map(str::to_owned),
arguments: input.requested_arguments.cloned(),
model_metadata: input.model_metadata.cloned(),
timestamp: chrono::Utc::now().timestamp() as u64,
};
let content_hash = chio_request
.content_hash()
.map_err(|e| HttpAuthorityError::ContentHash(e.to_string()))?;
let kernel_response = self.authorize_via_kernel(
&input.request_id,
input.method,
&input.route_pattern,
input.path,
&content_hash,
&caller_identity_hash,
input.session_id.as_deref(),
input.policy,
&presented_capability,
)?;
let verdict = projected_verdict(input.policy, &presented_capability);
let expected_allowed = verdict.is_allowed();
match (kernel_response.verdict, expected_allowed) {
(KernelVerdict::Allow, true) | (KernelVerdict::Deny, false) => {}
(KernelVerdict::Allow, false) => {
return Err(HttpAuthorityError::Kernel(
"kernel allowed an HTTP projection that should have been denied".to_string(),
));
}
(KernelVerdict::Deny, true) => {
let reason = kernel_response
.reason
.unwrap_or_else(|| "kernel denied an allowed HTTP projection".to_string());
return Err(HttpAuthorityError::Kernel(reason));
}
(KernelVerdict::PendingApproval, _) => {
return Err(HttpAuthorityError::PendingApproval {
approval_id: pending_approval_id(
kernel_response.receipt.metadata.as_ref(),
kernel_response.reason.as_deref(),
),
kernel_receipt_id: kernel_response.receipt.id,
});
}
}
let evidence = projected_evidence(input.policy, &presented_capability);
Ok(PreparedHttpEvaluation {
verdict,
evidence,
request_id: input.request_id,
route_pattern: input.route_pattern,
http_method: input.method,
caller_identity_hash,
content_hash,
session_id: input.session_id,
capability_id: presented_capability.capability_id,
kernel_receipt_id: kernel_response.receipt.id,
route_selection_metadata: metadata_value(
kernel_response.receipt.metadata.as_ref(),
"route_selection",
)
.cloned(),
})
}
pub fn sign_decision_receipt(
&self,
prepared: &PreparedHttpEvaluation,
) -> Result<HttpReceipt, HttpAuthorityError> {
self.sign_receipt(
prepared,
decision_status(&prepared.verdict),
decision_metadata(
Some(&prepared.kernel_receipt_id),
prepared.route_selection_metadata.as_ref(),
),
)
}
pub fn finalize_receipt(
&self,
prepared: &PreparedHttpEvaluation,
response_status: u16,
decision_receipt_id: Option<&str>,
) -> Result<HttpReceipt, HttpAuthorityError> {
self.sign_receipt(
prepared,
response_status,
final_metadata(
decision_receipt_id,
Some(&prepared.kernel_receipt_id),
prepared.route_selection_metadata.as_ref(),
),
)
}
pub fn finalize_decision_receipt(
&self,
decision_receipt: &HttpReceipt,
response_status: u16,
) -> Result<HttpReceipt, HttpAuthorityError> {
let mut body = decision_receipt.body();
let decision_receipt_id = body.id.clone();
let kernel_receipt_id = metadata_string(body.metadata.as_ref(), CHIO_KERNEL_RECEIPT_ID_KEY)
.map(ToOwned::to_owned);
let route_selection = metadata_value(body.metadata.as_ref(), "route_selection").cloned();
body.id = uuid::Uuid::now_v7().to_string();
body.response_status = response_status;
body.timestamp = chrono::Utc::now().timestamp() as u64;
body.metadata = final_metadata(
Some(&decision_receipt_id),
kernel_receipt_id.as_deref(),
route_selection.as_ref(),
);
HttpReceipt::sign(body, self.keypair.as_ref())
.map_err(|e| HttpAuthorityError::ReceiptSign(e.to_string()))
}
#[allow(clippy::too_many_arguments)]
fn authorize_via_kernel(
&self,
request_id: &str,
method: HttpMethod,
route_pattern: &str,
path: &str,
content_hash: &str,
caller_identity_hash: &str,
session_id: Option<&str>,
policy: HttpAuthorityPolicy,
presented_capability: &PresentedCapabilityState,
) -> Result<chio_kernel::ToolCallResponse, HttpAuthorityError> {
let capability = self
.kernel
.issue_capability(
&self.kernel_subject,
kernel_scope(),
HTTP_AUTHORITY_TTL_SECS,
)
.map_err(|error| HttpAuthorityError::Kernel(error.to_string()))?;
let projected = HttpKernelAuthorizationRequest {
request_id: request_id.to_string(),
method,
route_pattern: route_pattern.to_string(),
path: path.to_string(),
content_hash: content_hash.to_string(),
caller_identity_hash: caller_identity_hash.to_string(),
session_id: session_id.map(ToOwned::to_owned),
policy,
capability: HttpKernelCapabilityState {
id: presented_capability.capability_id.clone(),
invalid_reason: presented_capability.invalid_reason.clone(),
},
};
let request = ToolCallRequest {
request_id: request_id.to_string(),
capability,
tool_name: HTTP_AUTHORITY_TOOL_NAME.to_string(),
server_id: HTTP_AUTHORITY_SERVER_ID.to_string(),
agent_id: self.kernel_agent_id.clone(),
arguments: serde_json::to_value(projected)
.map_err(|error| HttpAuthorityError::Kernel(error.to_string()))?,
dpop_proof: None,
governed_intent: None,
approval_token: None,
model_metadata: None,
federated_origin_kernel_id: None,
};
let route_plan = plan_authoritative_route(
request_id,
DiscoveryProtocol::Http,
DiscoveryProtocol::Native,
None,
&TargetProtocolRegistry::new(DiscoveryProtocol::Native),
&BTreeMap::new(),
)
.map_err(|error| HttpAuthorityError::Kernel(error.to_string()))?;
let route_metadata = route_selection_metadata(&route_plan.evidence)
.map_err(|error| HttpAuthorityError::Kernel(error.to_string()))?;
self.kernel
.evaluate_tool_call_blocking_with_metadata(&request, Some(route_metadata))
.map_err(|error| HttpAuthorityError::Kernel(error.to_string()))
}
fn sign_receipt(
&self,
prepared: &PreparedHttpEvaluation,
response_status: u16,
metadata: Option<Value>,
) -> Result<HttpReceipt, HttpAuthorityError> {
let body = HttpReceiptBody {
id: uuid::Uuid::now_v7().to_string(),
request_id: prepared.request_id.clone(),
route_pattern: prepared.route_pattern.clone(),
method: prepared.http_method,
caller_identity_hash: prepared.caller_identity_hash.clone(),
session_id: prepared.session_id.clone(),
verdict: prepared.verdict.clone(),
evidence: prepared.evidence.clone(),
response_status,
timestamp: chrono::Utc::now().timestamp() as u64,
content_hash: prepared.content_hash.clone(),
policy_hash: self.policy_hash.clone(),
capability_id: prepared.capability_id.clone(),
metadata,
kernel_key: self.keypair.public_key(),
};
HttpReceipt::sign(body, self.keypair.as_ref())
.map_err(|e| HttpAuthorityError::ReceiptSign(e.to_string()))
}
}
fn kernel_scope() -> ChioScope {
ChioScope {
grants: vec![ToolGrant {
server_id: HTTP_AUTHORITY_SERVER_ID.to_string(),
tool_name: HTTP_AUTHORITY_TOOL_NAME.to_string(),
operations: vec![Operation::Invoke],
constraints: Vec::new(),
max_invocations: Some(1),
max_cost_per_invocation: None,
max_total_cost: None,
dpop_required: None,
}],
resource_grants: Vec::new(),
prompt_grants: Vec::new(),
}
}
fn decision_status(verdict: &Verdict) -> u16 {
match verdict {
Verdict::Allow => 200,
Verdict::Deny { http_status, .. } => *http_status,
Verdict::Cancel { .. } | Verdict::Incomplete { .. } => 500,
}
}
fn validate_presented_capability(
capability_id_hint: Option<&str>,
presented_capability: Option<&str>,
trusted_issuers: &[PublicKey],
requested_tool_server: Option<&str>,
requested_tool_name: Option<&str>,
requested_arguments: Option<&Value>,
model_metadata: Option<&ModelMetadata>,
) -> PresentedCapabilityState {
let requested_tool = match (requested_tool_server, requested_tool_name) {
(Some(server_id), Some(tool_name)) => Some(RequestedToolInvocation {
server_id,
tool_name,
arguments: requested_arguments.unwrap_or(&Value::Null),
}),
(None, None) => None,
_ => {
return PresentedCapabilityState {
capability_id: None,
invalid_reason: Some(
"tool-call evaluation requires both tool_server and tool_name".to_string(),
),
};
}
};
let Some(raw_capability) = presented_capability else {
return PresentedCapabilityState {
capability_id: None,
invalid_reason: None,
};
};
match validate_capability_token(
raw_capability,
trusted_issuers,
requested_tool,
model_metadata,
) {
Ok(token) => {
if let Some(hint) = capability_id_hint {
if hint != token.id {
return PresentedCapabilityState {
capability_id: None,
invalid_reason: Some(
"capability_id does not match the presented capability token"
.to_string(),
),
};
}
}
PresentedCapabilityState {
capability_id: Some(token.id),
invalid_reason: None,
}
}
Err(reason) => PresentedCapabilityState {
capability_id: None,
invalid_reason: Some(reason),
},
}
}
fn projected_verdict(
policy: HttpAuthorityPolicy,
presented_capability: &PresentedCapabilityState,
) -> Verdict {
if let Some(reason) = &presented_capability.invalid_reason {
return Verdict::deny(reason, "CapabilityGuard");
}
match policy {
HttpAuthorityPolicy::SessionAllow => Verdict::Allow,
HttpAuthorityPolicy::DenyByDefault => match &presented_capability.capability_id {
Some(_) => Verdict::Allow,
None => Verdict::deny(
"side-effect route requires a capability token",
"CapabilityGuard",
),
},
}
}
fn projected_evidence(
policy: HttpAuthorityPolicy,
presented_capability: &PresentedCapabilityState,
) -> Vec<GuardEvidence> {
if let Some(reason) = &presented_capability.invalid_reason {
return vec![GuardEvidence {
guard_name: "CapabilityGuard".to_string(),
verdict: false,
details: Some(reason.clone()),
}];
}
match policy {
HttpAuthorityPolicy::SessionAllow => vec![GuardEvidence {
guard_name: "DefaultPolicyGuard".to_string(),
verdict: true,
details: Some("safe method, session-scoped allow".to_string()),
}],
HttpAuthorityPolicy::DenyByDefault => match &presented_capability.capability_id {
Some(_) => vec![GuardEvidence {
guard_name: "CapabilityGuard".to_string(),
verdict: true,
details: Some("valid capability token presented".to_string()),
}],
None => vec![GuardEvidence {
guard_name: "CapabilityGuard".to_string(),
verdict: false,
details: Some("side-effect route requires a valid capability token".to_string()),
}],
},
}
}
fn validate_capability_token(
raw: &str,
trusted_issuers: &[PublicKey],
requested_tool: Option<RequestedToolInvocation<'_>>,
model_metadata: Option<&ModelMetadata>,
) -> Result<CapabilityToken, String> {
let token: CapabilityToken =
serde_json::from_str(raw).map_err(|e| format!("invalid capability token: {e}"))?;
if !trusted_issuers.contains(&token.issuer) {
return Err("capability issuer is not trusted".to_string());
}
let signature_valid = token
.verify_signature()
.map_err(|e| format!("capability signature verification failed: {e}"))?;
if !signature_valid {
return Err("capability signature verification failed".to_string());
}
token
.validate_time(chrono::Utc::now().timestamp() as u64)
.map_err(|e| format!("invalid capability token: {e}"))?;
if let Some(requested_tool) = requested_tool {
let matches = chio_kernel::capability_matches_request_with_model_metadata(
&token,
requested_tool.tool_name,
requested_tool.server_id,
requested_tool.arguments,
model_metadata,
)
.map_err(|e| format!("failed to evaluate capability scope: {e}"))?;
if !matches {
return Err(format!(
"capability does not authorize tool {} on server {}",
requested_tool.tool_name, requested_tool.server_id
));
}
}
Ok(token)
}
fn decision_metadata(
kernel_receipt_id: Option<&str>,
route_selection: Option<&Value>,
) -> Option<Value> {
let mut metadata = http_status_metadata_decision();
insert_metadata_string(&mut metadata, CHIO_KERNEL_RECEIPT_ID_KEY, kernel_receipt_id);
insert_metadata_value(&mut metadata, "route_selection", route_selection);
Some(metadata)
}
fn final_metadata(
decision_receipt_id: Option<&str>,
kernel_receipt_id: Option<&str>,
route_selection: Option<&Value>,
) -> Option<Value> {
let mut metadata = http_status_metadata_final(decision_receipt_id);
insert_metadata_string(&mut metadata, CHIO_KERNEL_RECEIPT_ID_KEY, kernel_receipt_id);
insert_metadata_value(&mut metadata, "route_selection", route_selection);
Some(metadata)
}
fn insert_metadata_string(metadata: &mut Value, key: &str, value: Option<&str>) {
let Some(value) = value else {
return;
};
if let Value::Object(map) = metadata {
map.insert(key.to_string(), Value::String(value.to_string()));
} else {
let mut map = Map::new();
map.insert(key.to_string(), Value::String(value.to_string()));
*metadata = Value::Object(map);
}
}
fn insert_metadata_value(metadata: &mut Value, key: &str, value: Option<&Value>) {
let Some(value) = value else {
return;
};
if let Value::Object(map) = metadata {
map.insert(key.to_string(), value.clone());
} else {
let mut map = Map::new();
map.insert(key.to_string(), value.clone());
*metadata = Value::Object(map);
}
}
fn metadata_string<'a>(metadata: Option<&'a Value>, key: &str) -> Option<&'a str> {
metadata
.and_then(Value::as_object)
.and_then(|map| map.get(key))
.and_then(Value::as_str)
}
fn metadata_value<'a>(metadata: Option<&'a Value>, key: &str) -> Option<&'a Value> {
metadata
.and_then(Value::as_object)
.and_then(|map| map.get(key))
}
fn pending_approval_id(metadata: Option<&Value>, reason: Option<&str>) -> Option<String> {
metadata_string(metadata, "approval_id")
.or_else(|| {
metadata_value(metadata, "pending_approval")
.and_then(Value::as_object)
.and_then(|pending| pending.get("approval_id"))
.and_then(Value::as_str)
})
.map(ToOwned::to_owned)
.or_else(|| extract_approval_id(reason))
}
fn extract_approval_id(reason: Option<&str>) -> Option<String> {
let reason = reason?;
for marker in ["/approvals/", "approval_id=", "approval_id:"] {
if let Some(start) = reason.find(marker) {
let suffix = reason[start + marker.len()..].trim_start();
let approval_id = suffix
.split(|character: char| {
character == '/'
|| character == ','
|| character == ';'
|| character.is_whitespace()
})
.next()?;
if !approval_id.is_empty() {
return Some(approval_id.to_string());
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
http_status_scope, AuthMethod, CHIO_DECISION_RECEIPT_ID_KEY,
CHIO_HTTP_STATUS_SCOPE_DECISION, CHIO_HTTP_STATUS_SCOPE_FINAL,
};
use chio_core_types::capability::{CapabilityTokenBody, ChioScope, Operation, ToolGrant};
fn signed_capability_token_json(issuer: &Keypair, id: &str) -> String {
signed_capability_token_json_with_scope(issuer, id, ChioScope::default())
}
fn signed_capability_token_json_with_scope(
issuer: &Keypair,
id: &str,
scope: ChioScope,
) -> String {
let now = chrono::Utc::now().timestamp() as u64;
let token = CapabilityToken::sign(
CapabilityTokenBody {
id: id.to_string(),
issuer: issuer.public_key(),
subject: issuer.public_key(),
scope,
issued_at: now.saturating_sub(60),
expires_at: now + 3600,
delegation_chain: Vec::new(),
},
&issuer,
)
.unwrap();
serde_json::to_string(&token).unwrap()
}
fn caller() -> CallerIdentity {
CallerIdentity {
subject: "tester".to_string(),
auth_method: AuthMethod::Anonymous,
verified: false,
tenant: None,
agent_id: None,
}
}
fn authority() -> HttpAuthority {
HttpAuthority::new(Keypair::generate(), "policy-hash".to_string())
}
fn authority_with_issuer() -> (HttpAuthority, Keypair) {
let issuer = Keypair::generate();
(
HttpAuthority::new(issuer.clone(), "policy-hash".to_string()),
issuer,
)
}
fn authority_with_trusted_issuer(trusted_issuer: PublicKey) -> HttpAuthority {
HttpAuthority::new_with_approval_store_and_trusted_issuers(
Keypair::generate(),
"policy-hash".to_string(),
Arc::new(InMemoryApprovalStore::new()),
vec![trusted_issuer],
)
}
#[test]
fn safe_policy_allows_without_capability() {
let query = HashMap::new();
let result = authority()
.evaluate(HttpAuthorityInput {
request_id: "req-1".to_string(),
method: HttpMethod::Get,
route_pattern: "/pets".to_string(),
path: "/pets",
query: &query,
caller: caller(),
body_hash: None,
body_length: 0,
session_id: None,
capability_id_hint: None,
presented_capability: None,
requested_tool_server: None,
requested_tool_name: None,
requested_arguments: None,
model_metadata: None,
policy: HttpAuthorityPolicy::SessionAllow,
})
.unwrap();
assert!(result.verdict.is_allowed());
assert_eq!(
http_status_scope(result.receipt.metadata.as_ref()),
Some(CHIO_HTTP_STATUS_SCOPE_DECISION)
);
assert!(
metadata_string(result.receipt.metadata.as_ref(), CHIO_KERNEL_RECEIPT_ID_KEY).is_some()
);
assert_eq!(
metadata_value(result.receipt.metadata.as_ref(), "route_selection")
.and_then(|value| value.get("selectedTargetProtocol"))
.and_then(Value::as_str),
Some("native")
);
}
#[test]
fn deny_by_default_requires_capability() {
let query = HashMap::new();
let result = authority()
.evaluate(HttpAuthorityInput {
request_id: "req-2".to_string(),
method: HttpMethod::Post,
route_pattern: "/pets".to_string(),
path: "/pets",
query: &query,
caller: caller(),
body_hash: Some("abc".to_string()),
body_length: 3,
session_id: None,
capability_id_hint: None,
presented_capability: None,
requested_tool_server: None,
requested_tool_name: None,
requested_arguments: None,
model_metadata: None,
policy: HttpAuthorityPolicy::DenyByDefault,
})
.unwrap();
assert!(result.verdict.is_denied());
assert_eq!(result.receipt.response_status, 403);
}
#[test]
fn invalid_presented_capability_denies_even_safe_route() {
let query = HashMap::new();
let result = authority()
.evaluate(HttpAuthorityInput {
request_id: "req-invalid".to_string(),
method: HttpMethod::Get,
route_pattern: "/pets".to_string(),
path: "/pets",
query: &query,
caller: caller(),
body_hash: None,
body_length: 0,
session_id: None,
capability_id_hint: None,
presented_capability: Some("{not-json"),
requested_tool_server: None,
requested_tool_name: None,
requested_arguments: None,
model_metadata: None,
policy: HttpAuthorityPolicy::SessionAllow,
})
.unwrap();
assert!(result.verdict.is_denied());
assert_eq!(result.receipt.evidence.len(), 1);
assert_eq!(result.receipt.evidence[0].guard_name, "CapabilityGuard");
}
#[test]
fn valid_capability_allows_deny_by_default() {
let query = HashMap::new();
let (authority, issuer) = authority_with_issuer();
let capability = signed_capability_token_json(&issuer, "cap-123");
let result = authority
.evaluate(HttpAuthorityInput {
request_id: "req-3".to_string(),
method: HttpMethod::Patch,
route_pattern: "/pets/{petId}".to_string(),
path: "/pets/42",
query: &query,
caller: caller(),
body_hash: Some("def".to_string()),
body_length: 3,
session_id: Some("session-1".to_string()),
capability_id_hint: None,
presented_capability: Some(&capability),
requested_tool_server: None,
requested_tool_name: None,
requested_arguments: None,
model_metadata: None,
policy: HttpAuthorityPolicy::DenyByDefault,
})
.unwrap();
assert!(result.verdict.is_allowed());
assert_eq!(result.receipt.capability_id.as_deref(), Some("cap-123"));
assert_eq!(result.receipt.session_id.as_deref(), Some("session-1"));
assert!(
metadata_string(result.receipt.metadata.as_ref(), CHIO_KERNEL_RECEIPT_ID_KEY).is_some()
);
}
#[test]
fn capability_hint_mismatch_becomes_denial() {
let query = HashMap::new();
let (authority, issuer) = authority_with_issuer();
let capability = signed_capability_token_json(&issuer, "cap-123");
let result = authority
.evaluate(HttpAuthorityInput {
request_id: "req-4".to_string(),
method: HttpMethod::Put,
route_pattern: "/pets/42".to_string(),
path: "/pets/42",
query: &query,
caller: caller(),
body_hash: None,
body_length: 0,
session_id: None,
capability_id_hint: Some("cap-other"),
presented_capability: Some(&capability),
requested_tool_server: None,
requested_tool_name: None,
requested_arguments: None,
model_metadata: None,
policy: HttpAuthorityPolicy::DenyByDefault,
})
.unwrap();
assert!(result.verdict.is_denied());
assert!(result.receipt.capability_id.is_none());
}
#[test]
fn untrusted_capability_denies_deny_by_default() {
let query = HashMap::new();
let authority = authority();
let capability = signed_capability_token_json(&Keypair::generate(), "cap-untrusted");
let result = authority
.evaluate(HttpAuthorityInput {
request_id: "req-untrusted".to_string(),
method: HttpMethod::Post,
route_pattern: "/pets".to_string(),
path: "/pets",
query: &query,
caller: caller(),
body_hash: Some("ghi".to_string()),
body_length: 3,
session_id: None,
capability_id_hint: None,
presented_capability: Some(&capability),
requested_tool_server: None,
requested_tool_name: None,
requested_arguments: None,
model_metadata: None,
policy: HttpAuthorityPolicy::DenyByDefault,
})
.unwrap();
assert!(result.verdict.is_denied());
assert_eq!(result.receipt.capability_id, None);
assert_eq!(
result.receipt.evidence[0].details.as_deref(),
Some("capability issuer is not trusted")
);
}
#[test]
fn configured_external_issuer_allows_deny_by_default() {
let query = HashMap::new();
let external_issuer = Keypair::generate();
let authority = authority_with_trusted_issuer(external_issuer.public_key());
let capability = signed_capability_token_json(&external_issuer, "cap-external");
let result = authority
.evaluate(HttpAuthorityInput {
request_id: "req-external".to_string(),
method: HttpMethod::Post,
route_pattern: "/pets".to_string(),
path: "/pets",
query: &query,
caller: caller(),
body_hash: Some("issuer".to_string()),
body_length: 6,
session_id: None,
capability_id_hint: None,
presented_capability: Some(&capability),
requested_tool_server: None,
requested_tool_name: None,
requested_arguments: None,
model_metadata: None,
policy: HttpAuthorityPolicy::DenyByDefault,
})
.unwrap();
assert!(result.verdict.is_allowed());
assert_eq!(
result.receipt.capability_id.as_deref(),
Some("cap-external")
);
}
#[test]
fn finalized_receipt_links_decision_receipt_and_kernel_receipt() {
let query = HashMap::new();
let shared = authority();
let decision = shared
.evaluate(HttpAuthorityInput {
request_id: "req-5".to_string(),
method: HttpMethod::Get,
route_pattern: "/pets".to_string(),
path: "/pets",
query: &query,
caller: caller(),
body_hash: None,
body_length: 0,
session_id: None,
capability_id_hint: None,
presented_capability: None,
requested_tool_server: None,
requested_tool_name: None,
requested_arguments: None,
model_metadata: None,
policy: HttpAuthorityPolicy::SessionAllow,
})
.unwrap()
.receipt;
let kernel_receipt_id =
metadata_string(decision.metadata.as_ref(), CHIO_KERNEL_RECEIPT_ID_KEY)
.map(ToOwned::to_owned)
.unwrap();
let final_receipt = shared.finalize_decision_receipt(&decision, 204).unwrap();
assert_ne!(final_receipt.id, decision.id);
assert_eq!(final_receipt.response_status, 204);
assert_eq!(
http_status_scope(final_receipt.metadata.as_ref()),
Some(CHIO_HTTP_STATUS_SCOPE_FINAL)
);
assert_eq!(
final_receipt
.metadata
.as_ref()
.and_then(|metadata| metadata.get(CHIO_DECISION_RECEIPT_ID_KEY))
.and_then(serde_json::Value::as_str),
Some(decision.id.as_str())
);
assert_eq!(
metadata_string(final_receipt.metadata.as_ref(), CHIO_KERNEL_RECEIPT_ID_KEY),
Some(kernel_receipt_id.as_str())
);
assert_eq!(
metadata_value(final_receipt.metadata.as_ref(), "route_selection")
.and_then(|value| value.get("selectedTargetProtocol"))
.and_then(Value::as_str),
Some("native")
);
}
#[test]
fn extract_approval_id_parses_resume_path() {
assert_eq!(
extract_approval_id(Some(
"kernel returned PendingApproval; resume via /approvals/ap-123/respond"
))
.as_deref(),
Some("ap-123")
);
assert_eq!(
extract_approval_id(Some("kernel returned PendingApproval; approval_id=ap-456"))
.as_deref(),
Some("ap-456")
);
assert_eq!(
extract_approval_id(Some("kernel returned PendingApproval; approval_id: ap-789"))
.as_deref(),
Some("ap-789")
);
assert!(extract_approval_id(Some("kernel returned PendingApproval")).is_none());
}
#[test]
fn pending_approval_id_reads_nested_metadata() {
let metadata = serde_json::json!({
"pending_approval": {
"approval_id": "ap-structured"
}
});
assert_eq!(
pending_approval_id(Some(&metadata), Some("kernel returned PendingApproval"))
.as_deref(),
Some("ap-structured")
);
}
#[test]
fn deny_by_default_requires_matching_tool_grant() {
let query = HashMap::new();
let (authority, issuer) = authority_with_issuer();
let capability = signed_capability_token_json_with_scope(
&issuer,
"cap-tool-scope",
ChioScope {
grants: vec![ToolGrant {
server_id: "math".to_string(),
tool_name: "double".to_string(),
operations: vec![Operation::Invoke],
constraints: Vec::new(),
max_invocations: None,
max_cost_per_invocation: None,
max_total_cost: None,
dpop_required: None,
}],
..ChioScope::default()
},
);
let result = authority
.evaluate(HttpAuthorityInput {
request_id: "req-tool-mismatch".to_string(),
method: HttpMethod::Post,
route_pattern: "/chio/tools/math/increment".to_string(),
path: "/chio/tools/math/increment",
query: &query,
caller: caller(),
body_hash: Some("toolhash".to_string()),
body_length: 8,
session_id: None,
capability_id_hint: None,
presented_capability: Some(&capability),
requested_tool_server: Some("math"),
requested_tool_name: Some("increment"),
requested_arguments: Some(&Value::Null),
model_metadata: None,
policy: HttpAuthorityPolicy::DenyByDefault,
})
.unwrap();
assert!(result.verdict.is_denied());
assert!(result.receipt.capability_id.is_none());
assert_eq!(
result.receipt.evidence[0].details.as_deref(),
Some("capability does not authorize tool increment on server math")
);
}
}