Skip to main content

chio_kernel/
operator_report.rs

1use serde::{Deserialize, Serialize};
2
3use chio_core::appraisal::AttestationVerifierFamily;
4use chio_core::capability::{
5    GovernedCallChainProvenance, MeteredSettlementMode, MonetaryAmount, RuntimeAssuranceTier,
6};
7use chio_core::receipt::{
8    ChioReceipt, Decision, EconomicAuthorizationReceiptMetadata,
9    FinancialBudgetAuthorityReceiptMetadata, GovernedTransactionReceiptMetadata,
10    MeteredUsageEvidenceReceiptMetadata, SettlementStatus, SignedExportEnvelope,
11};
12use chio_core::session::ChioIdentityAssertion;
13use chio_core::{
14    ChioGovernedAuthorizationBinding, ChioPortableClaimCatalog, ChioPortableIdentityBinding,
15};
16use chio_credit::{
17    CreditBondDisposition, CreditBondListReport, CreditFacilityDisposition,
18    CreditFacilityListReport, ExposureLedgerQuery,
19};
20use chio_underwriting::{UnderwritingDecisionListReport, UnderwritingDecisionOutcome};
21
22use crate::cost_attribution::CostAttributionQuery;
23use crate::evidence_export::{
24    EvidenceChildReceiptScope, EvidenceExportQuery, EvidenceLineageReferences,
25};
26use crate::receipt_analytics::{AnalyticsTimeBucket, ReceiptAnalyticsResponse};
27use crate::receipt_query::ReceiptQuery;
28use crate::receipt_store::FederatedEvidenceShareSummary;
29use crate::CostAttributionReport;
30
31/// Maximum number of budget rows returned in a single operator report.
32pub const MAX_OPERATOR_BUDGET_LIMIT: usize = 200;
33/// Maximum number of shared-evidence reference rows returned in one query.
34pub const MAX_SHARED_EVIDENCE_LIMIT: usize = 200;
35/// Maximum number of settlement backlog rows returned in one report.
36pub const MAX_SETTLEMENT_BACKLOG_LIMIT: usize = 200;
37/// Maximum number of receipt detail rows returned in one behavioral feed.
38pub const MAX_BEHAVIORAL_FEED_RECEIPT_LIMIT: usize = 200;
39/// Maximum number of metered-billing reconciliation rows returned in one report.
40pub const MAX_METERED_BILLING_LIMIT: usize = 200;
41/// Maximum number of authorization-context rows returned in one report.
42pub const MAX_AUTHORIZATION_CONTEXT_LIMIT: usize = 200;
43/// Maximum number of economic projection rows returned in one report.
44pub const MAX_ECONOMIC_RECEIPT_LIMIT: usize = 200;
45/// Stable schema identifier for Chio's normative OAuth-family authorization profile.
46pub const CHIO_OAUTH_AUTHORIZATION_PROFILE_SCHEMA: &str = "chio.oauth.authorization-profile.v1";
47/// Stable schema identifier for Chio's sender-constraint profile.
48pub const CHIO_OAUTH_SENDER_CONSTRAINT_SCHEMA: &str = "chio.oauth.sender-constraint.v1";
49/// Stable schema identifier for Chio authorization-context reports.
50pub const CHIO_OAUTH_AUTHORIZATION_CONTEXT_REPORT_SCHEMA: &str =
51    "chio.oauth.authorization-context-report.v1";
52/// Stable schema identifier for the deterministic economic completion flow bundle.
53pub const ECONOMIC_COMPLETION_FLOW_SCHEMA: &str = "chio.economic-completion-flow.v1";
54/// Stable schema identifier for Chio authorization-profile metadata artifacts.
55pub const CHIO_OAUTH_AUTHORIZATION_METADATA_SCHEMA: &str = "chio.oauth.authorization-metadata.v1";
56/// Stable schema identifier for Chio enterprise IAM reviewer packs.
57pub const CHIO_OAUTH_AUTHORIZATION_REVIEW_PACK_SCHEMA: &str =
58    "chio.oauth.authorization-review-pack.v1";
59/// Stable identifier for Chio's first governed authorization-details profile.
60pub const CHIO_OAUTH_AUTHORIZATION_PROFILE_ID: &str = "chio-governed-rar-v1";
61/// Detail type for the primary governed tool action.
62pub const CHIO_OAUTH_AUTHORIZATION_TOOL_DETAIL_TYPE: &str = "chio_governed_tool";
63/// Detail type for governed commerce scope.
64pub const CHIO_OAUTH_AUTHORIZATION_COMMERCE_DETAIL_TYPE: &str = "chio_governed_commerce";
65/// Detail type for governed metered-billing scope.
66pub const CHIO_OAUTH_AUTHORIZATION_METERED_BILLING_DETAIL_TYPE: &str =
67    "chio_governed_metered_billing";
68/// Stable label for Chio's capability-subject sender binding.
69pub const CHIO_OAUTH_SENDER_BINDING_CAPABILITY_SUBJECT: &str = "capability_subject";
70/// Stable label for Chio's Chio-native DPoP proof requirement.
71pub const CHIO_OAUTH_SENDER_PROOF_CHIO_DPOP: &str = "chio_dpop_v1";
72/// Stable label for Chio's bounded mTLS-thumbprint sender adapter.
73pub const CHIO_OAUTH_SENDER_PROOF_CHIO_MTLS: &str = "chio_mtls_thumbprint_v1";
74/// Stable label for Chio's bounded attestation-bound sender adapter.
75pub const CHIO_OAUTH_SENDER_PROOF_CHIO_ATTESTATION: &str = "chio_attestation_binding_v1";
76/// Stable request-time parameter for Chio governed authorization details.
77pub const CHIO_OAUTH_REQUEST_TIME_AUTHORIZATION_DETAILS_PARAMETER: &str = "authorization_details";
78/// Stable request-time parameter for Chio governed transaction context.
79pub const CHIO_OAUTH_REQUEST_TIME_TRANSACTION_CONTEXT_PARAMETER: &str = "chio_transaction_context";
80/// Stable access-token claim for Chio governed authorization details.
81pub const CHIO_OAUTH_REQUEST_TIME_AUTHORIZATION_DETAILS_CLAIM: &str = "authorization_details";
82/// Stable access-token claim for Chio governed transaction context.
83pub const CHIO_OAUTH_REQUEST_TIME_TRANSACTION_CONTEXT_CLAIM: &str = "chio_transaction_context";
84
85/// Filter surface for the operator-facing reporting API.
86#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
87#[serde(rename_all = "camelCase")]
88pub struct OperatorReportQuery {
89    #[serde(default, skip_serializing_if = "Option::is_none")]
90    pub capability_id: Option<String>,
91    #[serde(default, skip_serializing_if = "Option::is_none")]
92    pub agent_subject: Option<String>,
93    #[serde(default, skip_serializing_if = "Option::is_none")]
94    pub tool_server: Option<String>,
95    #[serde(default, skip_serializing_if = "Option::is_none")]
96    pub tool_name: Option<String>,
97    #[serde(default, skip_serializing_if = "Option::is_none")]
98    pub since: Option<u64>,
99    #[serde(default, skip_serializing_if = "Option::is_none")]
100    pub until: Option<u64>,
101    #[serde(default, skip_serializing_if = "Option::is_none")]
102    pub group_limit: Option<usize>,
103    #[serde(default, skip_serializing_if = "Option::is_none")]
104    pub time_bucket: Option<AnalyticsTimeBucket>,
105    #[serde(default, skip_serializing_if = "Option::is_none")]
106    pub attribution_limit: Option<usize>,
107    #[serde(default, skip_serializing_if = "Option::is_none")]
108    pub budget_limit: Option<usize>,
109    #[serde(default, skip_serializing_if = "Option::is_none")]
110    pub settlement_limit: Option<usize>,
111    #[serde(default, skip_serializing_if = "Option::is_none")]
112    pub metered_limit: Option<usize>,
113    #[serde(default, skip_serializing_if = "Option::is_none")]
114    pub authorization_limit: Option<usize>,
115    #[serde(default, skip_serializing_if = "Option::is_none")]
116    pub economic_limit: Option<usize>,
117}
118
119impl Default for OperatorReportQuery {
120    fn default() -> Self {
121        Self {
122            capability_id: None,
123            agent_subject: None,
124            tool_server: None,
125            tool_name: None,
126            since: None,
127            until: None,
128            group_limit: Some(50),
129            time_bucket: Some(AnalyticsTimeBucket::Day),
130            attribution_limit: Some(100),
131            budget_limit: Some(50),
132            settlement_limit: Some(50),
133            metered_limit: Some(50),
134            authorization_limit: Some(50),
135            economic_limit: Some(50),
136        }
137    }
138}
139
140impl OperatorReportQuery {
141    #[must_use]
142    pub fn to_receipt_analytics_query(&self) -> crate::ReceiptAnalyticsQuery {
143        crate::ReceiptAnalyticsQuery {
144            capability_id: self.capability_id.clone(),
145            agent_subject: self.agent_subject.clone(),
146            tool_server: self.tool_server.clone(),
147            tool_name: self.tool_name.clone(),
148            since: self.since,
149            until: self.until,
150            group_limit: self.group_limit,
151            time_bucket: self.time_bucket,
152        }
153    }
154
155    #[must_use]
156    pub fn to_cost_attribution_query(&self) -> CostAttributionQuery {
157        CostAttributionQuery {
158            capability_id: self.capability_id.clone(),
159            agent_subject: self.agent_subject.clone(),
160            tool_server: self.tool_server.clone(),
161            tool_name: self.tool_name.clone(),
162            since: self.since,
163            until: self.until,
164            limit: self.attribution_limit,
165        }
166    }
167
168    #[must_use]
169    pub fn to_evidence_export_query(&self) -> EvidenceExportQuery {
170        EvidenceExportQuery {
171            capability_id: self.capability_id.clone(),
172            agent_subject: self.agent_subject.clone(),
173            since: self.since,
174            until: self.until,
175            // Phase 1.5: operator_report does not scope by tenant today.
176            // When multi-tenant surfaces are introduced the caller
177            // layer must populate this from the authenticated context.
178            tenant: None,
179        }
180    }
181
182    #[must_use]
183    pub fn direct_evidence_export_supported(&self) -> bool {
184        self.tool_server.is_none() && self.tool_name.is_none()
185    }
186
187    #[must_use]
188    pub fn budget_limit_or_default(&self) -> usize {
189        self.budget_limit
190            .unwrap_or(50)
191            .clamp(1, MAX_OPERATOR_BUDGET_LIMIT)
192    }
193
194    #[must_use]
195    pub fn settlement_limit_or_default(&self) -> usize {
196        self.settlement_limit
197            .unwrap_or(50)
198            .clamp(1, MAX_SETTLEMENT_BACKLOG_LIMIT)
199    }
200
201    #[must_use]
202    pub fn metered_limit_or_default(&self) -> usize {
203        self.metered_limit
204            .unwrap_or(50)
205            .clamp(1, MAX_METERED_BILLING_LIMIT)
206    }
207
208    #[must_use]
209    pub fn authorization_limit_or_default(&self) -> usize {
210        self.authorization_limit
211            .unwrap_or(50)
212            .clamp(1, MAX_AUTHORIZATION_CONTEXT_LIMIT)
213    }
214
215    #[must_use]
216    pub fn economic_limit_or_default(&self) -> usize {
217        self.economic_limit
218            .unwrap_or(50)
219            .clamp(1, MAX_ECONOMIC_RECEIPT_LIMIT)
220    }
221
222    #[must_use]
223    pub fn to_shared_evidence_query(&self) -> SharedEvidenceQuery {
224        SharedEvidenceQuery {
225            capability_id: self.capability_id.clone(),
226            agent_subject: self.agent_subject.clone(),
227            tool_server: self.tool_server.clone(),
228            tool_name: self.tool_name.clone(),
229            since: self.since,
230            until: self.until,
231            issuer: None,
232            partner: None,
233            limit: self.group_limit,
234        }
235    }
236}
237
238/// Filter surface for the signed insurer/risk behavioral feed export.
239#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
240#[serde(rename_all = "camelCase")]
241pub struct BehavioralFeedQuery {
242    #[serde(default, skip_serializing_if = "Option::is_none")]
243    pub capability_id: Option<String>,
244    #[serde(default, skip_serializing_if = "Option::is_none")]
245    pub agent_subject: Option<String>,
246    #[serde(default, skip_serializing_if = "Option::is_none")]
247    pub tool_server: Option<String>,
248    #[serde(default, skip_serializing_if = "Option::is_none")]
249    pub tool_name: Option<String>,
250    #[serde(default, skip_serializing_if = "Option::is_none")]
251    pub since: Option<u64>,
252    #[serde(default, skip_serializing_if = "Option::is_none")]
253    pub until: Option<u64>,
254    #[serde(default, skip_serializing_if = "Option::is_none")]
255    pub receipt_limit: Option<usize>,
256}
257
258impl Default for BehavioralFeedQuery {
259    fn default() -> Self {
260        Self {
261            capability_id: None,
262            agent_subject: None,
263            tool_server: None,
264            tool_name: None,
265            since: None,
266            until: None,
267            receipt_limit: Some(100),
268        }
269    }
270}
271
272impl BehavioralFeedQuery {
273    #[must_use]
274    pub fn receipt_limit_or_default(&self) -> usize {
275        self.receipt_limit
276            .unwrap_or(100)
277            .clamp(1, MAX_BEHAVIORAL_FEED_RECEIPT_LIMIT)
278    }
279
280    #[must_use]
281    pub fn normalized(&self) -> Self {
282        let mut normalized = self.clone();
283        normalized.receipt_limit = Some(self.receipt_limit_or_default());
284        normalized
285    }
286
287    #[must_use]
288    pub fn to_operator_report_query(&self) -> OperatorReportQuery {
289        OperatorReportQuery {
290            capability_id: self.capability_id.clone(),
291            agent_subject: self.agent_subject.clone(),
292            tool_server: self.tool_server.clone(),
293            tool_name: self.tool_name.clone(),
294            since: self.since,
295            until: self.until,
296            ..OperatorReportQuery::default()
297        }
298    }
299
300    #[must_use]
301    pub fn to_receipt_query(&self) -> ReceiptQuery {
302        ReceiptQuery {
303            capability_id: self.capability_id.clone(),
304            tool_server: self.tool_server.clone(),
305            tool_name: self.tool_name.clone(),
306            outcome: None,
307            since: self.since,
308            until: self.until,
309            min_cost: None,
310            max_cost: None,
311            cursor: None,
312            limit: self.receipt_limit_or_default(),
313            agent_subject: self.agent_subject.clone(),
314            // Phase 1.5: operator_report does not scope by tenant today.
315            // When multi-tenant surfaces are introduced the caller layer
316            // must populate this from the authenticated context.
317            tenant_filter: None,
318        }
319    }
320}
321
322#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
323#[serde(rename_all = "camelCase")]
324pub struct SharedEvidenceQuery {
325    #[serde(default, skip_serializing_if = "Option::is_none")]
326    pub capability_id: Option<String>,
327    #[serde(default, skip_serializing_if = "Option::is_none")]
328    pub agent_subject: Option<String>,
329    #[serde(default, skip_serializing_if = "Option::is_none")]
330    pub tool_server: Option<String>,
331    #[serde(default, skip_serializing_if = "Option::is_none")]
332    pub tool_name: Option<String>,
333    #[serde(default, skip_serializing_if = "Option::is_none")]
334    pub since: Option<u64>,
335    #[serde(default, skip_serializing_if = "Option::is_none")]
336    pub until: Option<u64>,
337    #[serde(default, skip_serializing_if = "Option::is_none")]
338    pub issuer: Option<String>,
339    #[serde(default, skip_serializing_if = "Option::is_none")]
340    pub partner: Option<String>,
341    #[serde(default, skip_serializing_if = "Option::is_none")]
342    pub limit: Option<usize>,
343}
344
345impl Default for SharedEvidenceQuery {
346    fn default() -> Self {
347        Self {
348            capability_id: None,
349            agent_subject: None,
350            tool_server: None,
351            tool_name: None,
352            since: None,
353            until: None,
354            issuer: None,
355            partner: None,
356            limit: Some(50),
357        }
358    }
359}
360
361impl SharedEvidenceQuery {
362    #[must_use]
363    pub fn limit_or_default(&self) -> usize {
364        self.limit.unwrap_or(50).clamp(1, MAX_SHARED_EVIDENCE_LIMIT)
365    }
366}
367
368#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
369#[serde(rename_all = "camelCase")]
370pub struct BudgetUtilizationSummary {
371    pub matching_grants: u64,
372    pub returned_grants: u64,
373    pub distinct_capabilities: u64,
374    pub distinct_subjects: u64,
375    pub total_invocations: u64,
376    pub total_cost_charged: u64,
377    pub near_limit_count: u64,
378    pub exhausted_count: u64,
379    pub rows_missing_scope: u64,
380    pub rows_missing_lineage: u64,
381    pub truncated: bool,
382}
383
384#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
385#[serde(rename_all = "camelCase")]
386pub struct BudgetDimensionUsage {
387    pub used: u64,
388    pub limit: u64,
389    pub remaining: u64,
390    #[serde(default, skip_serializing_if = "Option::is_none")]
391    pub utilization_rate: Option<f64>,
392    pub near_limit: bool,
393    pub exhausted: bool,
394}
395
396#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
397#[serde(rename_all = "camelCase")]
398pub struct BudgetDimensionProfile {
399    #[serde(default, skip_serializing_if = "Option::is_none")]
400    pub invocations: Option<BudgetDimensionUsage>,
401    #[serde(default, skip_serializing_if = "Option::is_none")]
402    pub money: Option<BudgetDimensionUsage>,
403}
404
405#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
406#[serde(rename_all = "camelCase")]
407pub struct BudgetUtilizationRow {
408    pub capability_id: String,
409    pub grant_index: u32,
410    #[serde(default, skip_serializing_if = "Option::is_none")]
411    pub subject_key: Option<String>,
412    #[serde(default, skip_serializing_if = "Option::is_none")]
413    pub tool_server: Option<String>,
414    #[serde(default, skip_serializing_if = "Option::is_none")]
415    pub tool_name: Option<String>,
416    pub invocation_count: u32,
417    #[serde(default, skip_serializing_if = "Option::is_none")]
418    pub max_invocations: Option<u32>,
419    pub total_cost_charged: u64,
420    #[serde(default, skip_serializing_if = "Option::is_none")]
421    pub currency: Option<String>,
422    #[serde(default, skip_serializing_if = "Option::is_none")]
423    pub max_total_cost_units: Option<u64>,
424    #[serde(default, skip_serializing_if = "Option::is_none")]
425    pub remaining_cost_units: Option<u64>,
426    #[serde(default, skip_serializing_if = "Option::is_none")]
427    pub invocation_utilization_rate: Option<f64>,
428    #[serde(default, skip_serializing_if = "Option::is_none")]
429    pub cost_utilization_rate: Option<f64>,
430    pub near_limit: bool,
431    pub exhausted: bool,
432    pub updated_at: i64,
433    pub scope_resolved: bool,
434    #[serde(default, skip_serializing_if = "Option::is_none")]
435    pub scope_resolution_error: Option<String>,
436    #[serde(default, skip_serializing_if = "Option::is_none")]
437    pub dimensions: Option<BudgetDimensionProfile>,
438}
439
440#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
441#[serde(rename_all = "camelCase")]
442pub struct BudgetUtilizationReport {
443    pub summary: BudgetUtilizationSummary,
444    pub rows: Vec<BudgetUtilizationRow>,
445}
446
447#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
448#[serde(rename_all = "camelCase")]
449pub struct ComplianceReport {
450    pub matching_receipts: u64,
451    pub evidence_ready_receipts: u64,
452    pub uncheckpointed_receipts: u64,
453    #[serde(default, skip_serializing_if = "Option::is_none")]
454    pub checkpoint_coverage_rate: Option<f64>,
455    pub lineage_covered_receipts: u64,
456    pub lineage_gap_receipts: u64,
457    #[serde(default, skip_serializing_if = "Option::is_none")]
458    pub lineage_coverage_rate: Option<f64>,
459    pub pending_settlement_receipts: u64,
460    pub failed_settlement_receipts: u64,
461    pub direct_evidence_export_supported: bool,
462    pub child_receipt_scope: EvidenceChildReceiptScope,
463    pub proofs_complete: bool,
464    pub export_query: EvidenceExportQuery,
465    #[serde(default, skip_serializing_if = "Option::is_none")]
466    pub export_scope_note: Option<String>,
467}
468
469#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
470#[serde(rename_all = "snake_case")]
471pub enum SettlementReconciliationState {
472    Open,
473    Reconciled,
474    Ignored,
475    RetryScheduled,
476}
477
478#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
479#[serde(rename_all = "camelCase")]
480pub struct SettlementReconciliationSummary {
481    pub matching_receipts: u64,
482    pub returned_receipts: u64,
483    pub pending_receipts: u64,
484    pub failed_receipts: u64,
485    pub actionable_receipts: u64,
486    pub reconciled_receipts: u64,
487    pub truncated: bool,
488}
489
490#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
491#[serde(rename_all = "camelCase")]
492pub struct SettlementReconciliationRow {
493    pub receipt_id: String,
494    pub timestamp: u64,
495    pub capability_id: String,
496    #[serde(default, skip_serializing_if = "Option::is_none")]
497    pub subject_key: Option<String>,
498    pub tool_server: String,
499    pub tool_name: String,
500    #[serde(default, skip_serializing_if = "Option::is_none")]
501    pub payment_reference: Option<String>,
502    pub settlement_status: SettlementStatus,
503    #[serde(default, skip_serializing_if = "Option::is_none")]
504    pub cost_charged: Option<u64>,
505    #[serde(default, skip_serializing_if = "Option::is_none")]
506    pub currency: Option<String>,
507    #[serde(default, skip_serializing_if = "Option::is_none")]
508    pub budget_authority: Option<FinancialBudgetAuthorityReceiptMetadata>,
509    pub reconciliation_state: SettlementReconciliationState,
510    pub action_required: bool,
511    #[serde(default, skip_serializing_if = "Option::is_none")]
512    pub note: Option<String>,
513    #[serde(default, skip_serializing_if = "Option::is_none")]
514    pub updated_at: Option<u64>,
515}
516
517#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
518#[serde(rename_all = "camelCase")]
519pub struct SettlementReconciliationReport {
520    pub summary: SettlementReconciliationSummary,
521    pub receipts: Vec<SettlementReconciliationRow>,
522}
523
524#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
525#[serde(rename_all = "snake_case")]
526pub enum MeteredBillingReconciliationState {
527    Open,
528    Reconciled,
529    Ignored,
530    RetryScheduled,
531}
532
533#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
534#[serde(rename_all = "camelCase")]
535pub struct MeteredBillingEvidenceRecord {
536    pub usage_evidence: MeteredUsageEvidenceReceiptMetadata,
537    pub billed_cost: MonetaryAmount,
538    pub recorded_at: u64,
539}
540
541#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
542#[serde(rename_all = "camelCase")]
543pub struct MeteredBillingReconciliationSummary {
544    pub matching_receipts: u64,
545    pub returned_receipts: u64,
546    pub evidence_attached_receipts: u64,
547    pub missing_evidence_receipts: u64,
548    pub over_quoted_units_receipts: u64,
549    pub over_max_billed_units_receipts: u64,
550    pub over_quoted_cost_receipts: u64,
551    pub financial_mismatch_receipts: u64,
552    pub actionable_receipts: u64,
553    pub reconciled_receipts: u64,
554    pub truncated: bool,
555}
556
557#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
558#[serde(rename_all = "camelCase")]
559pub struct MeteredBillingReconciliationRow {
560    pub receipt_id: String,
561    pub timestamp: u64,
562    pub capability_id: String,
563    #[serde(default, skip_serializing_if = "Option::is_none")]
564    pub subject_key: Option<String>,
565    pub tool_server: String,
566    pub tool_name: String,
567    pub settlement_mode: MeteredSettlementMode,
568    pub provider: String,
569    pub quote_id: String,
570    pub billing_unit: String,
571    pub quoted_units: u64,
572    pub quoted_cost: MonetaryAmount,
573    #[serde(default, skip_serializing_if = "Option::is_none")]
574    pub max_billed_units: Option<u64>,
575    #[serde(default, skip_serializing_if = "Option::is_none")]
576    pub financial_cost_charged: Option<u64>,
577    #[serde(default, skip_serializing_if = "Option::is_none")]
578    pub financial_currency: Option<String>,
579    #[serde(default, skip_serializing_if = "Option::is_none")]
580    pub budget_authority: Option<FinancialBudgetAuthorityReceiptMetadata>,
581    #[serde(default, skip_serializing_if = "Option::is_none")]
582    pub evidence: Option<MeteredBillingEvidenceRecord>,
583    pub reconciliation_state: MeteredBillingReconciliationState,
584    pub action_required: bool,
585    pub evidence_missing: bool,
586    pub exceeds_quoted_units: bool,
587    pub exceeds_max_billed_units: bool,
588    pub exceeds_quoted_cost: bool,
589    pub financial_mismatch: bool,
590    #[serde(default, skip_serializing_if = "Option::is_none")]
591    pub note: Option<String>,
592    #[serde(default, skip_serializing_if = "Option::is_none")]
593    pub updated_at: Option<u64>,
594}
595
596#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
597#[serde(rename_all = "camelCase")]
598pub struct MeteredBillingReconciliationReport {
599    pub summary: MeteredBillingReconciliationSummary,
600    pub receipts: Vec<MeteredBillingReconciliationRow>,
601}
602
603#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
604#[serde(rename_all = "camelCase")]
605pub struct EconomicReceiptProjectionSummary {
606    pub matching_receipts: u64,
607    pub returned_receipts: u64,
608    pub metered_receipts: u64,
609    pub pending_settlement_receipts: u64,
610    pub failed_settlement_receipts: u64,
611    pub settlement_actionable_receipts: u64,
612    pub metering_actionable_receipts: u64,
613    pub metering_evidence_missing_receipts: u64,
614    pub metering_financial_mismatch_receipts: u64,
615    pub truncated: bool,
616}
617
618#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
619#[serde(rename_all = "camelCase")]
620pub struct EconomicReceiptSettlementProjection {
621    pub settlement_status: SettlementStatus,
622    pub reconciliation_state: SettlementReconciliationState,
623    pub action_required: bool,
624    #[serde(default, skip_serializing_if = "Option::is_none")]
625    pub note: Option<String>,
626    #[serde(default, skip_serializing_if = "Option::is_none")]
627    pub updated_at: Option<u64>,
628}
629
630#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
631#[serde(rename_all = "camelCase")]
632pub struct EconomicReceiptMeteringProjection {
633    pub reconciliation_state: MeteredBillingReconciliationState,
634    pub action_required: bool,
635    pub evidence_missing: bool,
636    pub exceeds_quoted_units: bool,
637    pub exceeds_max_billed_units: bool,
638    pub exceeds_quoted_cost: bool,
639    pub financial_mismatch: bool,
640    #[serde(default, skip_serializing_if = "Option::is_none")]
641    pub evidence: Option<MeteredBillingEvidenceRecord>,
642    #[serde(default, skip_serializing_if = "Option::is_none")]
643    pub note: Option<String>,
644    #[serde(default, skip_serializing_if = "Option::is_none")]
645    pub updated_at: Option<u64>,
646}
647
648#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
649#[serde(rename_all = "camelCase")]
650pub struct EconomicReceiptProjectionRow {
651    pub receipt_id: String,
652    pub timestamp: u64,
653    pub capability_id: String,
654    #[serde(default, skip_serializing_if = "Option::is_none")]
655    pub subject_key: Option<String>,
656    pub tool_server: String,
657    pub tool_name: String,
658    pub economic_authorization: EconomicAuthorizationReceiptMetadata,
659    #[serde(default, skip_serializing_if = "Option::is_none")]
660    pub budget_authority: Option<FinancialBudgetAuthorityReceiptMetadata>,
661    pub settlement: EconomicReceiptSettlementProjection,
662    #[serde(default, skip_serializing_if = "Option::is_none")]
663    pub metering: Option<EconomicReceiptMeteringProjection>,
664}
665
666#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
667#[serde(rename_all = "camelCase")]
668pub struct EconomicReceiptProjectionReport {
669    pub summary: EconomicReceiptProjectionSummary,
670    pub receipts: Vec<EconomicReceiptProjectionRow>,
671}
672
673#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
674#[serde(rename_all = "camelCase")]
675pub struct EconomicCompletionFlowSummary {
676    pub matching_receipts: u64,
677    pub returned_receipts: u64,
678    pub matching_underwriting_decisions: u64,
679    pub returned_underwriting_decisions: u64,
680    pub matching_credit_facilities: u64,
681    pub returned_credit_facilities: u64,
682    pub matching_credit_bonds: u64,
683    pub returned_credit_bonds: u64,
684    pub pending_settlement_receipts: u64,
685    pub failed_settlement_receipts: u64,
686    pub metering_actionable_receipts: u64,
687    #[serde(default, skip_serializing_if = "Option::is_none")]
688    pub latest_underwriting_decision_id: Option<String>,
689    #[serde(default, skip_serializing_if = "Option::is_none")]
690    pub latest_underwriting_outcome: Option<UnderwritingDecisionOutcome>,
691    #[serde(default, skip_serializing_if = "Option::is_none")]
692    pub latest_credit_facility_id: Option<String>,
693    #[serde(default, skip_serializing_if = "Option::is_none")]
694    pub latest_credit_facility_disposition: Option<CreditFacilityDisposition>,
695    #[serde(default, skip_serializing_if = "Option::is_none")]
696    pub latest_credit_bond_id: Option<String>,
697    #[serde(default, skip_serializing_if = "Option::is_none")]
698    pub latest_credit_bond_disposition: Option<CreditBondDisposition>,
699}
700
701#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
702#[serde(rename_all = "camelCase")]
703pub struct EconomicCompletionFlowReport {
704    pub schema: String,
705    pub generated_at: u64,
706    pub filters: ExposureLedgerQuery,
707    pub summary: EconomicCompletionFlowSummary,
708    pub economic_receipts: EconomicReceiptProjectionReport,
709    pub underwriting_decisions: UnderwritingDecisionListReport,
710    pub credit_facilities: CreditFacilityListReport,
711    pub credit_bonds: CreditBondListReport,
712}
713
714#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
715#[serde(rename_all = "camelCase")]
716pub struct GovernedAuthorizationCommerceDetail {
717    pub seller: String,
718    pub shared_payment_token_id: String,
719}
720
721#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
722#[serde(rename_all = "camelCase")]
723pub struct GovernedAuthorizationMeteredBillingDetail {
724    pub settlement_mode: MeteredSettlementMode,
725    pub provider: String,
726    pub quote_id: String,
727    pub billing_unit: String,
728    pub quoted_units: u64,
729    pub quoted_cost: MonetaryAmount,
730    #[serde(default, skip_serializing_if = "Option::is_none")]
731    pub max_billed_units: Option<u64>,
732}
733
734#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
735#[serde(rename_all = "camelCase")]
736pub struct GovernedAuthorizationDetail {
737    #[serde(rename = "type")]
738    pub detail_type: String,
739    #[serde(default, skip_serializing_if = "Vec::is_empty")]
740    pub locations: Vec<String>,
741    #[serde(default, skip_serializing_if = "Vec::is_empty")]
742    pub actions: Vec<String>,
743    #[serde(default, skip_serializing_if = "Option::is_none")]
744    pub purpose: Option<String>,
745    #[serde(default, skip_serializing_if = "Option::is_none")]
746    pub max_amount: Option<MonetaryAmount>,
747    #[serde(default, skip_serializing_if = "Option::is_none")]
748    pub commerce: Option<GovernedAuthorizationCommerceDetail>,
749    #[serde(default, skip_serializing_if = "Option::is_none")]
750    pub metered_billing: Option<GovernedAuthorizationMeteredBillingDetail>,
751}
752
753#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
754#[serde(rename_all = "camelCase")]
755pub struct GovernedAuthorizationTransactionContext {
756    pub intent_id: String,
757    pub intent_hash: String,
758    #[serde(default, skip_serializing_if = "Option::is_none")]
759    pub approval_token_id: Option<String>,
760    #[serde(default, skip_serializing_if = "Option::is_none")]
761    pub approval_approved: Option<bool>,
762    #[serde(default, skip_serializing_if = "Option::is_none")]
763    pub approver_key: Option<String>,
764    #[serde(default, skip_serializing_if = "Option::is_none")]
765    pub runtime_assurance_tier: Option<RuntimeAssuranceTier>,
766    #[serde(default, skip_serializing_if = "Option::is_none")]
767    pub runtime_assurance_schema: Option<String>,
768    #[serde(default, skip_serializing_if = "Option::is_none")]
769    pub runtime_assurance_verifier_family: Option<AttestationVerifierFamily>,
770    #[serde(default, skip_serializing_if = "Option::is_none")]
771    pub runtime_assurance_verifier: Option<String>,
772    #[serde(default, skip_serializing_if = "Option::is_none")]
773    pub runtime_assurance_evidence_sha256: Option<String>,
774    #[serde(default, skip_serializing_if = "Option::is_none")]
775    pub call_chain: Option<GovernedCallChainProvenance>,
776    #[serde(default, skip_serializing_if = "Option::is_none")]
777    pub identity_assertion: Option<ChioIdentityAssertion>,
778}
779
780#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
781#[serde(rename_all = "camelCase")]
782pub struct GovernedTransactionDiagnostics {
783    #[serde(default, skip_serializing_if = "Option::is_none")]
784    pub asserted_call_chain: Option<GovernedCallChainProvenance>,
785    #[serde(default, skip_serializing_if = "EvidenceLineageReferences::is_empty")]
786    pub lineage_references: EvidenceLineageReferences,
787}
788
789impl GovernedTransactionDiagnostics {
790    #[must_use]
791    pub fn is_empty(&self) -> bool {
792        self.asserted_call_chain.is_none() && self.lineage_references.is_empty()
793    }
794}
795
796fn default_authorization_context_report_schema() -> String {
797    CHIO_OAUTH_AUTHORIZATION_CONTEXT_REPORT_SCHEMA.to_string()
798}
799
800fn default_chio_oauth_authorization_profile() -> ChioOAuthAuthorizationProfile {
801    ChioOAuthAuthorizationProfile::default()
802}
803
804#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
805#[serde(rename_all = "camelCase")]
806pub struct ChioOAuthAuthorizationProfile {
807    pub schema: String,
808    pub id: String,
809    pub authoritative_source: String,
810    pub authorization_detail_types: Vec<String>,
811    pub transaction_context_fields: Vec<String>,
812    #[serde(default)]
813    pub portable_claim_catalog: ChioPortableClaimCatalog,
814    #[serde(default)]
815    pub portable_identity_binding: ChioPortableIdentityBinding,
816    #[serde(default)]
817    pub governed_auth_binding: ChioGovernedAuthorizationBinding,
818    #[serde(default)]
819    pub request_time_contract: ChioOAuthRequestTimeContract,
820    #[serde(default)]
821    pub resource_binding: ChioOAuthResourceBinding,
822    #[serde(default)]
823    pub artifact_boundary: ChioOAuthArtifactBoundary,
824    pub sender_constraints: ChioOAuthSenderConstraintProfile,
825    pub unsupported_shapes_fail_closed: bool,
826}
827
828#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
829#[serde(rename_all = "camelCase")]
830pub struct ChioOAuthRequestTimeContract {
831    pub authorization_details_parameter: String,
832    pub transaction_context_parameter: String,
833    pub access_token_authorization_details_claim: String,
834    pub access_token_transaction_context_claim: String,
835    pub request_time_authorization_details_supported: bool,
836    pub request_time_transaction_context_supported: bool,
837    pub governed_receipts_authoritative_post_execution: bool,
838}
839
840impl Default for ChioOAuthRequestTimeContract {
841    fn default() -> Self {
842        Self {
843            authorization_details_parameter:
844                CHIO_OAUTH_REQUEST_TIME_AUTHORIZATION_DETAILS_PARAMETER.to_string(),
845            transaction_context_parameter: CHIO_OAUTH_REQUEST_TIME_TRANSACTION_CONTEXT_PARAMETER
846                .to_string(),
847            access_token_authorization_details_claim:
848                CHIO_OAUTH_REQUEST_TIME_AUTHORIZATION_DETAILS_CLAIM.to_string(),
849            access_token_transaction_context_claim:
850                CHIO_OAUTH_REQUEST_TIME_TRANSACTION_CONTEXT_CLAIM.to_string(),
851            request_time_authorization_details_supported: true,
852            request_time_transaction_context_supported: true,
853            governed_receipts_authoritative_post_execution: true,
854        }
855    }
856}
857
858#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
859#[serde(rename_all = "camelCase")]
860pub struct ChioOAuthResourceBinding {
861    pub protected_resource_field: String,
862    pub request_resource_parameter: String,
863    pub access_token_audience_claim: String,
864    pub access_token_resource_claim: String,
865    pub request_resource_must_match_protected_resource: bool,
866    pub bearer_token_must_include_audience_or_resource: bool,
867}
868
869impl Default for ChioOAuthResourceBinding {
870    fn default() -> Self {
871        Self {
872            protected_resource_field: "resource".to_string(),
873            request_resource_parameter: "resource".to_string(),
874            access_token_audience_claim: "aud".to_string(),
875            access_token_resource_claim: "resource".to_string(),
876            request_resource_must_match_protected_resource: true,
877            bearer_token_must_include_audience_or_resource: true,
878        }
879    }
880}
881
882#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
883#[serde(rename_all = "camelCase")]
884pub struct ChioOAuthArtifactBoundary {
885    pub access_tokens_runtime_admission_supported: bool,
886    pub approval_tokens_runtime_admission_supported: bool,
887    pub capabilities_runtime_admission_supported: bool,
888    pub reviewer_evidence_runtime_admission_supported: bool,
889    pub governed_receipts_audit_evidence_supported: bool,
890}
891
892impl Default for ChioOAuthArtifactBoundary {
893    fn default() -> Self {
894        Self {
895            access_tokens_runtime_admission_supported: true,
896            approval_tokens_runtime_admission_supported: false,
897            capabilities_runtime_admission_supported: false,
898            reviewer_evidence_runtime_admission_supported: false,
899            governed_receipts_audit_evidence_supported: true,
900        }
901    }
902}
903
904#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
905#[serde(rename_all = "camelCase")]
906pub struct ChioOAuthSenderConstraintProfile {
907    pub schema: String,
908    pub subject_binding: String,
909    pub proof_types_supported: Vec<String>,
910    pub proof_required_when: String,
911    pub runtime_assurance_binding_fields: Vec<String>,
912    pub delegated_call_chain_field: String,
913    pub unsupported_sender_shapes_fail_closed: bool,
914}
915
916impl Default for ChioOAuthSenderConstraintProfile {
917    fn default() -> Self {
918        Self {
919            schema: CHIO_OAUTH_SENDER_CONSTRAINT_SCHEMA.to_string(),
920            subject_binding: CHIO_OAUTH_SENDER_BINDING_CAPABILITY_SUBJECT.to_string(),
921            proof_types_supported: vec![
922                CHIO_OAUTH_SENDER_PROOF_CHIO_DPOP.to_string(),
923                CHIO_OAUTH_SENDER_PROOF_CHIO_MTLS.to_string(),
924                CHIO_OAUTH_SENDER_PROOF_CHIO_ATTESTATION.to_string(),
925            ],
926            proof_required_when: "matchedGrant.dpopRequired == true".to_string(),
927            runtime_assurance_binding_fields: vec![
928                "runtimeAssuranceTier".to_string(),
929                "runtimeAssuranceSchema".to_string(),
930                "runtimeAssuranceVerifierFamily".to_string(),
931                "runtimeAssuranceVerifier".to_string(),
932                "runtimeAssuranceEvidenceSha256".to_string(),
933            ],
934            delegated_call_chain_field: "callChain".to_string(),
935            unsupported_sender_shapes_fail_closed: true,
936        }
937    }
938}
939
940impl Default for ChioOAuthAuthorizationProfile {
941    fn default() -> Self {
942        Self {
943            schema: CHIO_OAUTH_AUTHORIZATION_PROFILE_SCHEMA.to_string(),
944            id: CHIO_OAUTH_AUTHORIZATION_PROFILE_ID.to_string(),
945            authoritative_source: "governed_receipt_projection".to_string(),
946            authorization_detail_types: vec![
947                CHIO_OAUTH_AUTHORIZATION_TOOL_DETAIL_TYPE.to_string(),
948                CHIO_OAUTH_AUTHORIZATION_COMMERCE_DETAIL_TYPE.to_string(),
949                CHIO_OAUTH_AUTHORIZATION_METERED_BILLING_DETAIL_TYPE.to_string(),
950            ],
951            transaction_context_fields: vec![
952                "intentId".to_string(),
953                "intentHash".to_string(),
954                "approvalTokenId".to_string(),
955                "approvalApproved".to_string(),
956                "approverKey".to_string(),
957                "runtimeAssuranceTier".to_string(),
958                "runtimeAssuranceSchema".to_string(),
959                "runtimeAssuranceVerifierFamily".to_string(),
960                "runtimeAssuranceVerifier".to_string(),
961                "runtimeAssuranceEvidenceSha256".to_string(),
962                "callChain".to_string(),
963                "identityAssertion".to_string(),
964            ],
965            portable_claim_catalog: ChioPortableClaimCatalog::default(),
966            portable_identity_binding: ChioPortableIdentityBinding::default(),
967            governed_auth_binding: ChioGovernedAuthorizationBinding::default(),
968            request_time_contract: ChioOAuthRequestTimeContract::default(),
969            resource_binding: ChioOAuthResourceBinding::default(),
970            artifact_boundary: ChioOAuthArtifactBoundary::default(),
971            sender_constraints: ChioOAuthSenderConstraintProfile::default(),
972            unsupported_shapes_fail_closed: true,
973        }
974    }
975}
976
977#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
978#[serde(rename_all = "camelCase")]
979pub struct AuthorizationContextSummary {
980    pub matching_receipts: u64,
981    pub returned_receipts: u64,
982    pub approval_receipts: u64,
983    pub approved_receipts: u64,
984    pub commerce_receipts: u64,
985    pub metered_billing_receipts: u64,
986    pub runtime_assurance_receipts: u64,
987    pub call_chain_receipts: u64,
988    pub asserted_call_chain_receipts: u64,
989    pub observed_call_chain_receipts: u64,
990    pub verified_call_chain_receipts: u64,
991    pub max_amount_receipts: u64,
992    pub sender_bound_receipts: u64,
993    pub dpop_bound_receipts: u64,
994    pub runtime_assurance_bound_receipts: u64,
995    pub delegated_sender_bound_receipts: u64,
996    pub session_anchor_receipts: u64,
997    pub request_lineage_receipts: u64,
998    pub receipt_lineage_statement_receipts: u64,
999    pub truncated: bool,
1000}
1001
1002#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1003#[serde(rename_all = "camelCase")]
1004pub struct AuthorizationContextSenderConstraint {
1005    pub subject_key: String,
1006    pub subject_key_source: String,
1007    #[serde(default, skip_serializing_if = "String::is_empty")]
1008    pub issuer_key: String,
1009    #[serde(default, skip_serializing_if = "String::is_empty")]
1010    pub issuer_key_source: String,
1011    pub matched_grant_index: u32,
1012    pub proof_required: bool,
1013    #[serde(default, skip_serializing_if = "Option::is_none")]
1014    pub proof_type: Option<String>,
1015    #[serde(default, skip_serializing_if = "Option::is_none")]
1016    pub proof_schema: Option<String>,
1017    pub runtime_assurance_bound: bool,
1018    pub delegated_call_chain_bound: bool,
1019}
1020
1021#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1022#[serde(rename_all = "camelCase")]
1023pub struct AuthorizationContextRow {
1024    pub receipt_id: String,
1025    pub timestamp: u64,
1026    pub capability_id: String,
1027    #[serde(default, skip_serializing_if = "Option::is_none")]
1028    pub subject_key: Option<String>,
1029    pub tool_server: String,
1030    pub tool_name: String,
1031    pub decision: Decision,
1032    pub authorization_details: Vec<GovernedAuthorizationDetail>,
1033    pub transaction_context: GovernedAuthorizationTransactionContext,
1034    #[serde(default, skip_serializing_if = "Option::is_none")]
1035    pub governed_transaction_diagnostics: Option<GovernedTransactionDiagnostics>,
1036    pub sender_constraint: AuthorizationContextSenderConstraint,
1037}
1038
1039#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1040#[serde(rename_all = "camelCase")]
1041pub struct AuthorizationContextReport {
1042    #[serde(default = "default_authorization_context_report_schema")]
1043    pub schema: String,
1044    #[serde(default = "default_chio_oauth_authorization_profile")]
1045    pub profile: ChioOAuthAuthorizationProfile,
1046    pub summary: AuthorizationContextSummary,
1047    pub receipts: Vec<AuthorizationContextRow>,
1048}
1049
1050#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1051#[serde(rename_all = "camelCase")]
1052pub struct ChioOAuthAuthorizationDiscoveryMetadata {
1053    pub protected_resource_metadata_paths: Vec<String>,
1054    pub authorization_server_metadata_path_template: String,
1055    pub discovery_informational_only: bool,
1056}
1057
1058#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1059#[serde(rename_all = "camelCase")]
1060pub struct ChioOAuthAuthorizationSupportBoundary {
1061    pub governed_receipts_authoritative: bool,
1062    pub hosted_request_time_authorization_supported: bool,
1063    pub resource_indicator_binding_supported: bool,
1064    pub sender_constrained_projection: bool,
1065    pub runtime_assurance_projection: bool,
1066    pub delegated_call_chain_projection: bool,
1067    pub generic_token_issuance_supported: bool,
1068    pub oidc_identity_assertions_supported: bool,
1069    pub mtls_transport_binding_in_profile: bool,
1070    pub approval_tokens_runtime_authorization_supported: bool,
1071    pub capabilities_runtime_authorization_supported: bool,
1072    pub reviewer_evidence_runtime_authorization_supported: bool,
1073}
1074
1075#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1076#[serde(rename_all = "camelCase")]
1077pub struct ChioOAuthAuthorizationExampleMapping {
1078    pub authorization_detail_types: Vec<String>,
1079    pub transaction_context_fields: Vec<String>,
1080    pub sender_constraint_fields: Vec<String>,
1081}
1082
1083#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1084#[serde(rename_all = "camelCase")]
1085pub struct ChioOAuthAuthorizationMetadataReport {
1086    pub schema: String,
1087    pub generated_at: u64,
1088    pub profile: ChioOAuthAuthorizationProfile,
1089    pub report_schema: String,
1090    pub discovery: ChioOAuthAuthorizationDiscoveryMetadata,
1091    pub support_boundary: ChioOAuthAuthorizationSupportBoundary,
1092    pub example_mapping: ChioOAuthAuthorizationExampleMapping,
1093}
1094
1095#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1096#[serde(rename_all = "camelCase")]
1097pub struct ChioOAuthAuthorizationReviewPackSummary {
1098    pub matching_receipts: u64,
1099    pub returned_receipts: u64,
1100    pub dpop_required_receipts: u64,
1101    pub runtime_assurance_receipts: u64,
1102    pub delegated_call_chain_receipts: u64,
1103    pub asserted_call_chain_receipts: u64,
1104    pub observed_call_chain_receipts: u64,
1105    pub verified_call_chain_receipts: u64,
1106    pub session_anchor_receipts: u64,
1107    pub request_lineage_receipts: u64,
1108    pub receipt_lineage_statement_receipts: u64,
1109    pub truncated: bool,
1110}
1111
1112#[derive(Debug, Clone, Serialize, Deserialize)]
1113#[serde(rename_all = "camelCase")]
1114pub struct ChioOAuthAuthorizationReviewPackRecord {
1115    pub receipt_id: String,
1116    pub capability_id: String,
1117    pub authorization_context: AuthorizationContextRow,
1118    pub governed_transaction: GovernedTransactionReceiptMetadata,
1119    #[serde(default, skip_serializing_if = "Option::is_none")]
1120    pub governed_transaction_diagnostics: Option<GovernedTransactionDiagnostics>,
1121    pub signed_receipt: ChioReceipt,
1122}
1123
1124#[derive(Debug, Clone, Serialize, Deserialize)]
1125#[serde(rename_all = "camelCase")]
1126pub struct ChioOAuthAuthorizationReviewPack {
1127    pub schema: String,
1128    pub generated_at: u64,
1129    pub filters: OperatorReportQuery,
1130    pub metadata: ChioOAuthAuthorizationMetadataReport,
1131    pub summary: ChioOAuthAuthorizationReviewPackSummary,
1132    pub records: Vec<ChioOAuthAuthorizationReviewPackRecord>,
1133}
1134
1135#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1136#[serde(rename_all = "camelCase")]
1137pub struct SharedEvidenceReferenceSummary {
1138    pub matching_shares: u64,
1139    pub matching_references: u64,
1140    pub matching_local_receipts: u64,
1141    pub remote_tool_receipts: u64,
1142    pub remote_lineage_records: u64,
1143    pub distinct_remote_subjects: u64,
1144    pub proof_required_shares: u64,
1145    pub truncated: bool,
1146}
1147
1148#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1149#[serde(rename_all = "camelCase")]
1150pub struct SharedEvidenceReferenceRow {
1151    pub share: FederatedEvidenceShareSummary,
1152    pub capability_id: String,
1153    pub subject_key: String,
1154    pub issuer_key: String,
1155    pub delegation_depth: u64,
1156    #[serde(default, skip_serializing_if = "Option::is_none")]
1157    pub parent_capability_id: Option<String>,
1158    #[serde(default, skip_serializing_if = "Option::is_none")]
1159    pub local_anchor_capability_id: Option<String>,
1160    pub matched_local_receipts: u64,
1161    pub allow_count: u64,
1162    pub deny_count: u64,
1163    pub cancelled_count: u64,
1164    pub incomplete_count: u64,
1165    #[serde(default, skip_serializing_if = "Option::is_none")]
1166    pub first_seen: Option<u64>,
1167    #[serde(default, skip_serializing_if = "Option::is_none")]
1168    pub last_seen: Option<u64>,
1169}
1170
1171#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1172#[serde(rename_all = "camelCase")]
1173pub struct SharedEvidenceReferenceReport {
1174    pub summary: SharedEvidenceReferenceSummary,
1175    pub references: Vec<SharedEvidenceReferenceRow>,
1176}
1177
1178/// Stable schema identifier for insurer-facing behavioral feed exports.
1179pub const BEHAVIORAL_FEED_SCHEMA: &str = "chio.behavioral-feed.v1";
1180
1181#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1182#[serde(rename_all = "camelCase")]
1183pub struct BehavioralFeedPrivacyBoundary {
1184    pub matching_receipts: u64,
1185    pub returned_receipts: u64,
1186    pub direct_evidence_export_supported: bool,
1187    pub child_receipt_scope: EvidenceChildReceiptScope,
1188    pub proofs_complete: bool,
1189    pub export_query: EvidenceExportQuery,
1190    #[serde(default, skip_serializing_if = "Option::is_none")]
1191    pub export_scope_note: Option<String>,
1192}
1193
1194#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1195#[serde(rename_all = "camelCase")]
1196pub struct BehavioralFeedDecisionSummary {
1197    pub allow_count: u64,
1198    pub deny_count: u64,
1199    pub cancelled_count: u64,
1200    pub incomplete_count: u64,
1201}
1202
1203#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1204#[serde(rename_all = "camelCase")]
1205pub struct BehavioralFeedSettlementSummary {
1206    pub pending_receipts: u64,
1207    pub settled_receipts: u64,
1208    pub failed_receipts: u64,
1209    pub not_applicable_receipts: u64,
1210    pub actionable_receipts: u64,
1211    pub reconciled_receipts: u64,
1212}
1213
1214#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1215#[serde(rename_all = "camelCase")]
1216pub struct BehavioralFeedGovernedActionSummary {
1217    pub governed_receipts: u64,
1218    pub approval_receipts: u64,
1219    pub approved_receipts: u64,
1220    pub commerce_receipts: u64,
1221    pub max_amount_receipts: u64,
1222}
1223
1224#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1225#[serde(rename_all = "camelCase")]
1226pub struct BehavioralFeedMeteredBillingSummary {
1227    pub metered_receipts: u64,
1228    pub evidence_attached_receipts: u64,
1229    pub missing_evidence_receipts: u64,
1230    pub over_quoted_units_receipts: u64,
1231    pub over_max_billed_units_receipts: u64,
1232    pub over_quoted_cost_receipts: u64,
1233    pub financial_mismatch_receipts: u64,
1234    pub actionable_receipts: u64,
1235    pub reconciled_receipts: u64,
1236}
1237
1238#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1239#[serde(rename_all = "camelCase")]
1240pub struct BehavioralFeedReputationSummary {
1241    pub subject_key: String,
1242    pub effective_score: f64,
1243    pub probationary: bool,
1244    #[serde(default, skip_serializing_if = "Option::is_none")]
1245    pub resolved_tier: Option<String>,
1246    pub imported_signal_count: usize,
1247    pub accepted_imported_signal_count: usize,
1248}
1249
1250#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1251#[serde(rename_all = "camelCase")]
1252pub struct BehavioralFeedReceiptRow {
1253    pub receipt_id: String,
1254    pub timestamp: u64,
1255    pub capability_id: String,
1256    #[serde(default, skip_serializing_if = "Option::is_none")]
1257    pub subject_key: Option<String>,
1258    #[serde(default, skip_serializing_if = "Option::is_none")]
1259    pub issuer_key: Option<String>,
1260    pub tool_server: String,
1261    pub tool_name: String,
1262    pub decision: Decision,
1263    pub settlement_status: SettlementStatus,
1264    pub reconciliation_state: SettlementReconciliationState,
1265    pub action_required: bool,
1266    #[serde(default, skip_serializing_if = "Option::is_none")]
1267    pub cost_charged: Option<u64>,
1268    #[serde(default, skip_serializing_if = "Option::is_none")]
1269    pub attempted_cost: Option<u64>,
1270    #[serde(default, skip_serializing_if = "Option::is_none")]
1271    pub currency: Option<String>,
1272    #[serde(default, skip_serializing_if = "Option::is_none")]
1273    pub budget_authority: Option<FinancialBudgetAuthorityReceiptMetadata>,
1274    #[serde(default, skip_serializing_if = "Option::is_none")]
1275    pub governed: Option<GovernedTransactionReceiptMetadata>,
1276    #[serde(default, skip_serializing_if = "Option::is_none")]
1277    pub governed_transaction_diagnostics: Option<GovernedTransactionDiagnostics>,
1278    #[serde(default, skip_serializing_if = "Option::is_none")]
1279    pub metered_reconciliation: Option<BehavioralFeedMeteredBillingRow>,
1280}
1281
1282#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1283#[serde(rename_all = "camelCase")]
1284pub struct BehavioralFeedMeteredBillingRow {
1285    pub reconciliation_state: MeteredBillingReconciliationState,
1286    pub action_required: bool,
1287    pub evidence_missing: bool,
1288    pub exceeds_quoted_units: bool,
1289    pub exceeds_max_billed_units: bool,
1290    pub exceeds_quoted_cost: bool,
1291    pub financial_mismatch: bool,
1292    #[serde(default, skip_serializing_if = "Option::is_none")]
1293    pub evidence: Option<MeteredBillingEvidenceRecord>,
1294}
1295
1296#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1297#[serde(rename_all = "camelCase")]
1298pub struct BehavioralFeedReceiptSelection {
1299    pub matching_receipts: u64,
1300    pub receipts: Vec<BehavioralFeedReceiptRow>,
1301}
1302
1303#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1304#[serde(rename_all = "camelCase")]
1305pub struct BehavioralFeedReport {
1306    pub schema: String,
1307    pub generated_at: u64,
1308    pub filters: BehavioralFeedQuery,
1309    pub privacy: BehavioralFeedPrivacyBoundary,
1310    pub decisions: BehavioralFeedDecisionSummary,
1311    pub settlements: BehavioralFeedSettlementSummary,
1312    pub governed_actions: BehavioralFeedGovernedActionSummary,
1313    pub metered_billing: BehavioralFeedMeteredBillingSummary,
1314    #[serde(default, skip_serializing_if = "Option::is_none")]
1315    pub reputation: Option<BehavioralFeedReputationSummary>,
1316    pub shared_evidence: SharedEvidenceReferenceSummary,
1317    pub receipts: Vec<BehavioralFeedReceiptRow>,
1318}
1319
1320pub type SignedBehavioralFeed = SignedExportEnvelope<BehavioralFeedReport>;
1321
1322// ===========================================================================
1323// Phase 19.1 + 19.2 additive surfaces on top of ComplianceReport and
1324// BehavioralFeedReport. These helpers do not mutate the existing structs;
1325// they compute derived signals for the scoring / advisory pipeline.
1326// ===========================================================================
1327
1328impl ComplianceReport {
1329    /// Compute a weighted compliance score on top of this report.
1330    ///
1331    /// Thin convenience wrapper over
1332    /// [`crate::compliance_score::compliance_score`]. See that module for
1333    /// the scoring model and weights.
1334    #[must_use]
1335    pub fn compliance_score(
1336        &self,
1337        inputs: &crate::compliance_score::ComplianceScoreInputs,
1338        config: &crate::compliance_score::ComplianceScoreConfig,
1339        agent_id: &str,
1340        now: u64,
1341    ) -> crate::compliance_score::ComplianceScore {
1342        crate::compliance_score::compliance_score(self, inputs, config, agent_id, now)
1343    }
1344}
1345
1346/// EMA (exponentially-weighted moving average) baseline state for a
1347/// single (agent, metric) pair. Used by behavioral profiling to detect
1348/// z-score anomalies without storing every historical sample.
1349///
1350/// The baseline uses Welford-style incremental tracking of mean and
1351/// variance so callers can compute a z-score for any new sample
1352/// without re-reading history.
1353#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
1354#[serde(rename_all = "camelCase")]
1355pub struct EmaBaselineState {
1356    /// Number of samples folded into the baseline.
1357    pub sample_count: u64,
1358    /// Exponentially-weighted mean.
1359    pub ema_mean: f64,
1360    /// Exponentially-weighted variance.
1361    pub ema_variance: f64,
1362    /// Last update timestamp (unix seconds).
1363    pub last_update: u64,
1364}
1365
1366impl EmaBaselineState {
1367    /// Fold a new sample into the baseline with the provided smoothing
1368    /// factor `alpha` (0.0..=1.0). Higher alpha weighs recent samples
1369    /// more heavily.
1370    ///
1371    /// `alpha` is clamped to `(0.0, 1.0]`. `now` is recorded as
1372    /// `last_update`.
1373    pub fn update(&mut self, sample: f64, alpha: f64, now: u64) {
1374        let alpha = alpha.clamp(f64::MIN_POSITIVE, 1.0);
1375        if self.sample_count == 0 {
1376            self.ema_mean = sample;
1377            self.ema_variance = 0.0;
1378        } else {
1379            let prev_mean = self.ema_mean;
1380            self.ema_mean = prev_mean + alpha * (sample - prev_mean);
1381            // Incremental EWMA variance, following West (1979) / Welford.
1382            let diff = sample - prev_mean;
1383            self.ema_variance = (1.0 - alpha) * (self.ema_variance + alpha * diff * diff);
1384        }
1385        self.sample_count = self.sample_count.saturating_add(1);
1386        self.last_update = now;
1387    }
1388
1389    /// Standard deviation (sqrt of EWMA variance).
1390    #[must_use]
1391    pub fn stddev(&self) -> f64 {
1392        self.ema_variance.max(0.0).sqrt()
1393    }
1394
1395    /// Z-score for a new sample. Returns `None` when the baseline has
1396    /// fewer than two samples or zero variance (no meaningful signal).
1397    #[must_use]
1398    pub fn z_score(&self, sample: f64) -> Option<f64> {
1399        if self.sample_count < 2 {
1400            return None;
1401        }
1402        let stddev = self.stddev();
1403        if stddev <= f64::EPSILON {
1404            return None;
1405        }
1406        Some((sample - self.ema_mean) / stddev)
1407    }
1408}
1409
1410/// Summary of behavioral-anomaly signals derived from receipts over a
1411/// window. Used by `BehavioralProfileGuard` and surfaced in operator
1412/// UIs.
1413#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
1414#[serde(rename_all = "camelCase")]
1415pub struct BehavioralAnomalyScore {
1416    /// Agent subject this anomaly score applies to.
1417    pub agent_id: String,
1418    /// Baseline statistic the z-score is computed against.
1419    pub baseline: EmaBaselineState,
1420    /// Current-window sample value (e.g. call count per window).
1421    pub current_sample: f64,
1422    /// Computed z-score, or `None` when baseline is too small.
1423    pub z_score: Option<f64>,
1424    /// Threshold above which an advisory signal is raised.
1425    pub sigma_threshold: f64,
1426    /// Whether the current sample crossed the threshold.
1427    pub anomaly: bool,
1428    /// Unix timestamp (seconds) at which the score was computed.
1429    pub generated_at: u64,
1430}
1431
1432/// Compute a behavioral-anomaly score from a pre-existing baseline plus
1433/// a current-window sample. Exposes the same math the guard uses so
1434/// callers can surface anomaly scores in dashboards without rerunning
1435/// the guard.
1436#[must_use]
1437pub fn behavioral_anomaly_score(
1438    agent_id: &str,
1439    baseline: &EmaBaselineState,
1440    current_sample: f64,
1441    sigma_threshold: f64,
1442    now: u64,
1443) -> BehavioralAnomalyScore {
1444    let z_score = baseline.z_score(current_sample);
1445    let anomaly = z_score.is_some_and(|z| z.abs() > sigma_threshold);
1446    BehavioralAnomalyScore {
1447        agent_id: agent_id.to_string(),
1448        baseline: baseline.clone(),
1449        current_sample,
1450        z_score,
1451        sigma_threshold,
1452        anomaly,
1453        generated_at: now,
1454    }
1455}
1456
1457#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1458#[serde(rename_all = "camelCase")]
1459pub struct OperatorReport {
1460    pub generated_at: u64,
1461    pub filters: OperatorReportQuery,
1462    pub activity: ReceiptAnalyticsResponse,
1463    pub cost_attribution: CostAttributionReport,
1464    pub budget_utilization: BudgetUtilizationReport,
1465    pub compliance: ComplianceReport,
1466    pub settlement_reconciliation: SettlementReconciliationReport,
1467    pub metered_billing_reconciliation: MeteredBillingReconciliationReport,
1468    pub authorization_context: AuthorizationContextReport,
1469    pub shared_evidence: SharedEvidenceReferenceReport,
1470}
1471
1472#[cfg(test)]
1473#[allow(clippy::unwrap_used)]
1474mod tests {
1475    use super::*;
1476    use chio_core::capability::{GovernedCallChainContext, GovernedProvenanceEvidenceClass};
1477    use chio_core::receipt::{
1478        FinancialBudgetAuthorityReceiptMetadata, FinancialBudgetAuthorizeReceiptMetadata,
1479        FinancialBudgetHoldAuthorityMetadata, FinancialBudgetTerminalReceiptMetadata,
1480    };
1481
1482    #[test]
1483    fn operator_report_behavioral_feed_query_clamps_limit_and_translates_filters() {
1484        let query = BehavioralFeedQuery {
1485            capability_id: Some("cap-1".to_string()),
1486            agent_subject: Some("subject-1".to_string()),
1487            tool_server: Some("shell".to_string()),
1488            tool_name: Some("bash".to_string()),
1489            since: Some(10),
1490            until: Some(20),
1491            receipt_limit: Some(5_000),
1492        };
1493
1494        assert_eq!(
1495            query.receipt_limit_or_default(),
1496            MAX_BEHAVIORAL_FEED_RECEIPT_LIMIT
1497        );
1498        assert_eq!(
1499            query.normalized().receipt_limit,
1500            Some(MAX_BEHAVIORAL_FEED_RECEIPT_LIMIT)
1501        );
1502
1503        let operator_query = query.to_operator_report_query();
1504        assert_eq!(operator_query.capability_id.as_deref(), Some("cap-1"));
1505        assert_eq!(operator_query.agent_subject.as_deref(), Some("subject-1"));
1506        assert_eq!(operator_query.tool_server.as_deref(), Some("shell"));
1507        assert_eq!(operator_query.tool_name.as_deref(), Some("bash"));
1508        assert_eq!(operator_query.since, Some(10));
1509        assert_eq!(operator_query.until, Some(20));
1510        assert_eq!(operator_query.metered_limit_or_default(), 50);
1511
1512        let receipt_query = query.to_receipt_query();
1513        assert_eq!(receipt_query.limit, MAX_BEHAVIORAL_FEED_RECEIPT_LIMIT);
1514        assert_eq!(receipt_query.agent_subject.as_deref(), Some("subject-1"));
1515    }
1516
1517    #[test]
1518    fn operator_report_query_direct_export_support_requires_no_tool_filters() {
1519        let unrestricted = OperatorReportQuery::default();
1520        assert!(unrestricted.direct_evidence_export_supported());
1521
1522        let with_tool_filter = OperatorReportQuery {
1523            tool_server: Some("shell".to_string()),
1524            ..OperatorReportQuery::default()
1525        };
1526        assert!(!with_tool_filter.direct_evidence_export_supported());
1527    }
1528
1529    #[test]
1530    fn operator_report_query_clamps_metered_limit() {
1531        let query = OperatorReportQuery {
1532            metered_limit: Some(5_000),
1533            ..OperatorReportQuery::default()
1534        };
1535
1536        assert_eq!(query.metered_limit_or_default(), MAX_METERED_BILLING_LIMIT);
1537    }
1538
1539    #[test]
1540    fn operator_report_query_clamps_authorization_limit() {
1541        let query = OperatorReportQuery {
1542            authorization_limit: Some(5_000),
1543            ..OperatorReportQuery::default()
1544        };
1545
1546        assert_eq!(
1547            query.authorization_limit_or_default(),
1548            MAX_AUTHORIZATION_CONTEXT_LIMIT
1549        );
1550    }
1551
1552    #[test]
1553    fn operator_report_query_clamps_economic_limit() {
1554        let query = OperatorReportQuery {
1555            economic_limit: Some(5_000),
1556            ..OperatorReportQuery::default()
1557        };
1558
1559        assert_eq!(
1560            query.economic_limit_or_default(),
1561            MAX_ECONOMIC_RECEIPT_LIMIT
1562        );
1563    }
1564
1565    #[test]
1566    fn governed_transaction_diagnostics_serialization_omits_empty_fields() {
1567        let diagnostics = GovernedTransactionDiagnostics::default();
1568
1569        assert!(diagnostics.is_empty());
1570        assert_eq!(
1571            serde_json::to_value(diagnostics).unwrap(),
1572            serde_json::json!({})
1573        );
1574    }
1575
1576    #[test]
1577    fn governed_transaction_diagnostics_preserves_asserted_call_chain_and_lineage_references() {
1578        let diagnostics = GovernedTransactionDiagnostics {
1579            asserted_call_chain: Some(GovernedCallChainProvenance::asserted(
1580                GovernedCallChainContext {
1581                    chain_id: "chain-1".to_string(),
1582                    parent_request_id: "req-parent-1".to_string(),
1583                    parent_receipt_id: Some("rcpt-parent-1".to_string()),
1584                    origin_subject: "origin".to_string(),
1585                    delegator_subject: "delegator".to_string(),
1586                },
1587            )),
1588            lineage_references: EvidenceLineageReferences {
1589                session_anchor_id: Some("anchor-1".to_string()),
1590                request_lineage_id: Some("req-lineage-1".to_string()),
1591                receipt_lineage_statement_id: Some("stmt-1".to_string()),
1592            },
1593        };
1594
1595        let value = serde_json::to_value(&diagnostics).unwrap();
1596
1597        assert_eq!(
1598            value["assertedCallChain"]["evidenceClass"],
1599            serde_json::json!("asserted")
1600        );
1601        assert_eq!(value["lineageReferences"]["sessionAnchorId"], "anchor-1");
1602        assert_eq!(
1603            diagnostics
1604                .asserted_call_chain
1605                .as_ref()
1606                .map(|call_chain| call_chain.evidence_class),
1607            Some(GovernedProvenanceEvidenceClass::Asserted)
1608        );
1609    }
1610
1611    #[test]
1612    fn settlement_reconciliation_row_serialization_preserves_budget_authority_metadata() {
1613        let budget_authority = FinancialBudgetAuthorityReceiptMetadata {
1614            guarantee_level: "ha_quorum_commit".to_string(),
1615            authority_profile: "ha".to_string(),
1616            metering_profile: "metered".to_string(),
1617            hold_id: "hold-1".to_string(),
1618            budget_term: Some("term-7".to_string()),
1619            authority: Some(FinancialBudgetHoldAuthorityMetadata {
1620                authority_id: "http://leader-a".to_string(),
1621                lease_id: "lease-7".to_string(),
1622                lease_epoch: 7,
1623            }),
1624            authorize: FinancialBudgetAuthorizeReceiptMetadata {
1625                event_id: Some("hold-1:authorize".to_string()),
1626                budget_commit_index: Some(41),
1627                exposure_units: 120,
1628                committed_cost_units_after: 120,
1629            },
1630            terminal: Some(FinancialBudgetTerminalReceiptMetadata {
1631                disposition: "reconciled".to_string(),
1632                event_id: Some("hold-1:reconcile".to_string()),
1633                budget_commit_index: Some(42),
1634                exposure_units: 120,
1635                realized_spend_units: 75,
1636                committed_cost_units_after: 75,
1637            }),
1638        };
1639        let row = SettlementReconciliationRow {
1640            receipt_id: "rcpt-1".to_string(),
1641            timestamp: 42,
1642            capability_id: "cap-1".to_string(),
1643            subject_key: Some("subject-1".to_string()),
1644            tool_server: "payments".to_string(),
1645            tool_name: "charge".to_string(),
1646            payment_reference: Some("payref-1".to_string()),
1647            settlement_status: SettlementStatus::Pending,
1648            cost_charged: Some(75),
1649            currency: Some("usd".to_string()),
1650            budget_authority: Some(budget_authority.clone()),
1651            reconciliation_state: SettlementReconciliationState::Open,
1652            action_required: true,
1653            note: None,
1654            updated_at: None,
1655        };
1656
1657        let value = serde_json::to_value(&row).unwrap();
1658
1659        assert_eq!(
1660            value["budgetAuthority"]["guarantee_level"],
1661            serde_json::json!("ha_quorum_commit")
1662        );
1663        assert_eq!(
1664            value["budgetAuthority"]["hold_id"],
1665            serde_json::json!("hold-1")
1666        );
1667        assert_eq!(
1668            value["budgetAuthority"]["authority"]["authority_id"],
1669            serde_json::json!("http://leader-a")
1670        );
1671    }
1672
1673    #[test]
1674    fn metered_and_behavioral_rows_serialize_budget_authority_metadata() {
1675        let budget_authority = FinancialBudgetAuthorityReceiptMetadata {
1676            guarantee_level: "ha_quorum_commit".to_string(),
1677            authority_profile: "ha".to_string(),
1678            metering_profile: "metered".to_string(),
1679            hold_id: "hold-2".to_string(),
1680            budget_term: Some("term-8".to_string()),
1681            authority: Some(FinancialBudgetHoldAuthorityMetadata {
1682                authority_id: "http://leader-b".to_string(),
1683                lease_id: "lease-8".to_string(),
1684                lease_epoch: 8,
1685            }),
1686            authorize: FinancialBudgetAuthorizeReceiptMetadata {
1687                event_id: Some("hold-2:authorize".to_string()),
1688                budget_commit_index: Some(51),
1689                exposure_units: 150,
1690                committed_cost_units_after: 150,
1691            },
1692            terminal: None,
1693        };
1694
1695        let metered = MeteredBillingReconciliationRow {
1696            receipt_id: "rcpt-2".to_string(),
1697            timestamp: 99,
1698            capability_id: "cap-2".to_string(),
1699            subject_key: None,
1700            tool_server: "meter".to_string(),
1701            tool_name: "bill".to_string(),
1702            settlement_mode: MeteredSettlementMode::AllowThenSettle,
1703            provider: "provider-1".to_string(),
1704            quote_id: "quote-1".to_string(),
1705            billing_unit: "tokens".to_string(),
1706            quoted_units: 10,
1707            quoted_cost: MonetaryAmount {
1708                units: 50,
1709                currency: "USD".to_string(),
1710            },
1711            max_billed_units: Some(10),
1712            financial_cost_charged: Some(50),
1713            financial_currency: Some("USD".to_string()),
1714            budget_authority: Some(budget_authority.clone()),
1715            evidence: None,
1716            reconciliation_state: MeteredBillingReconciliationState::Open,
1717            action_required: true,
1718            evidence_missing: true,
1719            exceeds_quoted_units: false,
1720            exceeds_max_billed_units: false,
1721            exceeds_quoted_cost: false,
1722            financial_mismatch: false,
1723            note: None,
1724            updated_at: None,
1725        };
1726        let behavioral = BehavioralFeedReceiptRow {
1727            receipt_id: "rcpt-3".to_string(),
1728            timestamp: 100,
1729            capability_id: "cap-3".to_string(),
1730            subject_key: None,
1731            issuer_key: None,
1732            tool_server: "meter".to_string(),
1733            tool_name: "bill".to_string(),
1734            decision: Decision::Allow,
1735            settlement_status: SettlementStatus::Settled,
1736            reconciliation_state: SettlementReconciliationState::Reconciled,
1737            action_required: false,
1738            cost_charged: Some(50),
1739            attempted_cost: Some(60),
1740            currency: Some("USD".to_string()),
1741            budget_authority: Some(budget_authority),
1742            governed: None,
1743            governed_transaction_diagnostics: None,
1744            metered_reconciliation: None,
1745        };
1746
1747        let metered_value = serde_json::to_value(&metered).unwrap();
1748        let behavioral_value = serde_json::to_value(&behavioral).unwrap();
1749
1750        assert_eq!(
1751            metered_value["budgetAuthority"]["guarantee_level"],
1752            serde_json::json!("ha_quorum_commit")
1753        );
1754        assert_eq!(
1755            behavioral_value["budgetAuthority"]["hold_id"],
1756            serde_json::json!("hold-2")
1757        );
1758    }
1759}