1use super::*;
2
3#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
4#[serde(rename_all = "camelCase")]
5pub struct BehavioralFeedPrivacyBoundary {
6 pub matching_receipts: u64,
7 pub returned_receipts: u64,
8 pub direct_evidence_export_supported: bool,
9 pub child_receipt_scope: EvidenceChildReceiptScope,
10 pub proofs_complete: bool,
11 pub export_query: EvidenceExportQuery,
12 #[serde(default, skip_serializing_if = "Option::is_none")]
13 pub export_scope_note: Option<String>,
14}
15
16#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
17#[serde(rename_all = "camelCase")]
18pub struct BehavioralFeedDecisionSummary {
19 pub allow_count: u64,
20 pub deny_count: u64,
21 pub cancelled_count: u64,
22 pub incomplete_count: u64,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
26#[serde(rename_all = "camelCase")]
27pub struct BehavioralFeedSettlementSummary {
28 pub pending_receipts: u64,
29 pub settled_receipts: u64,
30 pub failed_receipts: u64,
31 pub not_applicable_receipts: u64,
32 pub actionable_receipts: u64,
33 pub reconciled_receipts: u64,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
37#[serde(rename_all = "camelCase")]
38pub struct BehavioralFeedGovernedActionSummary {
39 pub governed_receipts: u64,
40 pub approval_receipts: u64,
41 pub approved_receipts: u64,
42 pub commerce_receipts: u64,
43 pub max_amount_receipts: u64,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
47#[serde(rename_all = "camelCase")]
48pub struct BehavioralFeedMeteredBillingSummary {
49 pub metered_receipts: u64,
50 pub evidence_attached_receipts: u64,
51 pub missing_evidence_receipts: u64,
52 pub over_quoted_units_receipts: u64,
53 pub over_max_billed_units_receipts: u64,
54 pub over_quoted_cost_receipts: u64,
55 pub financial_mismatch_receipts: u64,
56 pub actionable_receipts: u64,
57 pub reconciled_receipts: u64,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
61#[serde(rename_all = "camelCase")]
62pub struct BehavioralFeedReputationSummary {
63 pub subject_key: String,
64 pub effective_score: f64,
65 pub probationary: bool,
66 #[serde(default, skip_serializing_if = "Option::is_none")]
67 pub resolved_tier: Option<String>,
68 pub imported_signal_count: usize,
69 pub accepted_imported_signal_count: usize,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
73#[serde(rename_all = "camelCase")]
74pub struct BehavioralFeedReceiptRow {
75 pub receipt_id: String,
76 pub timestamp: u64,
77 pub capability_id: String,
78 #[serde(default, skip_serializing_if = "Option::is_none")]
79 pub subject_key: Option<String>,
80 #[serde(default, skip_serializing_if = "Option::is_none")]
81 pub issuer_key: Option<String>,
82 pub tool_server: String,
83 pub tool_name: String,
84 #[serde(default, skip_serializing_if = "Option::is_none")]
85 pub decision: Option<Decision>,
86 #[serde(default)]
87 pub authorized: bool,
88 pub settlement_status: SettlementStatus,
89 pub reconciliation_state: SettlementReconciliationState,
90 pub action_required: bool,
91 #[serde(default, skip_serializing_if = "Option::is_none")]
92 pub cost_charged: Option<u64>,
93 #[serde(default, skip_serializing_if = "Option::is_none")]
94 pub attempted_cost: Option<u64>,
95 #[serde(default, skip_serializing_if = "Option::is_none")]
96 pub currency: Option<String>,
97 #[serde(default, skip_serializing_if = "Option::is_none")]
98 pub budget_authority: Option<FinancialBudgetAuthorityReceiptMetadata>,
99 #[serde(default, skip_serializing_if = "Option::is_none")]
100 pub governed: Option<GovernedTransactionReceiptMetadata>,
101 #[serde(default, skip_serializing_if = "Option::is_none")]
102 pub governed_transaction_diagnostics: Option<GovernedTransactionDiagnostics>,
103 #[serde(default, skip_serializing_if = "Option::is_none")]
104 pub metered_reconciliation: Option<BehavioralFeedMeteredBillingRow>,
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
108#[serde(rename_all = "camelCase")]
109pub struct BehavioralFeedMeteredBillingRow {
110 pub reconciliation_state: MeteredBillingReconciliationState,
111 pub action_required: bool,
112 pub evidence_missing: bool,
113 pub exceeds_quoted_units: bool,
114 pub exceeds_max_billed_units: bool,
115 pub exceeds_quoted_cost: bool,
116 pub financial_mismatch: bool,
117 #[serde(default, skip_serializing_if = "Option::is_none")]
118 pub evidence: Option<MeteredBillingEvidenceRecord>,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
122#[serde(rename_all = "camelCase")]
123pub struct BehavioralFeedReceiptSelection {
124 pub matching_receipts: u64,
125 pub receipts: Vec<BehavioralFeedReceiptRow>,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
129#[serde(rename_all = "camelCase")]
130pub struct BehavioralFeedReport {
131 pub schema: String,
132 pub generated_at: u64,
133 pub filters: BehavioralFeedQuery,
134 pub privacy: BehavioralFeedPrivacyBoundary,
135 pub decisions: BehavioralFeedDecisionSummary,
136 pub settlements: BehavioralFeedSettlementSummary,
137 pub governed_actions: BehavioralFeedGovernedActionSummary,
138 pub metered_billing: BehavioralFeedMeteredBillingSummary,
139 #[serde(default, skip_serializing_if = "Option::is_none")]
140 pub reputation: Option<BehavioralFeedReputationSummary>,
141 pub shared_evidence: SharedEvidenceReferenceSummary,
142 pub receipts: Vec<BehavioralFeedReceiptRow>,
143}
144
145pub type SignedBehavioralFeed = SignedExportEnvelope<BehavioralFeedReport>;
146
147#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
159#[serde(rename_all = "camelCase")]
160pub struct EmaBaselineState {
161 pub sample_count: u64,
163 pub ema_mean: f64,
165 pub ema_variance: f64,
167 pub last_update: u64,
169}
170
171impl EmaBaselineState {
172 pub fn update(&mut self, sample: f64, alpha: f64, now: u64) {
179 let alpha = alpha.clamp(f64::MIN_POSITIVE, 1.0);
180 if self.sample_count == 0 {
181 self.ema_mean = sample;
182 self.ema_variance = 0.0;
183 } else {
184 let prev_mean = self.ema_mean;
185 self.ema_mean = prev_mean + alpha * (sample - prev_mean);
186 let diff = sample - prev_mean;
188 self.ema_variance = (1.0 - alpha) * (self.ema_variance + alpha * diff * diff);
189 }
190 self.sample_count = self.sample_count.saturating_add(1);
191 self.last_update = now;
192 }
193
194 #[must_use]
196 pub fn stddev(&self) -> f64 {
197 self.ema_variance.max(0.0).sqrt()
198 }
199
200 #[must_use]
203 pub fn z_score(&self, sample: f64) -> Option<f64> {
204 if self.sample_count < 2 {
205 return None;
206 }
207 let stddev = self.stddev();
208 if stddev <= f64::EPSILON {
209 return None;
210 }
211 Some((sample - self.ema_mean) / stddev)
212 }
213}
214
215#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
219#[serde(rename_all = "camelCase")]
220pub struct BehavioralAnomalyScore {
221 pub agent_id: String,
223 pub baseline: EmaBaselineState,
225 pub current_sample: f64,
227 pub z_score: Option<f64>,
229 pub sigma_threshold: f64,
231 pub anomaly: bool,
233 pub generated_at: u64,
235}
236
237#[must_use]
242pub fn behavioral_anomaly_score(
243 agent_id: &str,
244 baseline: &EmaBaselineState,
245 current_sample: f64,
246 sigma_threshold: f64,
247 now: u64,
248) -> BehavioralAnomalyScore {
249 let z_score = baseline.z_score(current_sample);
250 let anomaly = z_score.is_some_and(|z| z.abs() > sigma_threshold);
251 BehavioralAnomalyScore {
252 agent_id: agent_id.to_string(),
253 baseline: baseline.clone(),
254 current_sample,
255 z_score,
256 sigma_threshold,
257 anomaly,
258 generated_at: now,
259 }
260}