1use std::collections::{BTreeMap, HashMap};
2use std::sync::Arc;
3
4use chio_core_types::capability::{
5 scope::{ChioScope, ModelMetadata, Operation, ToolGrant},
6 token::{CapabilityToken, CapabilityTokenBody},
7};
8use chio_core_types::crypto::{Keypair, PublicKey};
9use chio_core_types::receipt::decision::Decision;
10use chio_core_types::receipt::metadata::GuardEvidence;
11use chio_cross_protocol::discovery::{DiscoveryProtocol, TargetProtocolRegistry};
12use chio_cross_protocol::routing::{plan_authoritative_route, route_selection_metadata};
13use chio_kernel::{
14 ApprovalStore, ChioKernel, ExecutionNonceConfig, ExecutionNonceStore, Guard, GuardContext,
15 GuardDecision, InMemoryApprovalStore, KernelConfig, KernelError, ReceiptStore, RevocationStore,
16 SignedExecutionNonce, ToolCallRequest, ToolCallResponse, ToolServerConnection,
17 Verdict as KernelVerdict, DEFAULT_CHECKPOINT_BATCH_SIZE, DEFAULT_MAX_STREAM_DURATION_SECS,
18 DEFAULT_MAX_STREAM_TOTAL_BYTES,
19};
20use serde::{Deserialize, Serialize};
21use serde_json::{Map, Value};
22use thiserror::Error;
23
24use crate::{
25 authority_projection::{
26 capability_binding, HttpKernelAuthorizationRequest, HttpKernelCapabilityState,
27 },
28 http_status_metadata_decision, http_status_metadata_final, CallerIdentity, ChioHttpRequest,
29 HttpMethod, HttpReceipt, HttpReceiptBody, Verdict, CHIO_KERNEL_RECEIPT_ID_KEY,
30};
31
32pub const HTTP_AUTHORITY_SERVER_ID: &str = "chio_http_authority";
34pub const HTTP_AUTHORITY_TOOL_NAME: &str = "authorize_http_request";
36const HTTP_AUTHORITY_TTL_SECS: u64 = 60;
37
38const KERNEL_DURABILITY_FAILCLOSED_GUARD: &str = "kernel.receipt_persistence";
44
45fn is_durability_failclosed_denial(response: &ToolCallResponse) -> bool {
50 matches!(
51 response.receipt.decision.as_ref(),
52 Some(Decision::Deny { guard, .. }) if guard == KERNEL_DURABILITY_FAILCLOSED_GUARD
53 )
54}
55
56#[must_use]
57pub fn http_authority_tool_grant() -> ToolGrant {
58 ToolGrant {
59 server_id: HTTP_AUTHORITY_SERVER_ID.to_string(),
60 tool_name: HTTP_AUTHORITY_TOOL_NAME.to_string(),
61 operations: vec![Operation::Invoke],
62 constraints: Vec::new(),
63 max_invocations: None,
64 max_cost_per_invocation: None,
65 max_total_cost: None,
66 dpop_required: None,
67 }
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
71#[serde(rename_all = "snake_case")]
72pub enum HttpAuthorityPolicy {
73 SessionAllow,
74 DenyByDefault,
75}
76
77#[derive(Clone)]
78pub struct HttpAuthority {
79 keypair: Arc<Keypair>,
80 policy_hash: String,
81 kernel: Arc<ChioKernel>,
82 kernel_subject: PublicKey,
83 kernel_agent_id: String,
84 approval_store: Arc<dyn ApprovalStore>,
85 trusted_capability_issuers: Vec<PublicKey>,
86}
87
88impl std::fmt::Debug for HttpAuthority {
89 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90 f.debug_struct("HttpAuthority")
91 .field("policy_hash", &self.policy_hash)
92 .field("kernel_agent_id", &self.kernel_agent_id)
93 .finish_non_exhaustive()
94 }
95}
96
97pub struct HttpAuthorityInput<'a> {
98 pub request_id: String,
99 pub method: HttpMethod,
100 pub route_pattern: String,
101 pub path: &'a str,
102 pub query: &'a HashMap<String, String>,
103 pub caller: CallerIdentity,
104 pub body_hash: Option<String>,
105 pub body_length: u64,
106 pub session_id: Option<String>,
107 pub capability_id_hint: Option<&'a str>,
108 pub presented_capability: Option<&'a str>,
109 pub requested_tool_server: Option<&'a str>,
110 pub requested_tool_name: Option<&'a str>,
111 pub requested_arguments: Option<&'a Value>,
112 pub model_metadata: Option<&'a ModelMetadata>,
113 pub unsupported_authorization_extension: Option<&'a str>,
114 pub execution_nonce: Option<&'a SignedExecutionNonce>,
115 pub policy: HttpAuthorityPolicy,
116}
117
118pub struct TransportDenyInput<'a> {
122 pub request_id: &'a str,
123 pub route_pattern: &'a str,
124 pub method: HttpMethod,
125 pub caller_identity_hash: &'a str,
126 pub content_hash: Option<&'a str>,
127 pub verdict: Verdict,
128}
129
130#[derive(Debug, Clone)]
131pub struct PreparedHttpEvaluation {
132 pub verdict: Verdict,
133 pub evidence: Vec<GuardEvidence>,
134 pub request_id: String,
135 pub route_pattern: String,
136 pub http_method: HttpMethod,
137 pub caller_identity_hash: String,
138 pub content_hash: String,
139 pub session_id: Option<String>,
140 pub capability_id: Option<String>,
141 pub kernel_receipt_id: String,
142 pub route_selection_metadata: Option<Value>,
143 pub execution_nonce: Option<SignedExecutionNonce>,
144}
145
146#[derive(Debug, Clone)]
147pub struct HttpAuthorityEvaluation {
148 pub verdict: Verdict,
149 pub receipt: HttpReceipt,
150 pub evidence: Vec<GuardEvidence>,
151 pub execution_nonce: Option<SignedExecutionNonce>,
152}
153
154#[derive(Debug, Error)]
155pub enum HttpAuthorityError {
156 #[error("failed to hash caller identity: {0}")]
157 CallerIdentity(String),
158
159 #[error("failed to compute content hash: {0}")]
160 ContentHash(String),
161
162 #[error("kernel-backed authorization failed: {0}")]
163 Kernel(String),
164
165 #[error("kernel-backed authorization requires approval")]
166 PendingApproval {
167 approval_id: Option<String>,
168 kernel_receipt_id: String,
169 },
170
171 #[error("failed to sign receipt: {0}")]
172 ReceiptSign(String),
173}
174
175fn is_dispatch_failure(error: &HttpAuthorityError) -> bool {
182 !matches!(error, HttpAuthorityError::PendingApproval { .. })
183}
184
185#[derive(Debug, Clone)]
186struct PresentedCapabilityState {
187 capability_id: Option<String>,
188 invalid_reason: Option<String>,
189}
190
191#[derive(Clone, Copy)]
192struct RequestedToolInvocation<'a> {
193 server_id: &'a str,
194 tool_name: &'a str,
195 arguments: &'a Value,
196}
197
198struct HttpAuthorizationServer;
199
200#[async_trait::async_trait]
201impl ToolServerConnection for HttpAuthorizationServer {
202 fn server_id(&self) -> &str {
203 HTTP_AUTHORITY_SERVER_ID
204 }
205
206 fn tool_names(&self) -> Vec<String> {
207 vec![HTTP_AUTHORITY_TOOL_NAME.to_string()]
208 }
209
210 async fn invoke(
211 &self,
212 tool_name: &str,
213 _arguments: serde_json::Value,
214 _nested_flow_bridge: Option<&mut dyn chio_kernel::NestedFlowBridge>,
215 ) -> Result<serde_json::Value, KernelError> {
216 if tool_name != HTTP_AUTHORITY_TOOL_NAME {
217 return Err(KernelError::Internal(format!(
218 "unsupported HTTP authority tool: {tool_name}"
219 )));
220 }
221 Ok(serde_json::json!({ "authorized": true }))
222 }
223}
224
225struct HttpProjectionGuard;
226
227impl Guard for HttpProjectionGuard {
228 fn name(&self) -> &str {
229 "http_projection_policy"
230 }
231
232 fn evaluate(&self, ctx: &GuardContext<'_>) -> Result<GuardDecision, KernelError> {
233 let projected: HttpKernelAuthorizationRequest =
234 serde_json::from_value(ctx.request.arguments.clone()).map_err(|error| {
235 KernelError::Internal(format!(
236 "failed to decode projected HTTP authorization request: {error}"
237 ))
238 })?;
239
240 if let Some(reason) = projected.capability.invalid_reason {
241 return Err(KernelError::GuardDenied(reason));
242 }
243
244 match projected.policy {
245 HttpAuthorityPolicy::SessionAllow => Ok(GuardDecision::allow()),
246 HttpAuthorityPolicy::DenyByDefault => {
247 if projected.capability.id.is_some() {
248 Ok(GuardDecision::allow())
249 } else {
250 Err(KernelError::GuardDenied(
251 "side-effect route requires a capability token".to_string(),
252 ))
253 }
254 }
255 }
256 }
257}
258
259struct DurableAdmissionStores {
265 store: Arc<dyn chio_kernel::QualifiedAdmissionProjectionStore>,
266 outcome_store: Arc<dyn chio_kernel::tool_outcome::QualifiedToolOutcomeStore>,
267 fence: chio_kernel::admission_operation::StoreMutationFence,
268}
269
270#[derive(Default)]
271pub struct HttpAuthorityBuilder {
272 approval_store: Option<Arc<dyn ApprovalStore>>,
273 receipt_store: Option<Arc<dyn ReceiptStore>>,
274 revocation_store: Option<Arc<dyn RevocationStore>>,
275 durable_admission: Option<DurableAdmissionStores>,
276 trusted_capability_issuers: Vec<PublicKey>,
277 allow_ephemeral_receipt_log: bool,
278 allow_ephemeral_revocation_store: bool,
279}
280
281impl HttpAuthorityBuilder {
282 #[must_use]
283 pub fn receipt_store(mut self, store: Arc<dyn ReceiptStore>) -> Self {
284 self.receipt_store = Some(store);
285 self
286 }
287
288 #[must_use]
289 pub fn revocation_store(mut self, store: Arc<dyn RevocationStore>) -> Self {
290 self.revocation_store = Some(store);
291 self
292 }
293
294 #[must_use]
295 pub fn durable_admission_stores(
296 mut self,
297 store: Arc<dyn chio_kernel::QualifiedAdmissionProjectionStore>,
298 outcome_store: Arc<dyn chio_kernel::tool_outcome::QualifiedToolOutcomeStore>,
299 fence: chio_kernel::admission_operation::StoreMutationFence,
300 ) -> Self {
301 self.durable_admission = Some(DurableAdmissionStores {
302 store,
303 outcome_store,
304 fence,
305 });
306 self
307 }
308
309 #[must_use]
310 pub fn approval_store(mut self, store: Arc<dyn ApprovalStore>) -> Self {
311 self.approval_store = Some(store);
312 self
313 }
314
315 #[must_use]
316 pub fn trusted_capability_issuers(mut self, issuers: Vec<PublicKey>) -> Self {
317 self.trusted_capability_issuers = issuers;
318 self
319 }
320
321 #[must_use]
322 pub fn allow_ephemeral_receipt_log(mut self, allow: bool) -> Self {
323 self.allow_ephemeral_receipt_log = allow;
324 self
325 }
326
327 #[must_use]
328 pub fn allow_ephemeral_revocation_store(mut self, allow: bool) -> Self {
329 self.allow_ephemeral_revocation_store = allow;
330 self
331 }
332
333 pub fn build(
334 self,
335 keypair: Keypair,
336 policy_hash: String,
337 ) -> Result<HttpAuthority, HttpAuthorityError> {
338 let approval_store = self
339 .approval_store
340 .unwrap_or_else(|| Arc::new(InMemoryApprovalStore::new()));
341 let keypair = Arc::new(keypair);
342 let signer_public_key = keypair.public_key();
343 let mut trusted = self.trusted_capability_issuers;
344 if !trusted.contains(&signer_public_key) {
345 trusted.push(signer_public_key.clone());
346 }
347 let kernel_subject = Keypair::generate().public_key();
348 let kernel_agent_id = kernel_subject.to_hex();
349
350 let mut kernel = ChioKernel::new(HttpAuthority::kernel_config(
351 keypair.as_ref().clone(),
352 trusted.clone(),
353 policy_hash.clone(),
354 self.allow_ephemeral_receipt_log,
355 self.allow_ephemeral_revocation_store,
356 ));
357 if let Some(store) = self.receipt_store {
358 kernel
359 .set_receipt_store_handle(store)
360 .map_err(|error| HttpAuthorityError::Kernel(error.to_string()))?;
361 }
362 if let Some(store) = self.revocation_store {
363 kernel.set_revocation_store_handle(store);
364 }
365 if let Some(durable) = self.durable_admission {
366 kernel
367 .set_durable_admission_store(durable.store, durable.outcome_store, durable.fence)
368 .map_err(|error| HttpAuthorityError::Kernel(error.to_string()))?;
369 }
370 kernel.register_tool_server(Box::new(HttpAuthorizationServer));
371 kernel.add_guard(Box::new(HttpProjectionGuard));
372
373 Ok(HttpAuthority {
374 keypair,
375 policy_hash,
376 kernel: Arc::new(kernel),
377 kernel_subject,
378 kernel_agent_id,
379 approval_store,
380 trusted_capability_issuers: trusted,
381 })
382 }
383}
384
385impl HttpAuthority {
386 #[must_use]
390 pub fn builder() -> HttpAuthorityBuilder {
391 HttpAuthorityBuilder::default()
392 }
393
394 #[must_use]
401 pub fn new(keypair: Keypair, policy_hash: String) -> Self {
402 Self::assemble(
403 keypair,
404 policy_hash,
405 Arc::new(InMemoryApprovalStore::new()),
406 Vec::new(),
407 false,
408 false,
409 )
410 }
411
412 #[must_use]
415 pub fn new_ephemeral(keypair: Keypair, policy_hash: String) -> Self {
416 Self::assemble(
417 keypair,
418 policy_hash,
419 Arc::new(InMemoryApprovalStore::new()),
420 Vec::new(),
421 true,
422 true,
423 )
424 }
425
426 #[must_use]
434 pub fn new_with_approval_store(
435 keypair: Keypair,
436 policy_hash: String,
437 approval_store: Arc<dyn ApprovalStore>,
438 ) -> Self {
439 Self::assemble(
440 keypair,
441 policy_hash,
442 approval_store,
443 Vec::new(),
444 false,
445 false,
446 )
447 }
448
449 #[must_use]
453 pub fn new_with_approval_store_and_trusted_issuers(
454 keypair: Keypair,
455 policy_hash: String,
456 approval_store: Arc<dyn ApprovalStore>,
457 trusted_capability_issuers: Vec<PublicKey>,
458 ) -> Self {
459 Self::assemble(
460 keypair,
461 policy_hash,
462 approval_store,
463 trusted_capability_issuers,
464 false,
465 false,
466 )
467 }
468
469 #[must_use]
476 pub fn new_ephemeral_with_approval_store_and_trusted_issuers(
477 keypair: Keypair,
478 policy_hash: String,
479 approval_store: Arc<dyn ApprovalStore>,
480 trusted_capability_issuers: Vec<PublicKey>,
481 ) -> Self {
482 Self::assemble(
483 keypair,
484 policy_hash,
485 approval_store,
486 trusted_capability_issuers,
487 true,
488 true,
489 )
490 }
491
492 fn assemble(
497 keypair: Keypair,
498 policy_hash: String,
499 approval_store: Arc<dyn ApprovalStore>,
500 mut trusted_capability_issuers: Vec<PublicKey>,
501 allow_ephemeral_receipt_log: bool,
502 allow_ephemeral_revocation_store: bool,
503 ) -> Self {
504 let keypair = Arc::new(keypair);
505 let signer_public_key = keypair.public_key();
506 if !trusted_capability_issuers.contains(&signer_public_key) {
507 trusted_capability_issuers.push(signer_public_key.clone());
508 }
509 let kernel_subject = Keypair::generate().public_key();
510 let kernel_agent_id = kernel_subject.to_hex();
511
512 let mut kernel = ChioKernel::new(Self::kernel_config(
513 keypair.as_ref().clone(),
514 trusted_capability_issuers.clone(),
515 policy_hash.clone(),
516 allow_ephemeral_receipt_log,
517 allow_ephemeral_revocation_store,
518 ));
519 kernel.register_tool_server(Box::new(HttpAuthorizationServer));
520 kernel.add_guard(Box::new(HttpProjectionGuard));
521
522 Self {
523 keypair,
524 policy_hash,
525 kernel: Arc::new(kernel),
526 kernel_subject,
527 kernel_agent_id,
528 approval_store,
529 trusted_capability_issuers,
530 }
531 }
532
533 fn kernel_config(
537 keypair: Keypair,
538 ca_public_keys: Vec<PublicKey>,
539 policy_hash: String,
540 allow_ephemeral_receipt_log: bool,
541 allow_ephemeral_revocation_store: bool,
542 ) -> KernelConfig {
543 KernelConfig {
544 keypair,
545 ca_public_keys,
546 max_delegation_depth: 8,
547 policy_hash,
548 allow_sampling: false,
549 allow_sampling_tool_use: false,
550 allow_elicitation: false,
551 max_stream_duration_secs: DEFAULT_MAX_STREAM_DURATION_SECS,
552 max_stream_total_bytes: DEFAULT_MAX_STREAM_TOTAL_BYTES,
553 require_web3_evidence: false,
554 allow_ephemeral_receipt_log,
555 allow_ephemeral_revocation_store,
556 checkpoint_batch_size: DEFAULT_CHECKPOINT_BATCH_SIZE,
557 retention_config: None,
558 memory_budget: chio_kernel::MemoryBudgetConfig::defaults(),
559 deadlines: chio_kernel::HotPathDeadlineConfig::default(),
560 }
561 }
562
563 #[must_use]
564 pub fn approval_store(&self) -> Arc<dyn ApprovalStore> {
565 Arc::clone(&self.approval_store)
566 }
567
568 pub fn set_execution_nonce_store(
569 &mut self,
570 config: ExecutionNonceConfig,
571 store: Box<dyn ExecutionNonceStore>,
572 ) -> Result<(), HttpAuthorityError> {
573 let Some(kernel) = Arc::get_mut(&mut self.kernel) else {
574 return Err(HttpAuthorityError::Kernel(
575 "execution nonce store cannot be configured after HTTP authority is cloned"
576 .to_string(),
577 ));
578 };
579 kernel.set_execution_nonce_store(config, store);
580 Ok(())
581 }
582
583 fn trusted_capability_issuers(&self) -> &[PublicKey] {
584 &self.trusted_capability_issuers
585 }
586
587 pub fn evaluate(
588 &self,
589 input: HttpAuthorityInput<'_>,
590 ) -> Result<HttpAuthorityEvaluation, HttpAuthorityError> {
591 let started_at = std::time::Instant::now();
592 let result = self.prepare(input).and_then(|prepared| {
593 let receipt = self.sign_decision_receipt(&prepared)?;
594 Ok((prepared, receipt))
595 });
596 let elapsed_nanos = u64::try_from(started_at.elapsed().as_nanos()).unwrap_or(u64::MAX);
597 match result {
598 Ok((prepared, receipt)) => {
599 let outcome = if prepared.verdict.is_allowed() {
606 crate::metrics::GUARD_OUTCOME_ALLOW
607 } else {
608 crate::metrics::GUARD_OUTCOME_DENY
609 };
610 crate::metrics::observe_decision_latency_nanos_for_outcome(outcome, elapsed_nanos);
611 crate::metrics::record_guard_evaluation(outcome);
612 Ok(HttpAuthorityEvaluation {
613 verdict: prepared.verdict.clone(),
614 receipt,
615 evidence: prepared.evidence.clone(),
616 execution_nonce: prepared.execution_nonce.clone(),
617 })
618 }
619 Err(error) => {
620 if is_dispatch_failure(&error) {
629 crate::metrics::observe_decision_latency_nanos_for_outcome(
630 crate::metrics::GUARD_OUTCOME_ERROR,
631 elapsed_nanos,
632 );
633 crate::metrics::record_guard_evaluation(crate::metrics::GUARD_OUTCOME_ERROR);
634 crate::metrics::record_dispatch_failure(
635 crate::metrics::GUARD_LABEL_HTTP_AUTHORITY,
636 "error",
637 );
638 }
639 Err(error)
640 }
641 }
642 }
643
644 pub fn prepare(
645 &self,
646 input: HttpAuthorityInput<'_>,
647 ) -> Result<PreparedHttpEvaluation, HttpAuthorityError> {
648 let caller_identity_hash = input
649 .caller
650 .identity_hash()
651 .map_err(|e| HttpAuthorityError::CallerIdentity(e.to_string()))?;
652 let binding = capability_binding(&input, &caller_identity_hash);
653 let unsupported_reason = input.unsupported_authorization_extension.map(|field| {
654 format!("HTTP authority projection does not support authorization field {field}")
655 });
656 let presented_capability =
657 if let Some(reason) = unsupported_reason.or_else(|| binding.invalid_reason.clone()) {
658 PresentedCapabilityState {
659 capability_id: None,
660 invalid_reason: Some(reason),
661 }
662 } else {
663 validate_presented_capability(
664 input.capability_id_hint,
665 input.presented_capability,
666 self.trusted_capability_issuers(),
667 binding.requested_tool_server.as_deref(),
668 binding.requested_tool_name.as_deref(),
669 binding.requested_arguments.as_ref(),
670 input.model_metadata,
671 &|capability_id| {
672 self.kernel
673 .is_capability_revoked(capability_id)
674 .map_err(|error| HttpAuthorityError::Kernel(error.to_string()))
675 },
676 )
677 };
678
679 let chio_request = ChioHttpRequest {
680 request_id: input.request_id.clone(),
681 method: input.method,
682 route_pattern: input.route_pattern.clone(),
683 path: input.path.to_string(),
684 query: input.query.clone(),
685 headers: HashMap::new(),
686 caller: input.caller,
687 body_hash: input.body_hash,
688 body_length: input.body_length,
689 session_id: input.session_id.clone(),
690 capability_id: presented_capability.capability_id.clone(),
691 tool_server: binding.requested_tool_server.clone(),
692 tool_name: binding.requested_tool_name.clone(),
693 arguments: binding.requested_arguments.clone(),
694 model_metadata: input.model_metadata.cloned(),
695 governed_intent: None,
696 approval_token: None,
697 approval_tokens: None,
698 threshold_approval_proposal: None,
699 supplemental_authorization: None,
700 execution_nonce: input.execution_nonce.cloned(),
701 timestamp: chrono::Utc::now().timestamp() as u64,
702 };
703
704 let content_hash = chio_request
705 .content_hash()
706 .map_err(|e| HttpAuthorityError::ContentHash(e.to_string()))?;
707
708 let kernel_response = self.authorize_via_kernel(
709 &input.request_id,
710 input.method,
711 &input.route_pattern,
712 input.path,
713 &content_hash,
714 &caller_identity_hash,
715 input.session_id.as_deref(),
716 binding.policy,
717 &presented_capability,
718 input.execution_nonce,
719 )?;
720
721 if is_durability_failclosed_denial(&kernel_response) {
729 let reason = kernel_response
730 .reason
731 .clone()
732 .unwrap_or_else(|| "kernel denied for missing durable persistence".to_string());
733 return Err(HttpAuthorityError::Kernel(reason));
734 }
735
736 let verdict = projected_verdict(binding.policy, &presented_capability);
737 let expected_allowed = verdict.is_allowed();
738 match (kernel_response.verdict, expected_allowed) {
739 (KernelVerdict::Allow, true) | (KernelVerdict::Deny, false) => {}
740 (KernelVerdict::Allow, false) => {
741 return Err(HttpAuthorityError::Kernel(
742 "kernel allowed an HTTP projection that should have been denied".to_string(),
743 ));
744 }
745 (KernelVerdict::Deny, true) => {
746 let reason = kernel_response
747 .reason
748 .unwrap_or_else(|| "kernel denied an allowed HTTP projection".to_string());
749 return Err(HttpAuthorityError::Kernel(reason));
750 }
751 (KernelVerdict::PendingApproval, _) => {
752 return Err(HttpAuthorityError::PendingApproval {
753 approval_id: pending_approval_id(
754 kernel_response.receipt.metadata.as_ref(),
755 kernel_response.reason.as_deref(),
756 ),
757 kernel_receipt_id: kernel_response.receipt.id,
758 });
759 }
760 }
761 if is_execution_nonce_preflight(&kernel_response) {
762 let evidence = projected_evidence(binding.policy, &presented_capability);
763 return Ok(PreparedHttpEvaluation {
764 verdict: Verdict::Incomplete {
765 reason: "execution nonce preflight requires retry with presented nonce"
766 .to_string(),
767 },
768 evidence,
769 request_id: input.request_id,
770 route_pattern: input.route_pattern,
771 http_method: input.method,
772 caller_identity_hash,
773 content_hash,
774 session_id: input.session_id,
775 capability_id: presented_capability.capability_id,
776 kernel_receipt_id: kernel_response.receipt.id,
777 route_selection_metadata: metadata_value(
778 kernel_response.receipt.metadata.as_ref(),
779 "route_selection",
780 )
781 .cloned(),
782 execution_nonce: kernel_response.execution_nonce.as_deref().cloned(),
783 });
784 }
785
786 let evidence = projected_evidence(binding.policy, &presented_capability);
787
788 Ok(PreparedHttpEvaluation {
789 verdict,
790 evidence,
791 request_id: input.request_id,
792 route_pattern: input.route_pattern,
793 http_method: input.method,
794 caller_identity_hash,
795 content_hash,
796 session_id: input.session_id,
797 capability_id: presented_capability.capability_id,
798 kernel_receipt_id: kernel_response.receipt.id,
799 route_selection_metadata: metadata_value(
800 kernel_response.receipt.metadata.as_ref(),
801 "route_selection",
802 )
803 .cloned(),
804 execution_nonce: kernel_response.execution_nonce.as_deref().cloned(),
805 })
806 }
807
808 pub fn sign_decision_receipt(
809 &self,
810 prepared: &PreparedHttpEvaluation,
811 ) -> Result<HttpReceipt, HttpAuthorityError> {
812 self.sign_receipt(
813 prepared,
814 decision_status(&prepared.verdict),
815 decision_metadata(
816 Some(&prepared.kernel_receipt_id),
817 prepared.route_selection_metadata.as_ref(),
818 ),
819 )
820 }
821
822 pub fn finalize_receipt(
823 &self,
824 prepared: &PreparedHttpEvaluation,
825 response_status: u16,
826 decision_receipt_id: Option<&str>,
827 ) -> Result<HttpReceipt, HttpAuthorityError> {
828 self.sign_receipt(
829 prepared,
830 response_status,
831 final_metadata(
832 decision_receipt_id,
833 Some(&prepared.kernel_receipt_id),
834 prepared.route_selection_metadata.as_ref(),
835 ),
836 )
837 }
838
839 pub fn sign_transport_deny_receipt(
847 &self,
848 input: TransportDenyInput<'_>,
849 ) -> Result<HttpReceipt, HttpAuthorityError> {
850 if !input.verdict.is_denied() {
851 return Err(HttpAuthorityError::Kernel(
852 "sign_transport_deny_receipt requires a Deny verdict".to_string(),
853 ));
854 }
855 let response_status = decision_status(&input.verdict);
856 let body = HttpReceiptBody {
857 id: uuid::Uuid::now_v7().to_string(),
858 request_id: input.request_id.to_string(),
859 route_pattern: input.route_pattern.to_string(),
860 method: input.method,
861 caller_identity_hash: input.caller_identity_hash.to_string(),
862 session_id: None,
863 verdict: input.verdict,
864 receipt_kind: chio_core_types::receipt::kinds::ReceiptKind::MediatedDecision,
865 boundary_class: chio_core_types::receipt::kinds::BoundaryClass::Prevent,
866 observation_outcome: None,
867 tool_origin: chio_core_types::receipt::kinds::ToolOrigin::CallerExecuted,
868 redaction_mode: chio_core_types::receipt::kinds::RedactionMode::None,
869 actor_chain: Vec::new(),
870 evidence: Vec::new(),
871 response_status,
872 timestamp: chrono::Utc::now().timestamp() as u64,
873 content_hash: input.content_hash.unwrap_or_default().to_string(),
874 policy_hash: self.policy_hash.clone(),
875 trust_level: chio_core_types::receipt::kinds::TrustLevel::Mediated,
876 capability_id: None,
877 metadata: Some(http_status_metadata_final(None)),
878 kernel_key: self.keypair.public_key(),
879 };
880 HttpReceipt::sign(body, self.keypair.as_ref())
881 .map_err(|e| HttpAuthorityError::ReceiptSign(e.to_string()))
882 }
883
884 pub fn finalize_decision_receipt(
885 &self,
886 decision_receipt: &HttpReceipt,
887 response_status: u16,
888 ) -> Result<HttpReceipt, HttpAuthorityError> {
889 let mut body = decision_receipt.body();
890 let decision_receipt_id = body.id.clone();
891 let kernel_receipt_id = metadata_string(body.metadata.as_ref(), CHIO_KERNEL_RECEIPT_ID_KEY)
892 .map(ToOwned::to_owned);
893 let route_selection = metadata_value(body.metadata.as_ref(), "route_selection").cloned();
894 body.id = uuid::Uuid::now_v7().to_string();
895 body.response_status = response_status;
896 body.timestamp = chrono::Utc::now().timestamp() as u64;
897 body.metadata = final_metadata(
898 Some(&decision_receipt_id),
899 kernel_receipt_id.as_deref(),
900 route_selection.as_ref(),
901 );
902 HttpReceipt::sign(body, self.keypair.as_ref())
903 .map_err(|e| HttpAuthorityError::ReceiptSign(e.to_string()))
904 }
905
906 #[allow(clippy::too_many_arguments)]
907 fn authorize_via_kernel(
908 &self,
909 request_id: &str,
910 method: HttpMethod,
911 route_pattern: &str,
912 path: &str,
913 content_hash: &str,
914 caller_identity_hash: &str,
915 session_id: Option<&str>,
916 policy: HttpAuthorityPolicy,
917 presented_capability: &PresentedCapabilityState,
918 execution_nonce: Option<&SignedExecutionNonce>,
919 ) -> Result<chio_kernel::ToolCallResponse, HttpAuthorityError> {
920 let capability = match execution_nonce {
921 Some(nonce) => self.kernel_capability_for_nonce_retry(nonce)?,
922 None => self
923 .kernel
924 .issue_capability(
925 &self.kernel_subject,
926 kernel_scope(),
927 HTTP_AUTHORITY_TTL_SECS,
928 )
929 .map_err(|error| HttpAuthorityError::Kernel(error.to_string()))?,
930 };
931
932 let projected = HttpKernelAuthorizationRequest {
933 method,
934 route_pattern: route_pattern.to_string(),
935 path: path.to_string(),
936 content_hash: content_hash.to_string(),
937 caller_identity_hash: caller_identity_hash.to_string(),
938 session_id: session_id.map(ToOwned::to_owned),
939 policy,
940 capability: HttpKernelCapabilityState {
941 id: presented_capability.capability_id.clone(),
942 invalid_reason: presented_capability.invalid_reason.clone(),
943 },
944 };
945
946 let request = ToolCallRequest {
947 request_id: request_id.to_string(),
948 capability,
949 tool_name: HTTP_AUTHORITY_TOOL_NAME.to_string(),
950 server_id: HTTP_AUTHORITY_SERVER_ID.to_string(),
951 agent_id: self.kernel_agent_id.clone(),
952 arguments: serde_json::to_value(projected)
953 .map_err(|error| HttpAuthorityError::Kernel(error.to_string()))?,
954 dpop_proof: None,
955 execution_nonce: execution_nonce.cloned(),
956 governed_intent: None,
957 approval_token: None,
958 approval_tokens: Vec::new(),
959 threshold_approval_proposal: None,
960 supplemental_authorization: None,
961 model_metadata: None,
962 federated_origin_kernel_id: None,
963 };
964 let route_plan = plan_authoritative_route(
965 request_id,
966 DiscoveryProtocol::Http,
967 DiscoveryProtocol::Native,
968 None,
969 &TargetProtocolRegistry::new(DiscoveryProtocol::Native),
970 &BTreeMap::new(),
971 )
972 .map_err(|error| HttpAuthorityError::Kernel(error.to_string()))?;
973 let route_metadata = route_selection_metadata(&route_plan.evidence)
974 .map_err(|error| HttpAuthorityError::Kernel(error.to_string()))?;
975
976 self.kernel
977 .evaluate_tool_call_blocking_with_metadata(&request, Some(route_metadata))
978 .map_err(|error| HttpAuthorityError::Kernel(error.to_string()))
979 }
980
981 fn kernel_capability_for_nonce_retry(
982 &self,
983 nonce: &SignedExecutionNonce,
984 ) -> Result<CapabilityToken, HttpAuthorityError> {
985 let now = chrono::Utc::now().timestamp();
986 let issued_at = u64::try_from(now.max(0))
987 .map_err(|error| HttpAuthorityError::Kernel(error.to_string()))?;
988 let body = CapabilityTokenBody {
989 id: nonce.nonce.bound_to.capability_id.clone(),
990 issuer: self.keypair.public_key(),
991 subject: self.kernel_subject.clone(),
992 scope: kernel_scope(),
993 issued_at,
994 expires_at: issued_at.saturating_add(HTTP_AUTHORITY_TTL_SECS),
995 delegation_chain: vec![],
996 aggregate_invocation_budget: None,
997 };
998 CapabilityToken::sign(body, self.keypair.as_ref())
999 .map_err(|error| HttpAuthorityError::Kernel(error.to_string()))
1000 }
1001
1002 fn sign_receipt(
1003 &self,
1004 prepared: &PreparedHttpEvaluation,
1005 response_status: u16,
1006 metadata: Option<Value>,
1007 ) -> Result<HttpReceipt, HttpAuthorityError> {
1008 let body = HttpReceiptBody {
1009 id: uuid::Uuid::now_v7().to_string(),
1010 request_id: prepared.request_id.clone(),
1011 route_pattern: prepared.route_pattern.clone(),
1012 method: prepared.http_method,
1013 caller_identity_hash: prepared.caller_identity_hash.clone(),
1014 session_id: prepared.session_id.clone(),
1015 verdict: prepared.verdict.clone(),
1016 receipt_kind: chio_core_types::receipt::kinds::ReceiptKind::MediatedDecision,
1017 boundary_class: chio_core_types::receipt::kinds::BoundaryClass::Prevent,
1018 observation_outcome: None,
1019 tool_origin: chio_core_types::receipt::kinds::ToolOrigin::CallerExecuted,
1020 redaction_mode: chio_core_types::receipt::kinds::RedactionMode::None,
1021 actor_chain: Vec::new(),
1022 evidence: prepared.evidence.clone(),
1023 response_status,
1024 timestamp: chrono::Utc::now().timestamp() as u64,
1025 content_hash: prepared.content_hash.clone(),
1026 policy_hash: self.policy_hash.clone(),
1027 trust_level: chio_core_types::receipt::kinds::TrustLevel::Mediated,
1028 capability_id: prepared.capability_id.clone(),
1029 metadata,
1030 kernel_key: self.keypair.public_key(),
1031 };
1032
1033 HttpReceipt::sign(body, self.keypair.as_ref())
1034 .map_err(|e| HttpAuthorityError::ReceiptSign(e.to_string()))
1035 }
1036}
1037
1038fn kernel_scope() -> ChioScope {
1039 ChioScope {
1040 grants: vec![ToolGrant {
1041 server_id: HTTP_AUTHORITY_SERVER_ID.to_string(),
1042 tool_name: HTTP_AUTHORITY_TOOL_NAME.to_string(),
1043 operations: vec![Operation::Invoke],
1044 constraints: Vec::new(),
1045 max_invocations: None,
1046 max_cost_per_invocation: None,
1047 max_total_cost: None,
1048 dpop_required: None,
1049 }],
1050 resource_grants: Vec::new(),
1051 prompt_grants: Vec::new(),
1052 }
1053}
1054
1055fn decision_status(verdict: &Verdict) -> u16 {
1056 match verdict {
1057 Verdict::Allow => 200,
1058 Verdict::Deny { http_status, .. } => *http_status,
1059 Verdict::Cancel { .. } | Verdict::Incomplete { .. } => 500,
1060 }
1061}
1062
1063#[allow(clippy::too_many_arguments)]
1064fn validate_presented_capability(
1065 capability_id_hint: Option<&str>,
1066 presented_capability: Option<&str>,
1067 trusted_issuers: &[PublicKey],
1068 requested_tool_server: Option<&str>,
1069 requested_tool_name: Option<&str>,
1070 requested_arguments: Option<&Value>,
1071 model_metadata: Option<&ModelMetadata>,
1072 is_revoked: &dyn Fn(&str) -> Result<bool, HttpAuthorityError>,
1073) -> PresentedCapabilityState {
1074 let requested_tool = match (requested_tool_server, requested_tool_name) {
1075 (Some(server_id), Some(tool_name)) => Some(RequestedToolInvocation {
1076 server_id,
1077 tool_name,
1078 arguments: requested_arguments.unwrap_or(&Value::Null),
1079 }),
1080 (None, None) => None,
1081 _ => {
1082 return PresentedCapabilityState {
1083 capability_id: None,
1084 invalid_reason: Some(
1085 "tool-call evaluation requires both tool_server and tool_name".to_string(),
1086 ),
1087 };
1088 }
1089 };
1090 let Some(raw_capability) = presented_capability else {
1091 return PresentedCapabilityState {
1092 capability_id: None,
1093 invalid_reason: None,
1094 };
1095 };
1096
1097 match validate_capability_token(
1098 raw_capability,
1099 trusted_issuers,
1100 requested_tool,
1101 model_metadata,
1102 ) {
1103 Ok(token) => {
1104 if let Some(hint) = capability_id_hint {
1105 if hint != token.id {
1106 return PresentedCapabilityState {
1107 capability_id: None,
1108 invalid_reason: Some(
1109 "capability_id does not match the presented capability token"
1110 .to_string(),
1111 ),
1112 };
1113 }
1114 }
1115 match presented_capability_revocation(&token, is_revoked) {
1121 Ok(None) => PresentedCapabilityState {
1122 capability_id: Some(token.id),
1123 invalid_reason: None,
1124 },
1125 Ok(Some(reason)) | Err(reason) => PresentedCapabilityState {
1126 capability_id: None,
1127 invalid_reason: Some(reason),
1128 },
1129 }
1130 }
1131 Err(reason) => PresentedCapabilityState {
1132 capability_id: None,
1133 invalid_reason: Some(reason),
1134 },
1135 }
1136}
1137
1138fn presented_capability_revocation(
1144 token: &CapabilityToken,
1145 is_revoked: &dyn Fn(&str) -> Result<bool, HttpAuthorityError>,
1146) -> Result<Option<String>, String> {
1147 let chain_ids = token
1148 .delegation_chain
1149 .iter()
1150 .map(|link| link.capability_id.as_str());
1151 for capability_id in std::iter::once(token.id.as_str()).chain(chain_ids) {
1152 match is_revoked(capability_id) {
1153 Ok(false) => {}
1154 Ok(true) => {
1155 return Ok(Some(format!(
1156 "presented capability {capability_id} has been revoked"
1157 )))
1158 }
1159 Err(error) => return Err(format!("capability revocation status unavailable: {error}")),
1160 }
1161 }
1162 Ok(None)
1163}
1164
1165fn projected_verdict(
1166 policy: HttpAuthorityPolicy,
1167 presented_capability: &PresentedCapabilityState,
1168) -> Verdict {
1169 if let Some(reason) = &presented_capability.invalid_reason {
1170 return Verdict::deny(reason, "CapabilityGuard");
1171 }
1172
1173 match policy {
1174 HttpAuthorityPolicy::SessionAllow => Verdict::Allow,
1175 HttpAuthorityPolicy::DenyByDefault => match &presented_capability.capability_id {
1176 Some(_) => Verdict::Allow,
1177 None => Verdict::deny(
1178 "side-effect route requires a capability token",
1179 "CapabilityGuard",
1180 ),
1181 },
1182 }
1183}
1184
1185fn is_execution_nonce_preflight(response: &chio_kernel::ToolCallResponse) -> bool {
1186 response.verdict == KernelVerdict::Allow
1187 && response.execution_nonce.is_some()
1188 && response.output.is_none()
1189}
1190
1191fn projected_evidence(
1192 policy: HttpAuthorityPolicy,
1193 presented_capability: &PresentedCapabilityState,
1194) -> Vec<GuardEvidence> {
1195 if let Some(reason) = &presented_capability.invalid_reason {
1196 return vec![GuardEvidence {
1197 guard_name: "CapabilityGuard".to_string(),
1198 verdict: false,
1199 details: Some(reason.clone()),
1200 }];
1201 }
1202
1203 match policy {
1204 HttpAuthorityPolicy::SessionAllow => vec![GuardEvidence {
1205 guard_name: "DefaultPolicyGuard".to_string(),
1206 verdict: true,
1207 details: Some("safe method, session-scoped allow".to_string()),
1208 }],
1209 HttpAuthorityPolicy::DenyByDefault => match &presented_capability.capability_id {
1210 Some(_) => vec![GuardEvidence {
1211 guard_name: "CapabilityGuard".to_string(),
1212 verdict: true,
1213 details: Some("valid capability token presented".to_string()),
1214 }],
1215 None => vec![GuardEvidence {
1216 guard_name: "CapabilityGuard".to_string(),
1217 verdict: false,
1218 details: Some("side-effect route requires a valid capability token".to_string()),
1219 }],
1220 },
1221 }
1222}
1223
1224fn validate_capability_token(
1225 raw: &str,
1226 trusted_issuers: &[PublicKey],
1227 requested_tool: Option<RequestedToolInvocation<'_>>,
1228 model_metadata: Option<&ModelMetadata>,
1229) -> Result<CapabilityToken, String> {
1230 let token: CapabilityToken =
1231 serde_json::from_str(raw).map_err(|e| format!("invalid capability token: {e}"))?;
1232 if !trusted_issuers.contains(&token.issuer) {
1233 return Err("capability issuer is not trusted".to_string());
1234 }
1235 let signature_valid = token
1236 .verify_signature()
1237 .map_err(|e| format!("capability signature verification failed: {e}"))?;
1238 if !signature_valid {
1239 return Err("capability signature verification failed".to_string());
1240 }
1241 if token.attenuation_proof.is_some() {
1242 return Err(
1243 "chain-binding requires a trust-root resolver on the HTTP authority path".to_string(),
1244 );
1245 }
1246 token
1247 .validate_time(chrono::Utc::now().timestamp() as u64)
1248 .map_err(|e| format!("invalid capability token: {e}"))?;
1249
1250 if let Some(requested_tool) = requested_tool {
1251 let matches = chio_kernel::capability_matches_request_with_model_metadata(
1252 &token,
1253 requested_tool.tool_name,
1254 requested_tool.server_id,
1255 requested_tool.arguments,
1256 model_metadata,
1257 )
1258 .map_err(|e| format!("failed to evaluate capability scope: {e}"))?;
1259 if !matches {
1260 return Err(format!(
1261 "capability does not authorize tool {} on server {}",
1262 requested_tool.tool_name, requested_tool.server_id
1263 ));
1264 }
1265 }
1266 Ok(token)
1267}
1268
1269fn decision_metadata(
1270 kernel_receipt_id: Option<&str>,
1271 route_selection: Option<&Value>,
1272) -> Option<Value> {
1273 let mut metadata = http_status_metadata_decision();
1274 insert_metadata_string(&mut metadata, CHIO_KERNEL_RECEIPT_ID_KEY, kernel_receipt_id);
1275 insert_metadata_value(&mut metadata, "route_selection", route_selection);
1276 Some(metadata)
1277}
1278
1279fn final_metadata(
1280 decision_receipt_id: Option<&str>,
1281 kernel_receipt_id: Option<&str>,
1282 route_selection: Option<&Value>,
1283) -> Option<Value> {
1284 let mut metadata = http_status_metadata_final(decision_receipt_id);
1285 insert_metadata_string(&mut metadata, CHIO_KERNEL_RECEIPT_ID_KEY, kernel_receipt_id);
1286 insert_metadata_value(&mut metadata, "route_selection", route_selection);
1287 Some(metadata)
1288}
1289
1290fn insert_metadata_string(metadata: &mut Value, key: &str, value: Option<&str>) {
1291 let Some(value) = value else {
1292 return;
1293 };
1294 if let Value::Object(map) = metadata {
1295 map.insert(key.to_string(), Value::String(value.to_string()));
1296 } else {
1297 let mut map = Map::new();
1298 map.insert(key.to_string(), Value::String(value.to_string()));
1299 *metadata = Value::Object(map);
1300 }
1301}
1302
1303fn insert_metadata_value(metadata: &mut Value, key: &str, value: Option<&Value>) {
1304 let Some(value) = value else {
1305 return;
1306 };
1307 if let Value::Object(map) = metadata {
1308 map.insert(key.to_string(), value.clone());
1309 } else {
1310 let mut map = Map::new();
1311 map.insert(key.to_string(), value.clone());
1312 *metadata = Value::Object(map);
1313 }
1314}
1315
1316fn metadata_string<'a>(metadata: Option<&'a Value>, key: &str) -> Option<&'a str> {
1317 metadata
1318 .and_then(Value::as_object)
1319 .and_then(|map| map.get(key))
1320 .and_then(Value::as_str)
1321}
1322
1323fn metadata_value<'a>(metadata: Option<&'a Value>, key: &str) -> Option<&'a Value> {
1324 metadata
1325 .and_then(Value::as_object)
1326 .and_then(|map| map.get(key))
1327}
1328
1329fn pending_approval_id(metadata: Option<&Value>, reason: Option<&str>) -> Option<String> {
1330 metadata_string(metadata, "approval_id")
1331 .or_else(|| {
1332 metadata_value(metadata, "pending_approval")
1333 .and_then(Value::as_object)
1334 .and_then(|pending| pending.get("approval_id"))
1335 .and_then(Value::as_str)
1336 })
1337 .map(ToOwned::to_owned)
1338 .or_else(|| extract_approval_id(reason))
1339}
1340
1341fn extract_approval_id(reason: Option<&str>) -> Option<String> {
1342 let reason = reason?;
1343 for marker in ["/approvals/", "approval_id=", "approval_id:"] {
1344 if let Some(start) = reason.find(marker) {
1345 let suffix = reason[start + marker.len()..].trim_start();
1346 let approval_id = suffix
1347 .split(|character: char| {
1348 character == '/'
1349 || character == ','
1350 || character == ';'
1351 || character.is_whitespace()
1352 })
1353 .next()?;
1354 if !approval_id.is_empty() {
1355 return Some(approval_id.to_string());
1356 }
1357 }
1358 }
1359 None
1360}
1361
1362#[cfg(test)]
1363mod tests;