Skip to main content

chio_http_core/
authority.rs

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
32/// Tool server id for HTTP-sidecar capability grants.
33pub const HTTP_AUTHORITY_SERVER_ID: &str = "chio_http_authority";
34/// Tool name for HTTP-sidecar capability grants.
35pub const HTTP_AUTHORITY_TOOL_NAME: &str = "authorize_http_request";
36const HTTP_AUTHORITY_TTL_SECS: u64 = 60;
37
38/// Guard label the embedded kernel stamps on a deny receipt when it fails a
39/// mediated call closed for missing durable persistence (no receipt store or no
40/// durable revocation state). Kept in step with the kernel's fail-closed deny
41/// builder so the authority can surface a durability failure as an error rather
42/// than fold it into a routine deny receipt.
43const KERNEL_DURABILITY_FAILCLOSED_GUARD: &str = "kernel.receipt_persistence";
44
45/// Whether a kernel response is a fail-closed denial for missing durable
46/// persistence, as opposed to a routine policy or capability denial. The kernel
47/// runs its durability gate ahead of the HTTP projection guard, so this can fire
48/// for a request that is independently projected as denied.
49fn 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
118/// Input for [`HttpAuthority::sign_transport_deny_receipt`]: a deny verdict
119/// emitted by the transport layer (for example, a body-size guard) before
120/// the kernel evaluation pipeline has run.
121pub 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
175/// Whether an evaluation error is a genuine mediation-edge dispatch failure that
176/// must feed `chio_dispatch_failure_total` and the error-outcome latency/guard
177/// evaluation series. A [`HttpAuthorityError::PendingApproval`] is NOT a dispatch
178/// failure: it is the normal HITL approval-required flow (surfaced as a 409),
179/// so it is excluded from the paging metric and the error series. Every other
180/// error is a real evaluation failure.
181fn 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
259/// Builder for an [`HttpAuthority`] whose embedded kernel is backed by durable
260/// stores. Unlike the `-> Self` constructors, `build` attaches the receipt and
261/// revocation stores before the kernel is Arc-wrapped, and it is fallible
262/// because attaching a receipt store hydrates checkpoint counters and can fail.
263/// Both ephemeral opt-ins default to `false` (fail-closed).
264struct 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    /// Start building an authority whose embedded kernel is backed by durable
387    /// receipt and revocation stores. Stores must be attached before the kernel
388    /// is wrapped in an [`Arc`], which is what this builder does.
389    #[must_use]
390    pub fn builder() -> HttpAuthorityBuilder {
391        HttpAuthorityBuilder::default()
392    }
393
394    /// Fail-closed constructor: no receipt or revocation store is attached and
395    /// both ephemeral opt-ins are off. Existing callers compile unchanged, but
396    /// the first mediated call now denies with a durable-persistence error
397    /// until a durable store is wired through [`HttpAuthority::builder`]. This
398    /// matches the kernel-backed lanes, which already deny when durable
399    /// persistence is missing.
400    #[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    /// Explicitly ephemeral constructor (in-memory receipt log and revocation
413    /// store) for local scaffolds and tests that intend ephemerality.
414    #[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    /// Fail-closed constructor with a caller-provided approval store. Like
427    /// [`HttpAuthority::new`], no receipt or revocation store is attached and both
428    /// ephemeral opt-ins stay off, so a mediated side-effect call denies with a
429    /// durable-persistence error until durable stores are wired through
430    /// [`HttpAuthority::builder`]. Reach for
431    /// [`HttpAuthority::new_ephemeral_with_approval_store_and_trusted_issuers`]
432    /// when in-memory receipts and revocations are intended.
433    #[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    /// Fail-closed constructor with a caller-provided approval store and trusted
450    /// issuers. Side-effect calls deny until durable stores are attached; see
451    /// [`HttpAuthority::new_with_approval_store`].
452    #[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    /// Explicitly ephemeral constructor with a caller-provided approval store and
470    /// trusted issuers: the embedded kernel keeps its receipt log and revocation
471    /// state in memory, and both are lost on restart. Ephemerality is opted into
472    /// through the constructor name (unlike the fail-closed
473    /// [`HttpAuthority::new_with_approval_store_and_trusted_issuers`]) for local
474    /// scaffolds and tests that intend it.
475    #[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    /// Infallible assembly shared by every `-> Self` constructor: build the
493    /// kernel config, register the tool server and projection guard, and
494    /// Arc-wrap. No receipt store is attached here (attaching one is fallible
495    /// and lives in the builder), so these constructors keep their signatures.
496    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    /// The embedded kernel configuration for the HTTP mediation lane. The two
534    /// ephemeral flags are the only durability knobs a caller varies; every
535    /// other field is fixed for this lane.
536    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                // A policy/capability deny is an expected fail-closed decision,
600                // NOT a dispatch failure: it is tracked by the guard-verdict
601                // metrics only, never on chio_dispatch_failure_total, so a normal
602                // rejected request cannot page the P0 fail-open/dispatch-failure
603                // alert. Only a genuine evaluation error (the Err arm below) feeds
604                // the paging metric.
605                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                // A HITL PendingApproval is the normal approval-required control
621                // flow (the caller turns it into a 409 approval response), NOT a
622                // mediation-edge dispatch failure. Recording it as an error would
623                // feed chio_dispatch_failure_total (paging the P0 fail-open alert
624                // on every governed approval prompt) and skew the error-outcome
625                // latency/guard-eval series, which fold every unknown outcome into
626                // the error bucket. Only a genuine evaluation error feeds these
627                // instruments.
628                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        // A fail-closed durability denial (missing durable receipt store or
722        // revocation state) must surface as an error for every projection, not
723        // only allowed ones. The kernel runs its durability gate before the HTTP
724        // projection guard, so a request independently projected as denied would
725        // otherwise return a signed deny receipt while the misconfigured
726        // authority silently drops the denial audit record. Denial evidence must
727        // be as durable as allow evidence, so propagate it regardless of verdict.
728        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    /// Sign a deny receipt for a request that was rejected before the kernel
840    /// ever evaluated it (for example, an oversized HTTP body that the
841    /// transport layer refused to buffer). The caller supplies the verdict and
842    /// the surface-level identification fields directly because no kernel
843    /// receipt id, content hash, or capability id exists yet. The receipt
844    /// carries `final` HTTP status scope metadata so downstream auditors can
845    /// distinguish it from in-band kernel decisions.
846    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            // The kernel's revocation check runs against the internal authority
1116            // capability minted per request, never against the caller's token,
1117            // so a presented capability that has been revoked (directly or via a
1118            // revoked delegation ancestor) must be rejected here before it is
1119            // projected as authorized.
1120            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
1138/// Reject a presented capability whose id, or any id in its delegation chain,
1139/// is revoked. Returns `Ok(None)` when nothing is revoked, `Ok(Some(reason))`
1140/// when a revoked id is found, and `Err(reason)` when the revocation store
1141/// cannot be consulted, so an unavailable revocation store denies (fail-closed)
1142/// rather than silently projecting the capability as valid.
1143fn 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;