1#![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}