Skip to main content

chio_underwriting/
lib.rs

1//! Underwriting decision, simulation, and appeal contracts for the Chio
2//! protocol.
3//!
4//! This crate computes underwriting decisions and premiums from signed Chio
5//! evidence; `chio-market` and `chio-credit` consume its outputs. It defines
6//! the risk taxonomy and reason codes, evidence references (receipt,
7//! reputation, certification), the deterministic premium-pricing model
8//! (`price_premium`, risk multipliers, decline reasons), and reputation-tiered
9//! marketplace credit limits. It builds on the appraisal surface.
10//!
11//! # Modules
12//!
13//! - [`decision`] -- decision policy, artifacts, evaluator, lists, and simulations.
14//! - [`premium`] -- premium pricing, risk multipliers, and decline floors.
15//! - [`marketplace_limits`] -- reputation-tiered marketplace credit limits.
16
17#![forbid(unsafe_code)]
18
19pub use chio_appraisal as appraisal;
20pub use chio_core_types::{canonical, capability, crypto, receipt};
21
22pub mod decision;
23pub mod marketplace_limits;
24pub mod premium;
25pub use decision::{
26    build_fiscal_underwriting_decision_artifact, build_underwriting_decision_artifact,
27    evaluate_underwriting_policy_input, self_test_fiscal_decision_premium_adapter,
28    FiscalDecisionPremiumSchedule, FiscalUnderwritingDecisionError, SignedUnderwritingDecision,
29    UnderwritingBudgetAction, UnderwritingBudgetRecommendation, UnderwritingDecisionArtifact,
30    UnderwritingDecisionFinding, UnderwritingDecisionLifecycleState,
31    UnderwritingDecisionListReport, UnderwritingDecisionOutcome, UnderwritingDecisionPolicy,
32    UnderwritingDecisionQuery, UnderwritingDecisionReasonCode, UnderwritingDecisionReport,
33    UnderwritingDecisionRow, UnderwritingDecisionSummary, UnderwritingPremiumQuote,
34    UnderwritingPremiumState, UnderwritingRemediation, UnderwritingReviewState,
35    UnderwritingSimulationDelta, UnderwritingSimulationReport, UnderwritingSimulationRequest,
36    APPROVE_PREMIUM_BASIS_POINTS, REDUCE_CEILING_PREMIUM_BASIS_POINTS,
37};
38pub use marketplace_limits::{
39    compute_fiscal_marketplace_credit_limit, compute_marketplace_credit_limit,
40    self_test_fiscal_tier_limits_adapter, FiscalTierLimits, MarketplaceCreditLimitDecision,
41    MarketplaceCreditLimitRequest, MarketplaceLimitTier, MARKETPLACE_TIER_LIMIT_CURRENCY,
42    MARKETPLACE_TIER_LIMIT_UNITS,
43};
44pub use premium::{
45    price_fiscal_premium, price_premium, risk_multiplier,
46    self_test_fiscal_insurance_premium_adapter, FiscalInsurancePremiumSchedule, LookbackWindow,
47    PremiumDeclineReason, PremiumInputs, PremiumQuote, DEFAULT_BEHAVIORAL_PENALTY_CAP,
48    DEFAULT_BEHAVIORAL_PENALTY_PER_SIGMA, PREMIUM_DECLINE_FLOOR, PREMIUM_HIGH_RISK_FLOOR,
49    PREMIUM_LOW_RISK_FLOOR, PREMIUM_MEDIUM_RISK_FLOOR,
50};
51
52use serde::{Deserialize, Serialize};
53
54use crate::appraisal::AttestationVerifierFamily;
55use crate::capability::runtime_attestation::RuntimeAssuranceTier;
56use crate::receipt::lineage::SignedExportEnvelope;
57
58pub const UNDERWRITING_POLICY_INPUT_SCHEMA: &str = "chio.underwriting.policy-input.v1";
59pub const UNDERWRITING_COMPLIANCE_EVIDENCE_SCHEMA: &str =
60    "chio.underwriting.compliance-evidence.v1";
61pub const UNDERWRITING_RISK_TAXONOMY_VERSION: &str = "chio.underwriting.taxonomy.v1";
62pub const UNDERWRITING_DECISION_POLICY_SCHEMA: &str = "chio.underwriting.decision-policy.v1";
63pub const UNDERWRITING_DECISION_POLICY_VERSION: &str =
64    "chio.underwriting.decision-policy.default.v1";
65pub const UNDERWRITING_DECISION_REPORT_SCHEMA: &str = "chio.underwriting.decision-report.v1";
66pub const UNDERWRITING_SIMULATION_REPORT_SCHEMA: &str = "chio.underwriting.simulation-report.v1";
67pub const UNDERWRITING_DECISION_ARTIFACT_SCHEMA: &str = "chio.underwriting.decision.v1";
68pub const UNDERWRITING_APPEAL_SCHEMA: &str = "chio.underwriting.appeal.v1";
69pub const MAX_UNDERWRITING_RECEIPT_LIMIT: usize = 200;
70pub const MAX_UNDERWRITING_DECISION_LIMIT: usize = 200;
71
72pub(crate) fn bounded_underwriting_limit(
73    limit: Option<usize>,
74    default: usize,
75    max: usize,
76) -> usize {
77    limit.unwrap_or(default).clamp(1, max)
78}
79
80fn validate_optional_underwriting_filter(value: Option<&str>, field: &str) -> Result<(), String> {
81    let Some(value) = value else {
82        return Ok(());
83    };
84    if value.trim().is_empty() {
85        return Err(format!("{field} must not be empty"));
86    }
87    if value.trim() != value {
88        return Err(format!("{field} must not contain surrounding whitespace"));
89    }
90    Ok(())
91}
92
93#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
94#[serde(rename_all = "snake_case")]
95pub enum UnderwritingRiskClass {
96    Baseline,
97    Guarded,
98    Elevated,
99    Critical,
100}
101
102#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
103#[serde(rename_all = "snake_case")]
104pub enum UnderwritingReasonCode {
105    ProbationaryHistory,
106    LowReputation,
107    ImportedTrustDependency,
108    MissingCertification,
109    FailedCertification,
110    RevokedCertification,
111    MissingRuntimeAssurance,
112    WeakRuntimeAssurance,
113    PendingSettlementExposure,
114    FailedSettlementExposure,
115    MeteredBillingMismatch,
116    DelegatedCallChain,
117    SharedEvidenceProofRequired,
118}
119
120#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
121#[serde(rename_all = "snake_case")]
122pub enum UnderwritingEvidenceKind {
123    Receipt,
124    ReputationInspection,
125    CertificationArtifact,
126    RuntimeAssuranceEvidence,
127    SettlementReconciliation,
128    MeteredBillingReconciliation,
129    SharedEvidenceReference,
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
133#[serde(rename_all = "camelCase")]
134pub struct UnderwritingEvidenceReference {
135    pub kind: UnderwritingEvidenceKind,
136    pub reference_id: String,
137    #[serde(default, skip_serializing_if = "Option::is_none")]
138    pub observed_at: Option<u64>,
139    #[serde(default, skip_serializing_if = "Option::is_none")]
140    pub digest_sha256: Option<String>,
141    #[serde(default, skip_serializing_if = "Option::is_none")]
142    pub locator: Option<String>,
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
146#[serde(rename_all = "camelCase")]
147pub struct UnderwritingSignal {
148    pub class: UnderwritingRiskClass,
149    pub reason: UnderwritingReasonCode,
150    pub description: String,
151    #[serde(default, skip_serializing_if = "Vec::is_empty")]
152    pub evidence_refs: Vec<UnderwritingEvidenceReference>,
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
156#[serde(rename_all = "snake_case")]
157pub enum UnderwritingCertificationState {
158    Active,
159    Superseded,
160    Revoked,
161    NotFound,
162    Unavailable,
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
166#[serde(rename_all = "camelCase")]
167pub struct UnderwritingRiskTaxonomy {
168    pub version: String,
169    pub supported_classes: Vec<UnderwritingRiskClass>,
170    pub supported_reasons: Vec<UnderwritingReasonCode>,
171}
172
173impl Default for UnderwritingRiskTaxonomy {
174    fn default() -> Self {
175        Self {
176            version: UNDERWRITING_RISK_TAXONOMY_VERSION.to_string(),
177            supported_classes: vec![
178                UnderwritingRiskClass::Baseline,
179                UnderwritingRiskClass::Guarded,
180                UnderwritingRiskClass::Elevated,
181                UnderwritingRiskClass::Critical,
182            ],
183            supported_reasons: vec![
184                UnderwritingReasonCode::ProbationaryHistory,
185                UnderwritingReasonCode::LowReputation,
186                UnderwritingReasonCode::ImportedTrustDependency,
187                UnderwritingReasonCode::MissingCertification,
188                UnderwritingReasonCode::FailedCertification,
189                UnderwritingReasonCode::RevokedCertification,
190                UnderwritingReasonCode::MissingRuntimeAssurance,
191                UnderwritingReasonCode::WeakRuntimeAssurance,
192                UnderwritingReasonCode::PendingSettlementExposure,
193                UnderwritingReasonCode::FailedSettlementExposure,
194                UnderwritingReasonCode::MeteredBillingMismatch,
195                UnderwritingReasonCode::DelegatedCallChain,
196                UnderwritingReasonCode::SharedEvidenceProofRequired,
197            ],
198        }
199    }
200}
201
202#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
203#[serde(rename_all = "camelCase")]
204pub struct UnderwritingReceiptEvidence {
205    pub matching_receipts: u64,
206    pub returned_receipts: u64,
207    pub allow_count: u64,
208    pub deny_count: u64,
209    pub cancelled_count: u64,
210    pub incomplete_count: u64,
211    pub governed_receipts: u64,
212    pub approval_receipts: u64,
213    pub approved_receipts: u64,
214    pub call_chain_receipts: u64,
215    pub runtime_assurance_receipts: u64,
216    pub pending_settlement_receipts: u64,
217    pub failed_settlement_receipts: u64,
218    pub actionable_settlement_receipts: u64,
219    pub metered_receipts: u64,
220    pub actionable_metered_receipts: u64,
221    pub shared_evidence_reference_count: u64,
222    pub shared_evidence_proof_required_count: u64,
223    #[serde(default, skip_serializing_if = "Vec::is_empty")]
224    pub receipt_refs: Vec<UnderwritingEvidenceReference>,
225}
226
227#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
228#[serde(rename_all = "camelCase")]
229pub struct UnderwritingReputationEvidence {
230    pub subject_key: String,
231    pub effective_score: f64,
232    pub probationary: bool,
233    #[serde(default, skip_serializing_if = "Option::is_none")]
234    pub resolved_tier: Option<String>,
235    pub imported_signal_count: usize,
236    pub accepted_imported_signal_count: usize,
237}
238
239#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
240#[serde(rename_all = "camelCase")]
241pub struct UnderwritingCertificationEvidence {
242    pub tool_server_id: String,
243    pub state: UnderwritingCertificationState,
244    #[serde(default, skip_serializing_if = "Option::is_none")]
245    pub artifact_id: Option<String>,
246    #[serde(default, skip_serializing_if = "Option::is_none")]
247    pub verdict: Option<String>,
248    #[serde(default, skip_serializing_if = "Option::is_none")]
249    pub checked_at: Option<u64>,
250    #[serde(default, skip_serializing_if = "Option::is_none")]
251    pub published_at: Option<u64>,
252}
253
254#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
255#[serde(rename_all = "camelCase")]
256pub struct UnderwritingRuntimeAssuranceEvidence {
257    pub governed_receipts: u64,
258    pub runtime_assurance_receipts: u64,
259    #[serde(default, skip_serializing_if = "Option::is_none")]
260    pub highest_tier: Option<RuntimeAssuranceTier>,
261    #[serde(default, skip_serializing_if = "Option::is_none")]
262    pub latest_schema: Option<String>,
263    #[serde(default, skip_serializing_if = "Option::is_none")]
264    pub latest_verifier_family: Option<AttestationVerifierFamily>,
265    #[serde(default, skip_serializing_if = "Option::is_none")]
266    pub latest_verifier: Option<String>,
267    #[serde(default, skip_serializing_if = "Option::is_none")]
268    pub latest_evidence_sha256: Option<String>,
269    #[serde(default, skip_serializing_if = "Vec::is_empty")]
270    pub observed_verifier_families: Vec<AttestationVerifierFamily>,
271}
272
273#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
274#[serde(rename_all = "camelCase")]
275pub struct UnderwritingComplianceEvidence {
276    pub schema: String,
277    pub agent_id: String,
278    pub score: u32,
279    pub generated_at: u64,
280    pub total_receipts: u64,
281    pub deny_receipts: u64,
282    pub observed_capabilities: u64,
283    pub revoked_capabilities: u64,
284    #[serde(default, skip_serializing_if = "Option::is_none")]
285    pub attestation_age_secs: Option<u64>,
286}
287
288#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
289#[serde(rename_all = "camelCase")]
290pub struct UnderwritingPolicyInputQuery {
291    #[serde(default, skip_serializing_if = "Option::is_none")]
292    pub capability_id: Option<String>,
293    #[serde(default, skip_serializing_if = "Option::is_none")]
294    pub agent_subject: Option<String>,
295    #[serde(default, skip_serializing_if = "Option::is_none")]
296    pub tool_server: Option<String>,
297    #[serde(default, skip_serializing_if = "Option::is_none")]
298    pub tool_name: Option<String>,
299    #[serde(default, skip_serializing_if = "Option::is_none")]
300    pub since: Option<u64>,
301    #[serde(default, skip_serializing_if = "Option::is_none")]
302    pub until: Option<u64>,
303    #[serde(default, skip_serializing_if = "Option::is_none")]
304    pub receipt_limit: Option<usize>,
305}
306
307impl Default for UnderwritingPolicyInputQuery {
308    fn default() -> Self {
309        Self {
310            capability_id: None,
311            agent_subject: None,
312            tool_server: None,
313            tool_name: None,
314            since: None,
315            until: None,
316            receipt_limit: Some(100),
317        }
318    }
319}
320
321impl UnderwritingPolicyInputQuery {
322    #[must_use]
323    pub fn receipt_limit_or_default(&self) -> usize {
324        bounded_underwriting_limit(self.receipt_limit, 100, MAX_UNDERWRITING_RECEIPT_LIMIT)
325    }
326
327    #[must_use]
328    pub fn normalized(&self) -> Self {
329        let mut normalized = self.clone();
330        normalized.receipt_limit = Some(self.receipt_limit_or_default());
331        normalized
332    }
333
334    pub fn validate(&self) -> Result<(), String> {
335        validate_optional_underwriting_filter(self.capability_id.as_deref(), "capability_id")?;
336        validate_optional_underwriting_filter(self.agent_subject.as_deref(), "agent_subject")?;
337        validate_optional_underwriting_filter(self.tool_server.as_deref(), "tool_server")?;
338        validate_optional_underwriting_filter(self.tool_name.as_deref(), "tool_name")?;
339        if self.capability_id.is_none()
340            && self.agent_subject.is_none()
341            && self.tool_server.is_none()
342        {
343            return Err(
344                "underwriting input queries require at least one anchor: --capability, --agent-subject, or --tool-server".to_string(),
345            );
346        }
347        if self.tool_name.is_some() && self.tool_server.is_none() {
348            return Err(
349                "underwriting input queries that specify --tool-name must also specify --tool-server"
350                    .to_string(),
351            );
352        }
353        if matches!((self.since, self.until), (Some(since), Some(until)) if since > until) {
354            return Err("underwriting input query has since > until".to_string());
355        }
356        Ok(())
357    }
358}
359
360#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
361#[serde(rename_all = "camelCase")]
362pub struct UnderwritingPolicyInput {
363    pub schema: String,
364    pub generated_at: u64,
365    pub filters: UnderwritingPolicyInputQuery,
366    pub taxonomy: UnderwritingRiskTaxonomy,
367    pub receipts: UnderwritingReceiptEvidence,
368    #[serde(default, skip_serializing_if = "Option::is_none")]
369    pub reputation: Option<UnderwritingReputationEvidence>,
370    #[serde(default, skip_serializing_if = "Option::is_none")]
371    pub certification: Option<UnderwritingCertificationEvidence>,
372    #[serde(default, skip_serializing_if = "Option::is_none")]
373    pub runtime_assurance: Option<UnderwritingRuntimeAssuranceEvidence>,
374    #[serde(default, skip_serializing_if = "Option::is_none")]
375    pub compliance_score: Option<UnderwritingComplianceEvidence>,
376    #[serde(default, skip_serializing_if = "Vec::is_empty")]
377    pub signals: Vec<UnderwritingSignal>,
378}
379
380pub type SignedUnderwritingPolicyInput = SignedExportEnvelope<UnderwritingPolicyInput>;
381
382#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
383#[serde(rename_all = "snake_case")]
384pub enum UnderwritingAppealStatus {
385    Open,
386    Accepted,
387    Rejected,
388}
389
390#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
391#[serde(rename_all = "camelCase")]
392pub struct UnderwritingAppealRecord {
393    pub schema: String,
394    pub appeal_id: String,
395    pub decision_id: String,
396    pub requested_by: String,
397    pub reason: String,
398    pub status: UnderwritingAppealStatus,
399    pub created_at: u64,
400    pub updated_at: u64,
401    #[serde(default, skip_serializing_if = "Option::is_none")]
402    pub note: Option<String>,
403    #[serde(default, skip_serializing_if = "Option::is_none")]
404    pub resolved_by: Option<String>,
405    #[serde(default, skip_serializing_if = "Option::is_none")]
406    pub replacement_decision_id: Option<String>,
407}
408
409#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
410#[serde(rename_all = "camelCase")]
411pub struct UnderwritingAppealCreateRequest {
412    pub decision_id: String,
413    pub requested_by: String,
414    pub reason: String,
415    #[serde(default, skip_serializing_if = "Option::is_none")]
416    pub note: Option<String>,
417}
418
419#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
420#[serde(rename_all = "snake_case")]
421pub enum UnderwritingAppealResolution {
422    Accepted,
423    Rejected,
424}
425
426#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
427#[serde(rename_all = "camelCase")]
428pub struct UnderwritingAppealResolveRequest {
429    pub appeal_id: String,
430    pub resolution: UnderwritingAppealResolution,
431    pub resolved_by: String,
432    #[serde(default, skip_serializing_if = "Option::is_none")]
433    pub note: Option<String>,
434    #[serde(default, skip_serializing_if = "Option::is_none")]
435    pub replacement_decision_id: Option<String>,
436}